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

6.9 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 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

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 uniformément dans 1..config.maxTable (le joueur limite la difficulté en réduisant la table max) ; 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