docs: design spec pour le jeu de multiplications KMP
This commit is contained in:
1 file changed
+125
@@ -0,0 +1,125 @@
|
|||||||
|
# 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 | 1 à 12, tirage aléatoire, pas de sélection |
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
sealed interface Question {
|
||||||
|
val table: Int // a ou b ∈ 1..12
|
||||||
|
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 uniformément dans 1..12 ; 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) ; 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..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 des tables à travailler
|
||||||
|
- Modes de jeu alternatifs (contre-la-montre global, endless…)
|
||||||
|
- Son, animations avancées, accessibilité fine
|
||||||
|
- Synchronisation cloud, multi-joueurs
|
||||||
Reference in new issue
Block a user