mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
Never repeat an exercise within a mission; remove near-duplicate sentences (0.5.5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"]))
|
||||
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bignj6rqf37vv
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user