Files
livrets/docs/superpowers/specs/2026-08-28-livrets-multiplication-game-design.md

130 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design : Livrets — jeu de multiplications (Kotlin Multiplatform)
Date : 2026-08-28
Statut : Validé par le produit (décisions enregistrées avec l'utilisateur)
## 1. Objectif
Application mobile Kotlin Multiplatform (Android + iOS) proposant un jeu d'entraînement aux tables de multiplication (1 à 12), avec partie paramétrable, feedback immédiat, statistiques de fin de partie, historique des parties et stats cumulées par table.
## 2. Décisions clés
| Décision | Choix |
|---|---|
| Plateformes | Android + iOS |
| UI | Compose Multiplatform (UI 100 % partagée), interface en français |
| Persistance | JSON (kotlinx.serialization) via okio, chemin par `expect/actual` |
| Navigation | État d'écran simple (sealed interface), pas de lib de navigation |
| DI | Injection manuelle par constructeur |
| Tables | Tirage aléatoire jusqu'à une table maximum paramétrable (1–12, défaut 12) |
| Temps écoulé | Question comptée comme fausse, passage automatique |
| Feedback | Auto-avance après 1,5 s |
| Historique | Parties passées + stats cumulées par table |
Rejeté : SQLDelight (surdimensionné pour le volume de données) ; UI natives SwiftUI/Compose Android par plateforme (deux UIs à maintenir).
## 3. Architecture
Structure Gradle KMP standard :
```
livrets/
├── composeApp/ # module KMP partagé
│ ├── commonMain/ # domaine, logique de jeu, UI Compose, persistance
│ ├── androidMain/ # chemin de stockage + Activity Android
│ └── iosMain/ # chemin de stockage + points d'entrée iOS
└── iosApp/ # projet Xcode hôte
```
Couches dans `commonMain` :
- **domain** : types purs (`GameConfig`, `Question`, `AnswerRecord`, `GameResult`) et `GameEngine` (machine à états, coroutines uniquement pour le temps). Zéro dépendance UI — testable à 100 %.
- **data** : `HistoryRepository` (sérialisation JSON + okio), agrégation des stats par table.
- **ui** : écrans Compose + `GameViewModel` racine pilotant un `sealed interface Screen { Home, Game, Results, History }`.
Timer : coroutine `Job` par question, annulée à la réponse ; tick de 100 ms pour la barre de temps restante. Génération de nombres via `kotlin.random.Random`.
## 4. Domaine & logique de jeu
### Types
```kotlin
data class GameConfig(
val questionCount: Int,
val secondsPerQuestion: Int,
val maxTable: Int, // ∈ 1..12 : les facteurs sont tirés dans 1..maxTable
)
sealed interface Question {
val table: Int // a ou b ∈ 1..config.maxTable
data class Direct(val a: Int, val b: Int) : Question // "a × b = ?" → a*b
data class MissingFactor(val a: Int, val b: Int) : Question // "a × ? = b" → facteur manquant
}
data class AnswerRecord(
val question: Question,
val givenAnswer: Int?, // null = temps écoulé sans réponse
val correctAnswer: Int,
val isCorrect: Boolean,
val durationMs: Long,
)
data class GameResult(
val date: kotlinx.datetime.Instant, // dépendance kotlinx-datetime
val config: GameConfig,
val records: List<AnswerRecord>,
// dérivés : score = records corrects, taux = score/N, temps moyen de réponse
)
```
### GameEngine (machine à états)
États : `Config → Playing(question courante, index, temps restant) → Feedback(dernier AnswerRecord, 1,5 s) → … → Finished(GameResult)`.
Événements : `AnswerSubmitted(n)`, `Timeout`, `Tick`, `FeedbackElapsed`.
Règles :
- Génération : `a`, `b` tirés dans 1..`config.maxTable` avec poids ∝ facteur (amendé le 2026-08-31 : cf. spec « Génération pondérée des questions ») ; forme `Direct` ou `MissingFactor` au hasard (~50/50) ; interdiction de répéter exactement la même question deux fois de suite.
- Pour `MissingFactor(a, b)` : la question s'affiche `a × ? = a*b`, la réponse attendue est `b`.
- Réponse correcte → `Feedback` vert 1,5 s → question suivante.
- Réponse fausse → `Feedback` rouge 1,5 s (bonne réponse affichée) → question suivante.
- Temps écoulé sans validation → `givenAnswer = null`, `isCorrect = false`, feedback rouge → question suivante.
- Fin de partie : construction du `GameResult`, persistance, écran Résultats.
### Validation de saisie
Pavé numérique uniquement : chiffres 0-9 + retour arrière ; la saisie est validée par le bouton "OK" (ou auto-validation à la fin du temps). Entrée vide + timeout = réponse nulle.
## 5. Écrans & flux UX
1. **Accueil / Configuration** : nombre de questions (stepper 5–50, défaut 10) ; temps par question (stepper 3–30 s, défaut 10) ; "Jusqu'à la table de…" (stepper 1–12, défaut 12) ; boutons "Jouer" et "Historique".
2. **Jeu** : question en grand (`7 × 8 = ?` ou `7 × ? = 56`), barre de temps, saisie en gros, pavé numérique 0-9 + effacement + OK. À la validation ou au timeout : flash vert ✓ / rouge ✗ avec la bonne réponse, pause 1,5 s, question suivante. Bouton quitter avec confirmation (abandon = partie non persistée ou persistée comme interrompue : non persistée, par simplicité).
3. **Résultats** : score `x/N`, % de réussite, temps moyen de réponse ; liste des réponses fausses (question, réponse donnée ou "temps écoulé", bonne réponse) ; boutons "Rejouer" (mêmes paramètres) et "Accueil".
4. **Historique** : deux onglets — "Parties" (date, config, score, trié du plus récent au plus ancien) et "Tables" (barre de taux de réussite par table 1..12, tables < 60 % en rouge ; une table sans donnée est grise).
Flux : Accueil → Jeu → Résultats → (Rejouer | Accueil | Historique).
## 6. Persistance
- `HistoryRepository` : liste de `GameResult` sérialisée en JSON dans un fichier du stockage app (okio `FileSystem`).
- Chemin du fichier : `expect fun historyFilePath(): Path` — `filesDir` sur Android, `NSDocumentDirectory` sur iOS.
- Écriture après chaque partie terminée (ajout à la liste, réécriture du fichier — le volume est faible).
- Stats par table : recalculées en mémoire depuis l'historique au chargement de l'écran (pas de cache).
- Pas de limite de rétention dans cette version (volume faible) ; écrans de consultation en lecture simple.
## 7. Tests
Dans `commonTest` (kotlin.test + coroutines-test) :
- `GameEngine` : réponse juste, réponse fausse, timeout (= fausse, `givenAnswer = null`), enchaînement feedback → question suivante, fin de partie après N questions, arrêt propre du chrono pendant le feedback.
- Générateur de questions : bornes 1..`maxTable` (testé pour plusieurs valeurs, dont 6 et 12), les deux formes produites, pas de doublon consécutif.
- Agrégation stats par table : taux corrects par table, tables sans données.
- `HistoryRepository` : aller-retour sérialisation/désérialisation (FileSystem en mémoire via okio fake).
## 8. Hors périmètre (v1)
- Sélection fine des tables (par ex. travailler uniquement 7, 8 et 9)
- Modes de jeu alternatifs (contre-la-montre global, endless…)
- Son, animations avancées, accessibilité fine
- Synchronisation cloud, multi-joueurs