docs: plan génération pondérée des questions
This commit is contained in:
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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user