From 9978a294dbef2c5149d05f1d5454ecb3dccfbb75 Mon Sep 17 00:00:00 2001 From: Denis Defolie Date: Mon, 12 Oct 2026 10:18:20 +0200 Subject: [PATCH] Fix export on fresh clone (make import), add elision and duplicate-sentence guards, replace 14 duplicate sentences (0.5.3) Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 1 + Makefile | 23 ++-- .../ce1_accord_determinant_correction_04.json | 27 ++++- .../ce1/ce1_accord_determinant_qcm_01.json | 16 ++- .../ce1/ce1_accord_determinant_qcm_03.json | 16 ++- .../exercises/ce1/ce1_pluriel_nom_qcm_01.json | 16 ++- .../exercises/ce1/ce1_pluriel_nom_qcm_05.json | 15 ++- .../exercises/ce1/ce1_pluriel_nom_qcm_08.json | 14 ++- data/exercises/ce2/ce2_a_a_correction_02.json | 10 +- data/exercises/ce2/ce2_a_a_qcm_01.json | 2 +- .../ce2/ce2_et_est_correction_02.json | 6 +- data/exercises/ce2/ce2_et_est_qcm_01.json | 2 +- data/exercises/ce2/ce2_on_ont_qcm_01.json | 2 +- data/exercises/ce2/ce2_on_ont_qcm_02.json | 2 +- data/exercises/ce2/ce2_son_sont_qcm_01.json | 2 +- data/exercises/ce2/ce2_son_sont_qcm_04.json | 2 +- docs/review/content-review.md | 60 +++++----- docs/specs/decisions.md | 8 ++ project.godot | 2 +- tests/data/test_elision.gd | 110 ++++++++++++++++++ tests/data/test_elision.gd.uid | 1 + tests/data/test_no_duplicate_sentences.gd | 77 ++++++++++++ tests/data/test_no_duplicate_sentences.gd.uid | 1 + 23 files changed, 331 insertions(+), 84 deletions(-) create mode 100644 tests/data/test_elision.gd create mode 100644 tests/data/test_elision.gd.uid create mode 100644 tests/data/test_no_duplicate_sentences.gd create mode 100644 tests/data/test_no_duplicate_sentences.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index 88af383..ff9c7e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ Jeu éducatif de français CE1–CE2 (7–9 ans), aventure 2D, hors ligne, Godot Le `Makefile` est le seul point d'entrée pour les opérations système. Tenir cette liste à jour. - `make setup` : installe l'outillage (lint, tests, modèles d'export) +- `make import` : importe les ressources dans `.godot/` (deux passes ; appelé automatiquement par `run`, `test`, `validate-data`, `export-*`, `screenshot` : indispensable sur un dépôt fraîchement cloné) - `make run` : lance le jeu - `make lint` / `make format` : analyse statique et formatage - `make validate-data` : valide tous les JSON de `data/` contre leurs schémas diff --git a/Makefile b/Makefile index a29a65c..de243a2 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,21 @@ PYRIGHT := $(VENV)/bin/pyright PYTHON := $(VENV)/bin/python3 GD_SOURCES := autoload scripts tests -.PHONY: setup run lint format validate-data test check content-review generate-dictation-audio generate-sounds export-linux export-windows package screenshot +.PHONY: setup import run lint format validate-data test check content-review generate-dictation-audio generate-sounds export-linux export-windows package screenshot setup: tools/setup.sh -run: +# Importe les ressources (polices, SVG, audio...) dans .godot/. Indispensable +# sur un depot fraichement clone : sans cela, l'export plante ou produit un +# jeu sans theme. Deux passes : le theme n'est valide qu'une fois les polices +# importees. La premiere passe peut echouer (theme charge avant les polices) : +# son code de retour est ignore, la seconde fait foi. +import: + -@$(GODOT_BIN) --headless --path . --import >/dev/null 2>&1 + $(GODOT_BIN) --headless --path . --import >/dev/null + +run: import $(GODOT_BIN) --path . lint: @@ -21,10 +30,10 @@ lint: format: $(GDFORMAT) $(GD_SOURCES) -validate-data: +validate-data: import $(GODOT_BIN) --headless --path . -s res://scripts/data/validate_data_cli.gd -test: +test: import $(GODOT_BIN) --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit check: lint validate-data test @@ -38,11 +47,11 @@ generate-dictation-audio: generate-sounds: $(PYTHON) tools/generate_sounds.py -export-linux: +export-linux: import mkdir -p build/linux $(GODOT_BIN) --headless --path . --export-release "Linux" build/linux/aventuriers_des_mots.x86_64 -export-windows: +export-windows: import mkdir -p build/windows $(GODOT_BIN) --headless --path . --export-release "Windows" build/windows/aventuriers_des_mots.exe @@ -51,5 +60,5 @@ package: # Capture d'une scene dans une vraie fenetre (voir tools/screenshot.gd) : # make screenshot SCENE=res://scenes/world/world_map.tscn OUT=/tmp/map.png ARGS="CE2" -screenshot: +screenshot: import $(GODOT_BIN) --path . -s res://tools/screenshot.gd -- $(SCENE) $(OUT) $(ARGS) diff --git a/data/exercises/ce1/ce1_accord_determinant_correction_04.json b/data/exercises/ce1/ce1_accord_determinant_correction_04.json index 45ef0c8..55d434a 100644 --- a/data/exercises/ce1/ce1_accord_determinant_correction_04.json +++ b/data/exercises/ce1/ce1_accord_determinant_correction_04.json @@ -14,13 +14,28 @@ "expected": "Les", "rule_id": "regle_accord_determinant" }, - { "text": "petites", "correct": true }, - { "text": "fleurs", "correct": true }, - { "text": "sont", "correct": true }, - { "text": "jolies.", "correct": true } + { + "text": "grandes", + "correct": true + }, + { + "text": "maisons", + "correct": true + }, + { + "text": "sont", + "correct": true + }, + { + "text": "anciennes.", + "correct": true + } ], "hint": "Cherche le nom auquel le déterminant se rapporte, même si un autre mot s'intercale.", - "explanation": "Le déterminant se rapporte à « fleurs », qui est féminin pluriel, même si l'adjectif « petites » s'intercale : il faut écrire « Les ».", - "review_tags": ["pluriel", "accord_determinant"], + "explanation": "Le déterminant se rapporte à « maisons », qui est féminin pluriel, même si l'adjectif « grandes » s'intercale : il faut écrire « Les ».", + "review_tags": [ + "pluriel", + "accord_determinant" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_determinant_qcm_01.json b/data/exercises/ce1/ce1_accord_determinant_qcm_01.json index aedd78d..5a9c94e 100644 --- a/data/exercises/ce1/ce1_accord_determinant_qcm_01.json +++ b/data/exercises/ce1/ce1_accord_determinant_qcm_01.json @@ -7,11 +7,17 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon déterminant.", - "question": "... chat dort.", - "answers": ["Le", "La"], + "question": "... lapin dort.", + "answers": [ + "Le", + "La" + ], "correct_answer": 0, - "hint": "Le nom « chat » est masculin.", - "explanation": "« Chat » est un nom masculin : il faut le déterminant masculin « Le ».", - "review_tags": ["genre", "accord_determinant"], + "hint": "Le nom « lapin » est masculin.", + "explanation": "« Lapin » est un nom masculin : il faut le déterminant masculin « Le ».", + "review_tags": [ + "genre", + "accord_determinant" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_accord_determinant_qcm_03.json b/data/exercises/ce1/ce1_accord_determinant_qcm_03.json index edf557f..2438f69 100644 --- a/data/exercises/ce1/ce1_accord_determinant_qcm_03.json +++ b/data/exercises/ce1/ce1_accord_determinant_qcm_03.json @@ -7,11 +7,17 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon déterminant.", - "question": "... chats dorment.", - "answers": ["Le", "Les"], + "question": "... oiseaux chantent.", + "answers": [ + "Le", + "Les" + ], "correct_answer": 1, - "hint": "Le nom « chats » est au pluriel.", - "explanation": "« Chats » est au pluriel : il faut le déterminant pluriel « Les ».", - "review_tags": ["pluriel", "accord_determinant"], + "hint": "Le nom « oiseaux » est au pluriel.", + "explanation": "« Oiseaux » 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_pluriel_nom_qcm_01.json b/data/exercises/ce1/ce1_pluriel_nom_qcm_01.json index 970bac0..b7333ba 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_qcm_01.json +++ b/data/exercises/ce1/ce1_pluriel_nom_qcm_01.json @@ -7,11 +7,17 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Les ... jouent dans le jardin.", - "answers": ["chat", "chats"], + "question": "Les ... courent dans la cour.", + "answers": [ + "enfant", + "enfants" + ], "correct_answer": 1, - "hint": "Le déterminant « les » indique plusieurs chats.", - "explanation": "Pour former le pluriel d'un nom, on ajoute un -s à la fin : « chats ».", - "review_tags": ["pluriel", "pluriel_nom"], + "hint": "Le déterminant « les » indique plusieurs enfants.", + "explanation": "Pour former le pluriel d'un nom, on ajoute un -s à la fin : « enfants ».", + "review_tags": [ + "pluriel", + "pluriel_nom" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_pluriel_nom_qcm_05.json b/data/exercises/ce1/ce1_pluriel_nom_qcm_05.json index 383161c..4c34b09 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_qcm_05.json +++ b/data/exercises/ce1/ce1_pluriel_nom_qcm_05.json @@ -7,11 +7,16 @@ "difficulty": 2, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Le ... est sur la table.", - "answers": ["cahier", "cahiers"], + "question": "Le ... est dans le sac.", + "answers": [ + "livre", + "livres" + ], "correct_answer": 0, - "hint": "Le déterminant « le » indique un seul cahier.", - "explanation": "« Le » indique un seul objet : le nom reste au singulier, sans -s : « cahier ».", - "review_tags": ["pluriel_nom"], + "hint": "Le déterminant « le » indique un seul livre.", + "explanation": "« Le » indique un seul objet : le nom reste au singulier, sans -s : « livre ».", + "review_tags": [ + "pluriel_nom" + ], "review_status": "draft" } diff --git a/data/exercises/ce1/ce1_pluriel_nom_qcm_08.json b/data/exercises/ce1/ce1_pluriel_nom_qcm_08.json index b3e4459..f2bb129 100644 --- a/data/exercises/ce1/ce1_pluriel_nom_qcm_08.json +++ b/data/exercises/ce1/ce1_pluriel_nom_qcm_08.json @@ -7,11 +7,17 @@ "difficulty": 3, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Les jolies ... sentent bon.", - "answers": ["fleur", "fleurs"], + "question": "Les belles ... brillent dans le ciel.", + "answers": [ + "étoile", + "étoiles" + ], "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 « jolies » s'intercale avant le nom : il faut écrire « fleurs ».", - "review_tags": ["pluriel", "pluriel_nom"], + "explanation": "« Les » indique le pluriel, même si l'adjectif « belles » s'intercale avant le nom : il faut écrire « étoiles ».", + "review_tags": [ + "pluriel", + "pluriel_nom" + ], "review_status": "draft" } diff --git a/data/exercises/ce2/ce2_a_a_correction_02.json b/data/exercises/ce2/ce2_a_a_correction_02.json index b37241a..707b069 100644 --- a/data/exercises/ce2/ce2_a_a_correction_02.json +++ b/data/exercises/ce2/ce2_a_a_correction_02.json @@ -9,11 +9,11 @@ "instruction": "Touche le mot qui contient une erreur, puis corrige-le.", "tokens": [ { - "text": "Nous", + "text": "Ils", "correct": true }, { - "text": "allons", + "text": "vont", "correct": true }, { @@ -23,11 +23,7 @@ "rule_id": "regle_homophone_a_a" }, { - "text": "la", - "correct": true - }, - { - "text": "piscine.", + "text": "l'école.", "correct": true } ], diff --git a/data/exercises/ce2/ce2_a_a_qcm_01.json b/data/exercises/ce2/ce2_a_a_qcm_01.json index 72c5c5b..abe3d73 100644 --- a/data/exercises/ce2/ce2_a_a_qcm_01.json +++ b/data/exercises/ce2/ce2_a_a_qcm_01.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Léa ... un chat.", + "question": "Max ... un vélo.", "answers": [ "a", "à" diff --git a/data/exercises/ce2/ce2_et_est_correction_02.json b/data/exercises/ce2/ce2_et_est_correction_02.json index 9dd8b9f..3d9ec55 100644 --- a/data/exercises/ce2/ce2_et_est_correction_02.json +++ b/data/exercises/ce2/ce2_et_est_correction_02.json @@ -9,7 +9,7 @@ "instruction": "Touche le mot qui contient une erreur, puis corrige-le.", "tokens": [ { - "text": "Paul", + "text": "Sarah", "correct": true }, { @@ -19,11 +19,11 @@ "rule_id": "regle_homophone_et_est" }, { - "text": "Marie", + "text": "Tom", "correct": true }, { - "text": "jouent", + "text": "dessinent", "correct": true }, { diff --git a/data/exercises/ce2/ce2_et_est_qcm_01.json b/data/exercises/ce2/ce2_et_est_qcm_01.json index a84309f..77fd0f9 100644 --- a/data/exercises/ce2/ce2_et_est_qcm_01.json +++ b/data/exercises/ce2/ce2_et_est_qcm_01.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Le chat ... noir.", + "question": "Le ciel ... bleu.", "answers": [ "et", "est" diff --git a/data/exercises/ce2/ce2_on_ont_qcm_01.json b/data/exercises/ce2/ce2_on_ont_qcm_01.json index 1c4825a..fd66c8a 100644 --- a/data/exercises/ce2/ce2_on_ont_qcm_01.json +++ b/data/exercises/ce2/ce2_on_ont_qcm_01.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "... joue dehors.", + "question": "... chante une chanson.", "answers": [ "On", "Ont" diff --git a/data/exercises/ce2/ce2_on_ont_qcm_02.json b/data/exercises/ce2/ce2_on_ont_qcm_02.json index e493fe5..9901295 100644 --- a/data/exercises/ce2/ce2_on_ont_qcm_02.json +++ b/data/exercises/ce2/ce2_on_ont_qcm_02.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Les enfants ... faim.", + "question": "Les oiseaux ... froid.", "answers": [ "on", "ont" diff --git a/data/exercises/ce2/ce2_son_sont_qcm_01.json b/data/exercises/ce2/ce2_son_sont_qcm_01.json index f858bfb..ce08c2f 100644 --- a/data/exercises/ce2/ce2_son_sont_qcm_01.json +++ b/data/exercises/ce2/ce2_son_sont_qcm_01.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Les chats ... gentils.", + "question": "Les élèves ... contents.", "answers": [ "son", "sont" diff --git a/data/exercises/ce2/ce2_son_sont_qcm_04.json b/data/exercises/ce2/ce2_son_sont_qcm_04.json index 14601e9..751ab81 100644 --- a/data/exercises/ce2/ce2_son_sont_qcm_04.json +++ b/data/exercises/ce2/ce2_son_sont_qcm_04.json @@ -7,7 +7,7 @@ "difficulty": 1, "type": "multiple_choice", "instruction": "Choisis le bon mot.", - "question": "Paul range ... cartable.", + "question": "Julie ouvre ... cahier.", "answers": [ "son", "sont" diff --git a/docs/review/content-review.md b/docs/review/content-review.md index e8ace7b..77d9d5b 100644 --- a/docs/review/content-review.md +++ b/docs/review/content-review.md @@ -105,11 +105,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce1_accord_determinant_qcm_01** (difficulte 1) : ... chat dort. +- **ce1_accord_determinant_qcm_01** (difficulte 1) : ... lapin dort. - Reponses : Le, La - Reponse attendue : Le - - Indice : Le nom « chat » est masculin. - - Explication : « Chat » est un nom masculin : il faut le déterminant masculin « Le ». + - Indice : Le nom « lapin » est masculin. + - Explication : « Lapin » est un nom masculin : il faut le déterminant masculin « Le ». - Statut : draft - **ce1_accord_determinant_qcm_02** (difficulte 1) : ... fleur sent bon. - Reponses : Le, La @@ -117,11 +117,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le nom « fleur » est féminin. - Explication : « Fleur » est un nom féminin : il faut le déterminant féminin « La ». - Statut : draft -- **ce1_accord_determinant_qcm_03** (difficulte 1) : ... chats dorment. +- **ce1_accord_determinant_qcm_03** (difficulte 1) : ... oiseaux chantent. - Reponses : Le, Les - Reponse attendue : Les - - Indice : Le nom « chats » est au pluriel. - - Explication : « Chats » est au pluriel : il faut le déterminant pluriel « Les ». + - 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. - Reponses : La, Les @@ -168,9 +168,9 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le nom « chats » est au pluriel. - Explication : « Chats » est au pluriel : il faut le déterminant pluriel « Les ». - Statut : draft -- **ce1_accord_determinant_correction_04** (difficulte 3) : [Le -> Les] petites fleurs sont jolies. +- **ce1_accord_determinant_correction_04** (difficulte 3) : [Le -> Les] grandes maisons sont anciennes. - Indice : Cherche le nom auquel le déterminant se rapporte, même si un autre mot s'intercale. - - Explication : Le déterminant se rapporte à « fleurs », qui est féminin pluriel, même si l'adjectif « petites » s'intercale : il faut écrire « Les ». + - Explication : Le déterminant se rapporte à « maisons », qui est féminin pluriel, même si l'adjectif « grandes » s'intercale : il faut écrire « Les ». - Statut : draft ### Dictees @@ -198,7 +198,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce2_a_a_qcm_01** (difficulte 1) : Léa ... un chat. +- **ce2_a_a_qcm_01** (difficulte 1) : Max ... un vélo. - Reponses : a, à - Reponse attendue : a - Indice : Essaie de remplacer par « avait ». @@ -253,7 +253,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_correction_02** (difficulte 1) : Nous allons [a -> à] la piscine. +- **ce2_a_a_correction_02** (difficulte 1) : Ils vont [a -> à] l'école. - Indice : Essaie de remplacer par « avait ». - Explication : « avait » ne convient pas ici : c'est le petit mot « à », avec un accent. - Statut : draft @@ -291,7 +291,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce2_et_est_qcm_01** (difficulte 1) : Le chat ... noir. +- **ce2_et_est_qcm_01** (difficulte 1) : Le ciel ... bleu. - Reponses : et, est - Reponse attendue : est - Indice : Essaie de remplacer par « était ». @@ -346,7 +346,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Essaie de remplacer par « était ». - Explication : « était » convient ici : c'est le verbe être, on écrit « est ». - Statut : draft -- **ce2_et_est_correction_02** (difficulte 1) : Paul [est -> et] Marie jouent ensemble. +- **ce2_et_est_correction_02** (difficulte 1) : Sarah [est -> et] Tom dessinent ensemble. - 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 @@ -384,13 +384,13 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce2_on_ont_qcm_01** (difficulte 1) : ... joue dehors. +- **ce2_on_ont_qcm_01** (difficulte 1) : ... chante une chanson. - Reponses : On, Ont - Reponse attendue : On - Indice : Essaie de remplacer par « avaient », ou par « il ». - Explication : « avaient » ne convient pas ici, mais « il » convient : c'est le pronom « on ». - Statut : draft -- **ce2_on_ont_qcm_02** (difficulte 1) : Les enfants ... faim. +- **ce2_on_ont_qcm_02** (difficulte 1) : Les oiseaux ... froid. - Reponses : on, ont - Reponse attendue : ont - Indice : Essaie de remplacer par « avaient », ou par « il ». @@ -477,7 +477,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce2_son_sont_qcm_01** (difficulte 1) : Les chats ... gentils. +- **ce2_son_sont_qcm_01** (difficulte 1) : Les élèves ... contents. - Reponses : son, sont - Reponse attendue : sont - Indice : Essaie de remplacer par « étaient », ou par « mon ». @@ -495,7 +495,7 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Essaie de remplacer par « étaient », ou par « mon ». - Explication : « étaient » convient ici : c'est le verbe être, on écrit « sont ». - Statut : draft -- **ce2_son_sont_qcm_04** (difficulte 1) : Paul range ... cartable. +- **ce2_son_sont_qcm_04** (difficulte 1) : Julie ouvre ... cahier. - Reponses : son, sont - Reponse attendue : son - Indice : Essaie de remplacer par « étaient », ou par « mon ». @@ -570,11 +570,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne ### Choix multiples -- **ce1_pluriel_nom_qcm_01** (difficulte 1) : Les ... jouent dans le jardin. - - Reponses : chat, chats - - Reponse attendue : chats - - Indice : Le déterminant « les » indique plusieurs chats. - - Explication : Pour former le pluriel d'un nom, on ajoute un -s à la fin : « chats ». +- **ce1_pluriel_nom_qcm_01** (difficulte 1) : Les ... courent dans la cour. + - Reponses : enfant, enfants + - Reponse attendue : enfants + - Indice : Le déterminant « les » indique plusieurs enfants. + - Explication : Pour former le pluriel d'un nom, on ajoute un -s à la fin : « enfants ». - Statut : draft - **ce1_pluriel_nom_qcm_02** (difficulte 1) : Des ... poussent dans le jardin. - Reponses : fleur, fleurs @@ -594,11 +594,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - Indice : Le déterminant « des » indique plusieurs voitures. - Explication : Pour former le pluriel d'un nom, on ajoute un -s à la fin : « voitures ». - Statut : draft -- **ce1_pluriel_nom_qcm_05** (difficulte 2) : Le ... est sur la table. - - Reponses : cahier, cahiers - - Reponse attendue : cahier - - Indice : Le déterminant « le » indique un seul cahier. - - Explication : « Le » indique un seul objet : le nom reste au singulier, sans -s : « cahier ». +- **ce1_pluriel_nom_qcm_05** (difficulte 2) : Le ... est dans le sac. + - Reponses : livre, livres + - Reponse attendue : livre + - Indice : Le déterminant « le » indique un seul livre. + - Explication : « Le » indique un seul objet : le nom reste au singulier, sans -s : « livre ». - Statut : draft - **ce1_pluriel_nom_qcm_06** (difficulte 2) : La ... est ouverte. - Reponses : porte, portes @@ -612,11 +612,11 @@ Genere automatiquement depuis `data/` par `tools/generate_content_review.py`. Ne - 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 ». - Statut : draft -- **ce1_pluriel_nom_qcm_08** (difficulte 3) : Les jolies ... sentent bon. - - Reponses : fleur, fleurs - - Reponse attendue : fleurs +- **ce1_pluriel_nom_qcm_08** (difficulte 3) : Les belles ... brillent dans le ciel. + - Reponses : étoile, étoiles + - Reponse attendue : étoiles - 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 « jolies » s'intercale avant le nom : il faut écrire « fleurs ». + - Explication : « Les » indique le pluriel, même si l'adjectif « belles » s'intercale avant le nom : il faut écrire « étoiles ». - Statut : draft ### Corrections de phrase diff --git a/docs/specs/decisions.md b/docs/specs/decisions.md index 07f6c60..fc16de8 100644 --- a/docs/specs/decisions.md +++ b/docs/specs/decisions.md @@ -330,3 +330,11 @@ Ajouts à l'arborescence proposée : **Addendum (0.5.1) — choix de la musique de fond** (demande utilisateur) : 5 morceaux au choix, `forest`, `marsh`, `meadow` (prairie), `river` (rivière), `night` (nuit), plus un choix « Automatique » (par défaut : forêt en CE1, marais en CE2, comme avant). Le choix est un réglage de l'appareil (`music_track` dans `user://settings.json`, valeur inconnue ramenée à « auto ») : le format des sauvegardes de profil est inchangé. Sélectionner un morceau dans les réglages le fait entendre aussitôt. Sons toujours provisoires et jamais écoutés ici. **Correctif (0.5.2)** : les cinq morceaux de 0.5.1 étaient trop proches (même timbre de nappe + cloches, même niveau) : l'utilisateur n'a pas perçu de différence. Le changement de morceau fonctionnait (vérifié : identifiant, flux et bus corrects à chaque choix) ; seul le matériau sonore était indiscernable. Les morceaux sont désormais construits sur des styles distincts : forêt (nappe + cloches), marais (bourdon grave qui respire, rares cloches creuses), prairie (pizzicati allègres, basse sur le temps), rivière (arpège continu et lié), nuit (boîte à musique aiguë en 3 temps). Mesure objective : centroïde spectral de 220 à 1277 Hz, durée de boucle de 12 à 46 s, densité d'attaques de 3 à 36/s. Toujours jamais écouté ici. + +## 34. Corrections du 12 octobre (version 0.5.3) + +**Export cassé sur dépôt frais** : sur un clone sans `.godot/`, `make export-*` échouait (erreurs de chargement des polices et du thème, segfault de l'export, aucun binaire). Cause : le thème global est chargé au démarrage avant que les polices soient importées, donc une première passe d'import échoue ; le dépôt ne versionne pas `.godot/`. Correctif : cible `make import` (deux passes, la première tolérée en échec), prérequis de `run`, `test`, `validate-data`, `export-linux`, `export-windows`, `screenshot`. Vérifié sur un clone neuf : `make export-linux` produit un binaire qui démarre sans erreur. L'export avec Godot 4.7.2 (celui de l'utilisateur dans `~/bin`) fonctionne aussi ; le projet reste épinglé en 4.6.3 (`.godot-version`). + +**Élisions** : aucune élision manquante (« le arbre ») dans les données, vérifié sur tous les JSON y compris les listes de jetons ; garde-fou permanent `tests/data/test_elision.gd` (mots à « h » aspiré et exceptions listés ; l'assertion arrière écarte « écris-la en »). Si l'élision manquante vue en jeu vient d'ailleurs que des données, il faudra l'indication de l'écran concerné. + +**Phrases en double** : 14 phrases apparaissaient dans deux exercices ou dans un exercice et une dictée (ex. « Le chat dort. »). Les énoncés des QCM et corrections concernés ont été remplacés par de nouvelles phrases originales (indice et explication ajustés) ; les dictées, dont l'audio est généré, n'ont pas été touchées. Garde-fou permanent `tests/data/test_no_duplicate_sentences.gd` (phrase reconstituée en version correcte, comparée sans casse ni ponctuation). diff --git a/project.godot b/project.godot index 465685a..cb5a054 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.2" +config/version="0.5.3" run/main_scene="res://scenes/boot/boot.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" diff --git a/tests/data/test_elision.gd b/tests/data/test_elision.gd new file mode 100644 index 0000000..b2f7924 --- /dev/null +++ b/tests/data/test_elision.gd @@ -0,0 +1,110 @@ +extends GutTest +## Garde-fou orthographique sur tous les textes du jeu : « le arbre » doit +## s'ecrire « l'arbre ». Parcourt tous les JSON de data/ (chaines et listes de +## mots consecutifs) a la recherche d'une elision manquante. Les mots a « h » +## aspire et quelques exceptions legitimes sont listes ci-dessous. + +const DATA_DIR := "res://data" +## Mots a « h » aspire ou vocalique sans elision : « le hibou », « le onze ». +const NO_ELISION_WORDS: Array[String] = [ + "hibou", + "haie", + "haies", + "héros", + "hérisson", + "hamac", + "hanche", + "hangar", + "haricot", + "haricots", + "hasard", + "hache", + "hauteur", + "haut", + "haute", + "hautes", + "hauts", + "hall", + "hameau", + "harpe", + "hâte", + "hérissons", + "hurlement", + "huit", + "onze", + "oui", + "ouistiti", + "yaourt", + "yeux", + "ukulélé", +] + +## (*UCP) : \\w couvre les lettres accentuees. Motif : mot d'elision, espaces, +## puis mot a voyelle ou « h » initial ; l'assertion arriere ecarte « écris-la en ». +const PATTERN := ( + "(*UCP)(? void: + _regex.compile(PATTERN) + + +func test_no_missing_elision_in_game_texts() -> void: + _scan_dir(DATA_DIR) + assert_eq(_problems.size(), 0, "Elisions manquantes : %s" % "\n".join(_problems)) + + +func test_detector_catches_a_missing_elision() -> void: + _problems.clear() + _check_text("le arbre est grand", "exemple") + assert_eq(_problems.size(), 1) + _problems.clear() + _check_text("Le hibou dort. Écris-la en entier. Le onze.", "exemple") + assert_eq(_problems.size(), 0) + + +func _scan_dir(path: String) -> void: + var dir := DirAccess.open(path) + if dir == null: + return + for sub in dir.get_directories(): + if sub != "schemas": + _scan_dir(path.path_join(sub)) + for file_name in dir.get_files(): + if file_name.ends_with(".json"): + _scan_file(path.path_join(file_name)) + + +func _scan_file(path: String) -> void: + var file := FileAccess.open(path, FileAccess.READ) + var parsed: Variant = JSON.parse_string(file.get_as_text()) + _scan_value(parsed, path) + + +func _scan_value(value: Variant, where: String) -> void: + if typeof(value) == TYPE_STRING: + _check_text(value, where) + elif typeof(value) == TYPE_DICTIONARY: + for key: Variant in value: + _scan_value(value[key], where) + elif typeof(value) == TYPE_ARRAY: + var items: Array = value + # Mots consecutifs d'une phrase decoupee en jetons. + for i in range(items.size() - 1): + if typeof(items[i]) == TYPE_STRING and typeof(items[i + 1]) == TYPE_STRING: + _check_text("%s %s" % [items[i], items[i + 1]], where) + for item: Variant in items: + _scan_value(item, where) + + +func _check_text(text: String, where: String) -> void: + for match in _regex.search_all(text): + var next_word := match.get_string(2).to_lower() + if NO_ELISION_WORDS.has(next_word): + continue + _problems.append("%s : « %s »" % [where, match.get_string(0)]) diff --git a/tests/data/test_elision.gd.uid b/tests/data/test_elision.gd.uid new file mode 100644 index 0000000..faad0ef --- /dev/null +++ b/tests/data/test_elision.gd.uid @@ -0,0 +1 @@ +uid://beh3tst4how7j diff --git a/tests/data/test_no_duplicate_sentences.gd b/tests/data/test_no_duplicate_sentences.gd new file mode 100644 index 0000000..38414fc --- /dev/null +++ b/tests/data/test_no_duplicate_sentences.gd @@ -0,0 +1,77 @@ +extends GutTest +## Aucune phrase ne doit apparaitre deux fois dans le jeu (exercices et +## dictees confondus) : un meme enonce revu dans deux missions est une +## redite, pas un nouvel exercice. La phrase est reconstituee en version +## correcte (blanc rempli, mot fautif corrige) puis comparee sans casse ni +## ponctuation. + +const EXERCISES_DIR := "res://data/exercises" +const DICTATIONS_DIR := "res://data/dictations" + + +func test_no_sentence_is_used_twice() -> void: + var seen: Dictionary = {} + var duplicates: Array[String] = [] + for entry in _collect_sentences(): + var key := _normalize(entry["sentence"]) + if seen.has(key): + duplicates.append("« %s » : %s et %s" % [entry["sentence"], seen[key], entry["id"]]) + else: + seen[key] = entry["id"] + assert_eq(duplicates.size(), 0, "Phrases en double :\n%s" % "\n".join(duplicates)) + assert_gt(seen.size(), 100, "le test doit avoir vu les phrases du jeu") + + +func _collect_sentences() -> Array[Dictionary]: + var sentences: 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(): + sentences.append({"id": data["id"], "sentence": sentence}) + for path in _json_files(DICTATIONS_DIR): + var data := _read(path) + sentences.append({"id": data["id"], "sentence": data["target_text"]}) + return sentences + + +func _normalize(sentence: String) -> String: + var cleaned := "" + for character in sentence.to_lower(): + if character.is_valid_int() or character == "'" or character == " ": + cleaned += character + elif character.to_upper() != character or "éèêàâîïôûùçœ".contains(character): + cleaned += character + else: + cleaned += " " + var parts := cleaned.split(" ", false) + return " ".join(parts) + + +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_duplicate_sentences.gd.uid b/tests/data/test_no_duplicate_sentences.gd.uid new file mode 100644 index 0000000..7fb9dc4 --- /dev/null +++ b/tests/data/test_no_duplicate_sentences.gd.uid @@ -0,0 +1 @@ +uid://cekmrk80y81fk