diff --git a/autoload/exercise_engine.gd b/autoload/exercise_engine.gd index 69cfcbf..94dc998 100644 --- a/autoload/exercise_engine.gd +++ b/autoload/exercise_engine.gd @@ -70,6 +70,21 @@ func pick_exercise_for_mission( return picked if picked != null else {} +## Comme pick_exercise_for_mission, mais ne redonne jamais un exercice deja vu +## pendant la mission tant qu'il en reste un autre (un enfant ne doit pas voir +## deux fois la meme phrase dans une meme mission). +func pick_unseen_exercise_for_mission( + mission: Dictionary, seen_ids: Array[String], difficulty: int +) -> Dictionary: + var candidates: Array[Dictionary] = _candidates_for( + mission["rule_id"], mission["exercise_type"] + ) + var picked: Variant = ExercisePicker.pick_unseen( + _filter_by_difficulty(candidates, difficulty), candidates, seen_ids, _rng + ) + return picked if picked != null else {} + + ## Ne garde que les candidats a la difficulte demandee ; si aucun ne la ## propose, retombe sur l'ensemble des candidats plutot que de n'en donner ## aucun (le contenu disponible est encore limite, voir docs/specs/decisions.md). diff --git a/data/exercises/ce1/ce1_accord_adjectif_correction_01.json b/data/exercises/ce1/ce1_accord_adjectif_correction_01.json index f1ce2d5..0cd7611 100644 --- a/data/exercises/ce1/ce1_accord_adjectif_correction_01.json +++ b/data/exercises/ce1/ce1_accord_adjectif_correction_01.json @@ -8,18 +8,30 @@ "type": "sentence_correction", "instruction": "Touche le mot qui contient une erreur, puis corrige-le.", "tokens": [ - { "text": "Les", "correct": true }, + { + "text": "Les", + "correct": true + }, { "text": "petit", "correct": false, "expected": "petits", "rule_id": "regle_accord_adjectif" }, - { "text": "chiens", "correct": true }, - { "text": "courent.", "correct": true } + { + "text": "canards", + "correct": true + }, + { + "text": "nagent.", + "correct": true + } ], "hint": "Le déterminant « les » indique un pluriel.", - "explanation": "L'adjectif « petit » s'accorde avec le nom « chiens », qui est au pluriel : il faut écrire « petits ».", - "review_tags": ["pluriel", "accord_adjectif"], + "explanation": "L'adjectif « petit » s'accorde avec le nom « canards », qui est au pluriel : il faut écrire « petits ».", + "review_tags": [ + "pluriel", + "accord_adjectif" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_adjectif_qcm_01.json b/data/exercises/ce1/ce1_accord_adjectif_qcm_01.json index dc88178..75a4968 100644 --- a/data/exercises/ce1/ce1_accord_adjectif_qcm_01.json +++ b/data/exercises/ce1/ce1_accord_adjectif_qcm_01.json @@ -7,11 +7,18 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon groupe de mots.", - "question": "Les ... chiens jouent.", - "answers": ["petit", "petits", "petite"], + "question": "Les ... singes mangent.", + "answers": [ + "petit", + "petits", + "petite" + ], "correct_answer": 1, - "hint": "Observe le déterminant « les » et le nom « chiens ».", + "hint": "Observe le déterminant « les » et le nom « singes ».", "explanation": "Le groupe nominal est au pluriel. L'adjectif prend ici la marque du pluriel.", - "review_tags": ["pluriel", "accord_adjectif"], + "review_tags": [ + "pluriel", + "accord_adjectif" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_adjectif_qcm_04.json b/data/exercises/ce1/ce1_accord_adjectif_qcm_04.json index a73b2e2..c10d947 100644 --- a/data/exercises/ce1/ce1_accord_adjectif_qcm_04.json +++ b/data/exercises/ce1/ce1_accord_adjectif_qcm_04.json @@ -7,11 +7,19 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon groupe de mots.", - "question": "Les ... fleurs sentent bon.", - "answers": ["joli", "jolie", "jolies"], + "question": "Les ... roses sentent bon.", + "answers": [ + "joli", + "jolie", + "jolies" + ], "correct_answer": 2, - "hint": "Observe le déterminant « les » et le nom « fleurs ».", - "explanation": "Le nom « fleurs » est féminin pluriel. L'adjectif prend la marque du féminin et du pluriel : « jolies ».", - "review_tags": ["genre", "pluriel", "accord_adjectif"], + "hint": "Observe le déterminant « les » et le nom « roses ».", + "explanation": "Le nom « roses » est féminin pluriel. L'adjectif prend la marque du féminin et du pluriel : « jolies ».", + "review_tags": [ + "genre", + "pluriel", + "accord_adjectif" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_adjectif_qcm_05.json b/data/exercises/ce1/ce1_accord_adjectif_qcm_05.json index c6ca052..d877f51 100644 --- a/data/exercises/ce1/ce1_accord_adjectif_qcm_05.json +++ b/data/exercises/ce1/ce1_accord_adjectif_qcm_05.json @@ -7,11 +7,17 @@ "difficulty": 2, "type": "multiple_choice", "instruction": "Choisis le bon groupe de mots.", - "question": "Le chat est ... .", - "answers": ["petit", "petits", "petite"], + "question": "Le mouton est ... .", + "answers": [ + "petit", + "petits", + "petite" + ], "correct_answer": 0, - "hint": "Le nom « chat » est masculin singulier.", + "hint": "Le nom « mouton » est masculin singulier.", "explanation": "Au masculin singulier, l'adjectif ne prend ni « s » ni « e » : on écrit « petit ».", - "review_tags": ["accord_adjectif"], + "review_tags": [ + "accord_adjectif" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_adjectif_qcm_06.json b/data/exercises/ce1/ce1_accord_adjectif_qcm_06.json index ddc7342..c9cf80a 100644 --- a/data/exercises/ce1/ce1_accord_adjectif_qcm_06.json +++ b/data/exercises/ce1/ce1_accord_adjectif_qcm_06.json @@ -7,11 +7,18 @@ "difficulty": 2, "type": "multiple_choice", "instruction": "Choisis le bon groupe de mots.", - "question": "La robe est ... .", - "answers": ["joli", "jolie", "jolis"], + "question": "La jupe est ... .", + "answers": [ + "joli", + "jolie", + "jolis" + ], "correct_answer": 1, - "hint": "Le nom « robe » est féminin singulier.", - "explanation": "Le nom « robe » est féminin singulier. L'adjectif prend la marque du féminin : « jolie ».", - "review_tags": ["genre", "accord_adjectif"], + "hint": "Le nom « jupe » est féminin singulier.", + "explanation": "Le nom « jupe » est féminin singulier. L'adjectif prend la marque du féminin : « jolie ».", + "review_tags": [ + "genre", + "accord_adjectif" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_determinant_correction_01.json b/data/exercises/ce1/ce1_accord_determinant_correction_01.json index 5b2448a..0544c28 100644 --- a/data/exercises/ce1/ce1_accord_determinant_correction_01.json +++ b/data/exercises/ce1/ce1_accord_determinant_correction_01.json @@ -14,12 +14,24 @@ "expected": "La", "rule_id": "regle_accord_determinant" }, - { "text": "fleur", "correct": true }, - { "text": "est", "correct": true }, - { "text": "jolie.", "correct": true } + { + "text": "tulipe", + "correct": true + }, + { + "text": "est", + "correct": true + }, + { + "text": "jaune.", + "correct": true + } + ], + "hint": "Le nom « tulipe » est féminin.", + "explanation": "« Tulipe » est un nom féminin : il faut le déterminant féminin « La ».", + "review_tags": [ + "genre", + "accord_determinant" ], - "hint": "Le nom « fleur » est féminin.", - "explanation": "« Fleur » est un nom féminin : il faut le déterminant féminin « La ».", - "review_tags": ["genre", "accord_determinant"], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_determinant_qcm_04.json b/data/exercises/ce1/ce1_accord_determinant_qcm_04.json index f40cd85..0ed7ad6 100644 --- a/data/exercises/ce1/ce1_accord_determinant_qcm_04.json +++ b/data/exercises/ce1/ce1_accord_determinant_qcm_04.json @@ -7,11 +7,17 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon déterminant.", - "question": "... fleurs sentent bon.", - "answers": ["La", "Les"], + "question": "... pommes sentent bon.", + "answers": [ + "La", + "Les" + ], "correct_answer": 1, - "hint": "Le nom « fleurs » est au pluriel.", - "explanation": "« Fleurs » est au pluriel : il faut le déterminant pluriel « Les ».", - "review_tags": ["pluriel", "accord_determinant"], + "hint": "Le nom « pommes » est au pluriel.", + "explanation": "« Pommes » est au pluriel : il faut le déterminant pluriel « Les ».", + "review_tags": [ + "pluriel", + "accord_determinant" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_determinant_qcm_08.json b/data/exercises/ce1/ce1_accord_determinant_qcm_08.json index 67e472a..2374278 100644 --- a/data/exercises/ce1/ce1_accord_determinant_qcm_08.json +++ b/data/exercises/ce1/ce1_accord_determinant_qcm_08.json @@ -7,11 +7,17 @@ "difficulty": 3, "type": "multiple_choice", "instruction": "Choisis le bon déterminant.", - "question": "... maison est grande.", - "answers": ["Le", "La"], + "question": "... cabane est grande.", + "answers": [ + "Le", + "La" + ], "correct_answer": 1, - "hint": "« Maison » se termine par un -n, mais écoute bien son genre.", - "explanation": "« Maison » est un nom féminin : il faut le déterminant féminin « La ».", - "review_tags": ["genre", "accord_determinant"], + "hint": "Dis « une » ou « un » devant « cabane » pour trouver son genre.", + "explanation": "« Cabane » est un nom féminin : il faut le déterminant féminin « La ».", + "review_tags": [ + "genre", + "accord_determinant" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_pluriel_nom_correction_01.json b/data/exercises/ce1/ce1_pluriel_nom_correction_01.json index 21a0be5..107c8e8 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_correction_01.json +++ b/data/exercises/ce1/ce1_pluriel_nom_correction_01.json @@ -8,20 +8,38 @@ "type": "sentence_correction", "instruction": "Touche le mot qui contient une erreur, puis corrige-le.", "tokens": [ - { "text": "Les", "correct": true }, { - "text": "chat", + "text": "Les", + "correct": true + }, + { + "text": "lapin", "correct": false, - "expected": "chats", + "expected": "lapins", "rule_id": "regle_pluriel_nom" }, - { "text": "dorment", "correct": true }, - { "text": "dans", "correct": true }, - { "text": "le", "correct": true }, - { "text": "jardin.", "correct": true } + { + "text": "mangent", + "correct": true + }, + { + "text": "dans", + "correct": true + }, + { + "text": "le", + "correct": true + }, + { + "text": "pré.", + "correct": true + } + ], + "hint": "Le déterminant « les » indique plusieurs lapins.", + "explanation": "Le nom « lapin » doit prendre la marque du pluriel, un -s, car il est annoncé par « les » : il faut écrire « lapins ».", + "review_tags": [ + "pluriel", + "pluriel_nom" ], - "hint": "Le déterminant « les » indique plusieurs chats.", - "explanation": "Le nom « chat » doit prendre la marque du pluriel, un -s, car il est annoncé par « les » : il faut écrire « chats ».", - "review_tags": ["pluriel", "pluriel_nom"], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_pluriel_nom_correction_04.json b/data/exercises/ce1/ce1_pluriel_nom_correction_04.json index e088592..e2145ac 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_correction_04.json +++ b/data/exercises/ce1/ce1_pluriel_nom_correction_04.json @@ -8,21 +8,42 @@ "type": "sentence_correction", "instruction": "Touche le mot qui contient une erreur, puis corrige-le.", "tokens": [ - { "text": "Les", "correct": true }, - { "text": "petits", "correct": true }, { - "text": "chat", + "text": "Les", + "correct": true + }, + { + "text": "gros", + "correct": true + }, + { + "text": "poisson", "correct": false, - "expected": "chats", + "expected": "poissons", "rule_id": "regle_pluriel_nom" }, - { "text": "jouent", "correct": true }, - { "text": "dans", "correct": true }, - { "text": "le", "correct": true }, - { "text": "jardin.", "correct": true } + { + "text": "nagent", + "correct": true + }, + { + "text": "dans", + "correct": true + }, + { + "text": "la", + "correct": true + }, + { + "text": "mare.", + "correct": true + } ], "hint": "Cherche le déterminant, même si un autre mot s'intercale avant le nom.", - "explanation": "« Les » indique le pluriel, même si l'adjectif « petits » s'intercale avant le nom : il faut écrire « chats ».", - "review_tags": ["pluriel", "pluriel_nom"], + "explanation": "« Les » indique le pluriel, même si l'adjectif « gros » s'intercale avant le nom : il faut écrire « poissons ».", + "review_tags": [ + "pluriel", + "pluriel_nom" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_pluriel_nom_qcm_07.json b/data/exercises/ce1/ce1_pluriel_nom_qcm_07.json index 12966e2..8ac6c23 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_qcm_07.json +++ b/data/exercises/ce1/ce1_pluriel_nom_qcm_07.json @@ -7,11 +7,17 @@ "difficulty": 3, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Les petits ... courent vite.", - "answers": ["chien", "chiens"], + "question": "Les grands ... volent haut.", + "answers": [ + "oiseau", + "oiseaux" + ], "correct_answer": 1, "hint": "Cherche le déterminant, même si un autre mot s'intercale avant le nom.", - "explanation": "« Les » indique le pluriel, même si l'adjectif « petits » s'intercale avant le nom : il faut écrire « chiens ».", - "review_tags": ["pluriel", "pluriel_nom"], + "explanation": "« Les » indique le pluriel, même si l'adjectif « grands » s'intercale avant le nom : il faut écrire « oiseaux ».", + "review_tags": [ + "pluriel", + "pluriel_nom" + ], "review_status": "draft" } diff --git a/data/exercises/ce2/ce2_a_a_qcm_02.json b/data/exercises/ce2/ce2_a_a_qcm_02.json index 578ea1c..468df53 100644 --- a/data/exercises/ce2/ce2_a_a_qcm_02.json +++ b/data/exercises/ce2/ce2_a_a_qcm_02.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Nous allons ... la plage.", + "question": "Elle va ... la boulangerie.", "answers": [ "a", "à" diff --git a/data/exercises/ce2/ce2_et_est_qcm_03.json b/data/exercises/ce2/ce2_et_est_qcm_03.json index 060bccd..655d4b8 100644 --- a/data/exercises/ce2/ce2_et_est_qcm_03.json +++ b/data/exercises/ce2/ce2_et_est_qcm_03.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Ma maison ... grande.", + "question": "Ma chambre ... grande.", "answers": [ "et", "est" diff --git a/data/exercises/ce2/ce2_et_est_qcm_06.json b/data/exercises/ce2/ce2_et_est_qcm_06.json index 7f532f3..cff0ce7 100644 --- a/data/exercises/ce2/ce2_et_est_qcm_06.json +++ b/data/exercises/ce2/ce2_et_est_qcm_06.json @@ -7,7 +7,7 @@ "difficulty": 2, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Le gâteau ... sur la table.", + "question": "Le gâteau ... dans le four.", "answers": [ "et", "est" diff --git a/docs/review/content-review.md b/docs/review/content-review.md index 77d9d5b..43ea9c1 100644 --- a/docs/review/content-review.md +++ b/docs/review/content-review.md @@ -12,10 +12,10 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce1_accord_adjectif_qcm_01** (difficulte 1) : Les ... chiens jouent. +- **ce1_accord_adjectif_qcm_01** (difficulte 1) : Les ... singes mangent. - Reponses : petit, petits, petite - Reponse attendue : petits - - Indice : Observe le déterminant « les » et le nom « chiens ». + - Indice : Observe le déterminant « les » et le nom « singes ». - Explication : Le groupe nominal est au pluriel. L'adjectif prend ici la marque du pluriel. - Statut : draft - **ce1_accord_adjectif_qcm_02** (difficulte 1) : La ... maison est jolie. @@ -30,23 +30,23 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Observe le déterminant « les » et le nom « jardins ». - Explication : Le nom « jardins » est masculin pluriel. L'adjectif prend la marque du pluriel : « jolis ». - Statut : draft -- **ce1_accord_adjectif_qcm_04** (difficulte 1) : Les ... fleurs sentent bon. +- **ce1_accord_adjectif_qcm_04** (difficulte 1) : Les ... roses sentent bon. - Reponses : joli, jolie, jolies - Reponse attendue : jolies - - Indice : Observe le déterminant « les » et le nom « fleurs ». - - Explication : Le nom « fleurs » est féminin pluriel. L'adjectif prend la marque du féminin et du pluriel : « jolies ». + - Indice : Observe le déterminant « les » et le nom « roses ». + - Explication : Le nom « roses » est féminin pluriel. L'adjectif prend la marque du féminin et du pluriel : « jolies ». - Statut : draft -- **ce1_accord_adjectif_qcm_05** (difficulte 2) : Le chat est ... . +- **ce1_accord_adjectif_qcm_05** (difficulte 2) : Le mouton est ... . - Reponses : petit, petits, petite - Reponse attendue : petit - - Indice : Le nom « chat » est masculin singulier. + - Indice : Le nom « mouton » est masculin singulier. - Explication : Au masculin singulier, l'adjectif ne prend ni « s » ni « e » : on écrit « petit ». - Statut : draft -- **ce1_accord_adjectif_qcm_06** (difficulte 2) : La robe est ... . +- **ce1_accord_adjectif_qcm_06** (difficulte 2) : La jupe est ... . - Reponses : joli, jolie, jolis - Reponse attendue : jolie - - Indice : Le nom « robe » est féminin singulier. - - Explication : Le nom « robe » est féminin singulier. L'adjectif prend la marque du féminin : « jolie ». + - Indice : Le nom « jupe » est féminin singulier. + - Explication : Le nom « jupe » est féminin singulier. L'adjectif prend la marque du féminin : « jolie ». - Statut : draft - **ce1_accord_adjectif_qcm_07** (difficulte 3) : Les chats de la maison sont ... . - Reponses : petit, petits, petite @@ -63,9 +63,9 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Corrections de phrase -- **ce1_accord_adjectif_correction_01** (difficulte 2) : Les [petit -> petits] chiens courent. +- **ce1_accord_adjectif_correction_01** (difficulte 2) : Les [petit -> petits] canards nagent. - Indice : Le déterminant « les » indique un pluriel. - - Explication : L'adjectif « petit » s'accorde avec le nom « chiens », qui est au pluriel : il faut écrire « petits ». + - Explication : L'adjectif « petit » s'accorde avec le nom « canards », qui est au pluriel : il faut écrire « petits ». - Statut : draft - **ce1_accord_adjectif_correction_02** (difficulte 1) : Les enfants [sage -> sages] jouent dehors. - Indice : Le nom « enfants » est au pluriel. @@ -123,11 +123,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le nom « oiseaux » est au pluriel. - Explication : « Oiseaux » est au pluriel : il faut le déterminant pluriel « Les ». - Statut : draft -- **ce1_accord_determinant_qcm_04** (difficulte 1) : ... fleurs sentent bon. +- **ce1_accord_determinant_qcm_04** (difficulte 1) : ... pommes sentent bon. - Reponses : La, Les - Reponse attendue : Les - - Indice : Le nom « fleurs » est au pluriel. - - Explication : « Fleurs » est au pluriel : il faut le déterminant pluriel « Les ». + - Indice : Le nom « pommes » est au pluriel. + - Explication : « Pommes » est au pluriel : il faut le déterminant pluriel « Les ». - Statut : draft - **ce1_accord_determinant_qcm_05** (difficulte 2) : J'ai ... chat noir. - Reponses : un, une @@ -147,18 +147,18 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : « Arbre » se termine par un -e, mais il est masculin. - Explication : « Arbre » est un nom masculin, même s'il se termine par -e : il faut le déterminant masculin « Le ». - Statut : draft -- **ce1_accord_determinant_qcm_08** (difficulte 3) : ... maison est grande. +- **ce1_accord_determinant_qcm_08** (difficulte 3) : ... cabane est grande. - Reponses : Le, La - Reponse attendue : La - - Indice : « Maison » se termine par un -n, mais écoute bien son genre. - - Explication : « Maison » est un nom féminin : il faut le déterminant féminin « La ». + - Indice : Dis « une » ou « un » devant « cabane » pour trouver son genre. + - Explication : « Cabane » est un nom féminin : il faut le déterminant féminin « La ». - Statut : draft ### Corrections de phrase -- **ce1_accord_determinant_correction_01** (difficulte 1) : [Le -> La] fleur est jolie. - - Indice : Le nom « fleur » est féminin. - - Explication : « Fleur » est un nom féminin : il faut le déterminant féminin « La ». +- **ce1_accord_determinant_correction_01** (difficulte 1) : [Le -> La] tulipe est jaune. + - Indice : Le nom « tulipe » est féminin. + - Explication : « Tulipe » est un nom féminin : il faut le déterminant féminin « La ». - Statut : draft - **ce1_accord_determinant_correction_02** (difficulte 1) : [La -> Le] chat dort sous la table. - Indice : Le nom « chat » est masculin. @@ -204,7 +204,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Essaie de remplacer par « avait ». - Explication : « avait » convient ici : c'est le verbe avoir, on écrit « a » sans accent. - Statut : draft -- **ce2_a_a_qcm_02** (difficulte 1) : Nous allons ... la plage. +- **ce2_a_a_qcm_02** (difficulte 1) : Elle va ... la boulangerie. - Reponses : a, à - Reponse attendue : à - Indice : Essaie de remplacer par « avait ». @@ -303,7 +303,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Essaie de remplacer par « était ». - Explication : « était » ne convient pas ici, mais « et puis » convient : c'est « et », qui relie deux mots. - Statut : draft -- **ce2_et_est_qcm_03** (difficulte 1) : Ma maison ... grande. +- **ce2_et_est_qcm_03** (difficulte 1) : Ma chambre ... grande. - Reponses : et, est - Reponse attendue : est - Indice : Essaie de remplacer par « était ». @@ -321,7 +321,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Essaie de remplacer par « était ». - Explication : « était » ne convient pas ici, mais « et puis » convient : c'est « et », qui relie deux mots. - Statut : draft -- **ce2_et_est_qcm_06** (difficulte 2) : Le gâteau ... sur la table. +- **ce2_et_est_qcm_06** (difficulte 2) : Le gâteau ... dans le four. - Reponses : et, est - Reponse attendue : est - Indice : Essaie de remplacer par « était ». @@ -606,11 +606,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le déterminant « la » indique une seule porte. - Explication : « La » indique un seul objet : le nom reste au singulier, sans -s : « porte ». - Statut : draft -- **ce1_pluriel_nom_qcm_07** (difficulte 3) : Les petits ... courent vite. - - Reponses : chien, chiens - - Reponse attendue : chiens +- **ce1_pluriel_nom_qcm_07** (difficulte 3) : Les grands ... volent haut. + - Reponses : oiseau, oiseaux + - Reponse attendue : oiseaux - Indice : Cherche le déterminant, même si un autre mot s'intercale avant le nom. - - Explication : « Les » indique le pluriel, même si l'adjectif « petits » s'intercale avant le nom : il faut écrire « chiens ». + - Explication : « Les » indique le pluriel, même si l'adjectif « grands » s'intercale avant le nom : il faut écrire « oiseaux ». - Statut : draft - **ce1_pluriel_nom_qcm_08** (difficulte 3) : Les belles ... brillent dans le ciel. - Reponses : étoile, étoiles @@ -621,9 +621,9 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Corrections de phrase -- **ce1_pluriel_nom_correction_01** (difficulte 1) : Les [chat -> chats] dorment dans le jardin. - - Indice : Le déterminant « les » indique plusieurs chats. - - Explication : Le nom « chat » doit prendre la marque du pluriel, un -s, car il est annoncé par « les » : il faut écrire « chats ». +- **ce1_pluriel_nom_correction_01** (difficulte 1) : Les [lapin -> lapins] mangent dans le pré. + - Indice : Le déterminant « les » indique plusieurs lapins. + - Explication : Le nom « lapin » doit prendre la marque du pluriel, un -s, car il est annoncé par « les » : il faut écrire « lapins ». - Statut : draft - **ce1_pluriel_nom_correction_02** (difficulte 1) : Des [pomme -> pommes] sont dans le panier. - Indice : Le déterminant « des » indique plusieurs pommes. @@ -633,9 +633,9 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le déterminant « le » indique un seul chien. - Explication : « Le » indique un seul animal : le nom reste au singulier, sans -s : « chien ». - Statut : draft -- **ce1_pluriel_nom_correction_04** (difficulte 3) : Les petits [chat -> chats] jouent dans le jardin. +- **ce1_pluriel_nom_correction_04** (difficulte 3) : Les gros [poisson -> poissons] nagent dans la mare. - Indice : Cherche le déterminant, même si un autre mot s'intercale avant le nom. - - Explication : « Les » indique le pluriel, même si l'adjectif « petits » s'intercale avant le nom : il faut écrire « chats ». + - Explication : « Les » indique le pluriel, même si l'adjectif « gros » s'intercale avant le nom : il faut écrire « poissons ». - Statut : draft ### Dictees diff --git a/docs/specs/decisions.md b/docs/specs/decisions.md index 80d5010..7e692e3 100644 --- a/docs/specs/decisions.md +++ b/docs/specs/decisions.md @@ -346,3 +346,11 @@ Ajouts à l'arborescence proposée : **Vérification** : copie des vrais profils de l'utilisateur dans un profil temporaire, puis parcours de chaque écran (récompense, carte, collection, espace adulte, réglages, introduction, choix de profil) et chargement de toutes les missions de leur niveau : aucune erreur de script. À retenir : tout état relu de JSON doit être converti avant d'être affecté à un tableau typé. **Son (non reproduit)** : sur la machine de développement, les exports Linux (4.6.3 et 4.7.2) jouent bien la voix de la dictée par le parcours réel (flux PipeWire non coupé, niveau mesuré sur le moniteur du périphérique). Cause de l'absence de son chez l'utilisateur non identifiée ; diagnostic demandé (pilote audio, sortie par défaut, messages console). + +## 36. Redites : jamais deux fois le même exercice dans une mission (version 0.5.5) + +**Cause du « même énoncé deux fois »** : le tirage n'excluait que le dernier exercice vu, donc une mission de 3 exercices pouvait donner A, B, A (le point 34 n'avait traité que les doublons de contenu). Le tirage exclut maintenant tous les exercices déjà vus dans la mission (`ExercisePicker.pick_unseen`, `ExerciseEngine.pick_unseen_exercise_for_mission`) : d'abord à la difficulté visée, puis toutes difficultés, et ne répète que si le contenu est épuisé. Test de non-régression sur l'hôte d'exercice (10 missions simulées). + +**Phrases quasi identiques** : 14 énoncés (QCM et corrections) trop proches d'une autre phrase (ex. « Les petits chiens courent. » / « Les petits chiens jouent. » / « Les petits chiens courent vite. ») ont été remplacés par des phrases originales distinctes, indices et explications ajustés. Garde-fou permanent `tests/data/test_no_near_duplicate_sentences.gd` : deux phrases d'au moins 4 mots ne peuvent pas partager 75 % ou plus de leurs mots. Les dictées (audio déjà généré) n'ont pas été modifiées. + +**Voix** : l'utilisateur confirme que la voix de synthèse (« La fleur est belle. ») fonctionne avec le build Linux. diff --git a/project.godot b/project.godot index ab2be91..7ae2814 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Les Aventuriers des mots" -config/version="0.5.4" +config/version="0.5.5" run/main_scene="res://scenes/boot/boot.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" diff --git a/scenes/exercises/exercise_host.gd b/scenes/exercises/exercise_host.gd index 6b3e02c..106d4ce 100644 --- a/scenes/exercises/exercise_host.gd +++ b/scenes/exercises/exercise_host.gd @@ -22,7 +22,7 @@ var _current_exercise_scene: Control = null var _current_difficulty := 1 var _attempt_number := 1 var _completed_count := 0 -var _last_exercise_id := "" +var _seen_exercise_ids: Array[String] = [] var _outcomes: Array[String] = [] var _pending_outcome := "" @@ -97,9 +97,10 @@ func _on_rule_panel_continued(rule_panel: Control) -> void: func _load_next_exercise() -> void: - _current_exercise = ExerciseEngine.pick_exercise_for_mission( - _mission, _last_exercise_id, _current_difficulty + _current_exercise = ExerciseEngine.pick_unseen_exercise_for_mission( + _mission, _seen_exercise_ids, _current_difficulty ) + _seen_exercise_ids.append(_current_exercise.get("id", "")) _attempt_number = 1 _update_progress_label() @@ -196,7 +197,6 @@ func _on_feedback_continued(feedback: Control, is_terminal: bool) -> void: _current_difficulty = ProgressManager.next_difficulty_for_skill( _skill_id, _current_difficulty ) - _last_exercise_id = _current_exercise.get("id", "") _completed_count += 1 _outcomes.append(_pending_outcome) diff --git a/scripts/data/exercise_picker.gd b/scripts/data/exercise_picker.gd index 31470d1..ba2881e 100644 --- a/scripts/data/exercise_picker.gd +++ b/scripts/data/exercise_picker.gd @@ -23,3 +23,24 @@ static func pick( pool = candidates return pool[rng.randi_range(0, pool.size() - 1)] + + +## Tire un exercice qui n'a pas encore ete vu pendant la mission : d'abord +## parmi `preferred` (la difficulte visee), puis parmi `all` si tous les +## preferes ont ete vus. Quand tout a ete vu (mission plus longue que le +## contenu disponible), retombe sur `pick` en evitant au moins le dernier vu. +static func pick_unseen( + preferred: Array[Dictionary], + all: Array[Dictionary], + seen_ids: Array[String], + rng: RandomNumberGenerator +) -> Variant: + for source in [preferred, all]: + var pool: Array[Dictionary] = [] + for candidate: Dictionary in source: + if not seen_ids.has(candidate["id"]): + pool.append(candidate) + if not pool.is_empty(): + return pool[rng.randi_range(0, pool.size() - 1)] + var last := "" if seen_ids.is_empty() else seen_ids[seen_ids.size() - 1] + return pick(preferred, last, rng) diff --git a/tests/data/test_exercise_picker.gd b/tests/data/test_exercise_picker.gd index 649c05f..8bb253f 100644 --- a/tests/data/test_exercise_picker.gd +++ b/tests/data/test_exercise_picker.gd @@ -31,3 +31,25 @@ func test_returns_the_only_candidate_even_if_excluded() -> void: var candidates: Array[Dictionary] = [{"id": "a"}] var picked: Dictionary = ExercisePicker.pick(candidates, "a", rng) assert_eq(picked["id"], "a") + + +func test_pick_unseen_never_repeats_while_others_remain() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = 3 + var candidates: Array[Dictionary] = [{"id": "a"}, {"id": "b"}, {"id": "c"}] + var seen: Array[String] = [] + for _i in range(3): + var picked: Dictionary = ExercisePicker.pick_unseen(candidates, candidates, seen, rng) + assert_false(seen.has(picked["id"]), "un exercice deja vu a ete redonne") + seen.append(picked["id"]) + assert_eq(seen.size(), 3) + + +func test_pick_unseen_falls_back_to_other_difficulty_then_to_repeat() -> void: + var rng := RandomNumberGenerator.new() + var preferred: Array[Dictionary] = [{"id": "a"}] + var all: Array[Dictionary] = [{"id": "a"}, {"id": "b"}] + var picked: Dictionary = ExercisePicker.pick_unseen(preferred, all, ["a"], rng) + assert_eq(picked["id"], "b") + var again: Dictionary = ExercisePicker.pick_unseen(preferred, all, ["a", "b"], rng) + assert_true(["a", "b"].has(again["id"])) diff --git a/tests/data/test_no_near_duplicate_sentences.gd b/tests/data/test_no_near_duplicate_sentences.gd new file mode 100644 index 0000000..4367ed7 --- /dev/null +++ b/tests/data/test_no_near_duplicate_sentences.gd @@ -0,0 +1,100 @@ +extends GutTest +## Deux phrases qui ne different que d'un mot (« Les petits chiens courent. » +## / « Les petits chiens jouent. ») donnent a l'enfant l'impression d'une +## redite. On compare les ensembles de mots : au-dela de 75 % de mots communs +## (phrases d'au moins 4 mots), les deux phrases sont jugees trop proches. + +const EXERCISES_DIR := "res://data/exercises" +const DICTATIONS_DIR := "res://data/dictations" +const MAX_SIMILARITY := 0.75 +const MIN_WORDS := 4 + + +func test_no_two_sentences_are_nearly_identical() -> void: + var entries := _collect() + var close: Array[String] = [] + for i in range(entries.size()): + for j in range(i + 1, entries.size()): + var a: Array = entries[i]["words"] + var b: Array = entries[j]["words"] + if a.size() < MIN_WORDS or b.size() < MIN_WORDS: + continue + var common := 0 + for word: String in a: + if b.has(word): + common += 1 + var similarity := float(common) / float(maxi(a.size(), b.size())) + if similarity >= MAX_SIMILARITY: + close.append( + ( + "%s « %s » ~ %s « %s »" + % [ + entries[i]["id"], + entries[i]["text"], + entries[j]["id"], + entries[j]["text"] + ] + ) + ) + assert_eq(close.size(), 0, "Phrases trop proches :\n%s" % "\n".join(close)) + assert_gt(entries.size(), 100) + + +func _collect() -> Array[Dictionary]: + var entries: Array[Dictionary] = [] + for path in _json_files(EXERCISES_DIR): + var data := _read(path) + var sentence := "" + if data.get("type", "") == "multiple_choice": + var answers: Array = data["answers"] + sentence = str(data["question"]).replace( + "...", str(answers[int(data["correct_answer"])]) + ) + elif data.get("type", "") == "sentence_correction": + var words: Array[String] = [] + for token: Dictionary in data["tokens"]: + words.append(token["text"] if token["correct"] else token["expected"]) + sentence = " ".join(words) + if not sentence.is_empty(): + entries.append({"id": data["id"], "text": sentence, "words": _words(sentence)}) + for path in _json_files(DICTATIONS_DIR): + var data := _read(path) + var text: String = data["target_text"] + entries.append({"id": data["id"], "text": text, "words": _words(text)}) + return entries + + +## Mots distincts, en minuscules, sans ponctuation. +func _words(sentence: String) -> Array[String]: + var cleaned := "" + for character in sentence.to_lower(): + if character == "'" or character == " ": + cleaned += " " + elif character.to_upper() != character or "éèêàâîïôûùçœ".contains(character): + cleaned += character + else: + cleaned += " " + var result: Array[String] = [] + for word in cleaned.split(" ", false): + if not result.has(word): + result.append(word) + return result + + +func _json_files(path: String) -> Array[String]: + var found: Array[String] = [] + var dir := DirAccess.open(path) + if dir == null: + return found + for sub in dir.get_directories(): + found.append_array(_json_files(path.path_join(sub))) + for file_name in dir.get_files(): + if file_name.ends_with(".json"): + found.append(path.path_join(file_name)) + return found + + +func _read(path: String) -> Dictionary: + var file := FileAccess.open(path, FileAccess.READ) + var parsed: Variant = JSON.parse_string(file.get_as_text()) + return parsed if typeof(parsed) == TYPE_DICTIONARY else {} diff --git a/tests/data/test_no_near_duplicate_sentences.gd.uid b/tests/data/test_no_near_duplicate_sentences.gd.uid new file mode 100644 index 0000000..cc6aa55 --- /dev/null +++ b/tests/data/test_no_near_duplicate_sentences.gd.uid @@ -0,0 +1 @@ +uid://bignj6rqf37vv diff --git a/tests/exercises/test_exercise_host_flow.gd b/tests/exercises/test_exercise_host_flow.gd index 13ec3c9..7484f0b 100644 --- a/tests/exercises/test_exercise_host_flow.gd +++ b/tests/exercises/test_exercise_host_flow.gd @@ -86,3 +86,31 @@ func test_wrong_answer_escalates_hint_then_explanation_then_reveal() -> void: var skill_state := ProgressManager.get_skill_state("accord_groupe_nominal") assert_eq(skill_state["recent_results"], [false, false, false]) assert_eq(skill_state["recent_hint_used"], [false, true, true]) + + +func test_a_mission_never_shows_the_same_exercise_twice() -> void: + # Regression : le tirage n'excluait que le dernier exercice, donc le meme + # enonce pouvait revenir dans une meme mission (A, B, A). On s'arrete au + # troisieme exercice charge, sans le terminer (pas de changement de scene). + for _run in range(10): + var host := await _start_host() + host.get_node("%OverlayContainer").get_child(0).continued.emit() + await wait_process_frames(1) + var shown: Array[String] = [] + for step in range(3): + shown.append(host.get_current_exercise()["id"]) + if step == 2: + break + var scene: Control = host.get_node("%ExerciseContainer").get_child(0) + scene.answer_submitted.emit(host.get_current_exercise()["correct_answer"]) + await wait_process_frames(1) + var overlay: Control = host.get_node("%OverlayContainer") + var feedback: Control = overlay.get_child(overlay.get_child_count() - 1) + feedback.get_node("%ContinueButton").pressed.emit() + await wait_process_frames(1) + var unique := {} + for exercise_id in shown: + unique[exercise_id] = true + assert_eq(unique.size(), 3, "exercices vus : %s" % [shown]) + host.queue_free() + await wait_process_frames(1)