docs: plan génération pondérée des questions

This commit is contained in:
herel committed 2026-08-31 09:33:04 +02:00
1 parent 5a87562e94
commit b454fbd0d7
2 files changed
+148 -1

No files matched your search

@@ -0,0 +1,146 @@
# 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 dans `commonTest`, 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`) :
```kotlin
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 :
```kotlin
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 :
```markdown
- 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 :
```markdown
- 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**
```bash
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)"
```
@@ -37,8 +37,9 @@ Mise à jour/ajouts :
1. Bornes : facteurs dans 1..maxTable (inchangé). 1. Bornes : facteurs dans 1..maxTable (inchangé).
2. Formes Direct et MissingFactor produites (inchangé). 2. Formes Direct et MissingFactor produites (inchangé).
3. Pas de doublon consécutif (inchangé). 3. Pas de doublon consécutif (inchangé).
4. **Nouveau** — biais progressif : avec une graine fixe et 5 000 tirages (maxTable = 10), la fréquence des questions contenant un facteur 1 est < 1/10 de celle contenant un facteur 10 (attendu ≈ 3,6 % vs ≈ 32,7 %). 4. **Nouveau** — biais progressif : avec une graine fixe et 5 000 tirages (maxTable = 10), la fréquence des questions contenant un facteur 1 est < 1/5 de celle contenant un facteur 10 (attendu ≈ 3,6 % vs ≈ 33,1 %).
5. **Nouveau** — les petits facteurs restent possibles : avec une graine fixe et 5 000 tirages (maxTable = 10), au moins une question contient un facteur 1 (attendu ≈ 3,6 %, soit ~180 occurrences). 5. **Nouveau** — les petits facteurs restent possibles : avec une graine fixe et 5 000 tirages (maxTable = 10), au moins une question contient un facteur 1 (attendu ≈ 3,6 %, soit ~180 occurrences).
6. **Nouveau** — maxTable = 1 : 200 appels à `next()` retournent tous une question à facteurs `1 × 1` (aucune boucle infinie).
## 6. Hors périmètre ## 6. Hors périmètre