7.0 KiB
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) etGameEngine(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 +
GameViewModelracine pilotant unsealed 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
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,btirés dans 1..config.maxTableavec poids ∝ facteur (amendé le 2026-08-31 : cf. spec « Génération pondérée des questions ») ; formeDirectouMissingFactorau hasard (~50/50) ; interdiction de répéter exactement la même question deux fois de suite. - Pour
MissingFactor(a, b): la question s'affichea × ? = a*b, la réponse attendue estb. - Réponse correcte →
Feedbackvert 1,5 s → question suivante. - Réponse fausse →
Feedbackrouge 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
- 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".
- Jeu : question en grand (
7 × 8 = ?ou7 × ? = 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é). - 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". - 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 deGameResultsérialisée en JSON dans un fichier du stockage app (okioFileSystem).- Chemin du fichier :
expect fun historyFilePath(): Path—filesDirsur Android,NSDocumentDirectorysur 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