6.6 KiB
Génération pondérée des questions — Plan d'implémentation
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remplacer le tirage uniforme des facteurs par une pondération progressive (P(k) ∝ k) pour éviter les questions trop faciles (1 × 2, 3 × ? = 3).
Architecture: Un seul fichier de production modifié : QuestionGenerator.kt. Chaque facteur est tiré indépendamment avec un poids linéaire sur 1..maxTable (tirage par cumul de poids, aucune boucle de rejet sur la difficulté). Formes Direct/MissingFactor et anti-doublon consécutif inchangés.
Tech Stack: Kotlin Multiplatform (commonMain/commonTest), kotlin.random.Random, kotlin.test, Gradle.
Spec: docs/superpowers/specs/2026-08-31-generation-ponderee-questions-design.md
Global Constraints
- Commande de test :
./gradlew :sharedUI:jvmTest(logique de jeu danscommonTest, kotlin.test) - Fichier de production modifié : uniquement
sharedUI/src/commonMain/kotlin/com/livrets/game/QuestionGenerator.kt - Comportements conservés : facteurs dans 1..maxTable ; formes
Direct/MissingFactor~50/50 ; pas de doublon consécutif ; terminaison garantie même pour maxTable = 1 (poids > 0 pour tout facteur) - Pas de dépendance nouvelle ; pas de commentaire dans le code
- Interface et docs du projet en français
Task 1: Générateur pondéré + tests
Files:
- Modify:
sharedUI/src/commonMain/kotlin/com/livrets/game/QuestionGenerator.kt:10-18 - Test:
sharedUI/src/commonTest/kotlin/com/livrets/game/QuestionGeneratorTest.kt - Modify:
docs/superpowers/specs/2026-08-28-livrets-multiplication-game-design.md:87(amendement de la règle de génération)
Interfaces:
-
Consumes:
Question.Direct(a, b),Question.MissingFactor(a, b)(existants, inchangés) -
Produces:
QuestionGenerator(random: Random) : QuestionSource— signature inchangée, seule la distribution change -
Step 1: Write the failing tests
Ajouter à la fin de la classe QuestionGeneratorTest (après le test affichage des deux formes) :
private fun factors(q: Question): Pair<Int, Int> = when (q) {
is Question.Direct -> q.a to q.b
is Question.MissingFactor -> q.a to q.b
}
@Test
fun `biais progressif - les facteurs eleves reviennent plus souvent`() {
val generator = QuestionGenerator(Random(42))
var with1 = 0
var with10 = 0
repeat(5000) {
val (a, b) = factors(generator.next(previous = null, maxTable = 10))
if (a == 1 || b == 1) with1++
if (a == 10 || b == 10) with10++
}
assertTrue(with1 < with10 / 5, "with1=$with1, with10=$with10")
}
@Test
fun `les petits facteurs restent possibles`() {
val generator = QuestionGenerator(Random(42))
var with1 = 0
repeat(5000) {
val (a, b) = factors(generator.next(previous = null, maxTable = 10))
if (a == 1 || b == 1) with1++
}
assertTrue(with1 > 0, "with1=$with1")
}
@Test
fun `maxTable egale a 1 - seule question possible sans boucle infinie`() {
val generator = QuestionGenerator(Random(42))
repeat(200) {
val (a, b) = factors(generator.next(previous = null, maxTable = 1))
assertEquals(1, a)
assertEquals(1, b)
}
}
- Step 2: Run tests to verify they fail
Run: ./gradlew :sharedUI:jvmTest
Expected: FAIL — biais progressif - les facteurs eleves reviennent plus souvent échoue (tirage uniforme : with1 ≈ with10 ≈ 19 % chacun, l'assertion with1 < with10 / 5 est fausse). Les autres tests (existants, les petits facteurs restent possibles, maxTable egale a 1) passent.
- Step 3: Write the implementation
Remplacer tout le contenu de sharedUI/src/commonMain/kotlin/com/livrets/game/QuestionGenerator.kt par :
package com.livrets.game
import kotlin.random.Random
fun interface QuestionSource {
fun next(previous: Question?, maxTable: Int): Question
}
class QuestionGenerator(private val random: Random = Random.Default) : QuestionSource {
override fun next(previous: Question?, maxTable: Int): Question {
while (true) {
val a = nextFactor(maxTable)
val b = nextFactor(maxTable)
val question =
if (random.nextBoolean()) Question.Direct(a, b) else Question.MissingFactor(a, b)
if (question != previous) return question
}
}
private fun nextFactor(maxTable: Int): Int {
val totalWeight = maxTable * (maxTable + 1) / 2
var roll = random.nextDouble() * totalWeight
for (k in 1..maxTable) {
roll -= k
if (roll < 0) return k
}
return maxTable
}
}
La boucle for consomme les poids cumulés (1, 2, …, maxTable) : chaque facteur k est retourné avec une probabilité k / (maxTable·(maxTable+1)/2). Le return maxTable final est un garde-fou contre l'arrondi flottant (roll ≥ 0 après soustraction de tous les poids). Aucun rejet sur la difficulté : terminaison garantie, y compris maxTable = 1.
- Step 4: Run the full test suite
Run: ./gradlew :sharedUI:jvmTest
Expected: PASS — les tests existants (bornes, formes produites, pas de doublon consécutif, engine, persistance) et les deux nouveaux tests passent.
- Step 5: Amend the original spec's generation rule
Dans docs/superpowers/specs/2026-08-28-livrets-multiplication-game-design.md, ligne 87, remplacer :
- 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.
par :
- 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.
- Step 6: Commit
git add sharedUI/src/commonMain/kotlin/com/livrets/game/QuestionGenerator.kt sharedUI/src/commonTest/kotlin/com/livrets/game/QuestionGeneratorTest.kt docs/superpowers/specs/2026-08-28-livrets-multiplication-game-design.md docs/superpowers/specs/2026-08-31-generation-ponderee-questions-design.md
git commit -m "feat: génération pondérée des questions (les grands facteurs sortent plus souvent)"