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

6.6 KiB
Raw Blame History

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

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