mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
2fe455f5d1
scripts/correction/: AnswerValidator (multiple choice, sentence correction via tap-the-faulty-word + retype) and DictationAnalyzer build the CorrectionResult structure from cahier des charges §6. Dictation analysis aligns words with WordAligner (Wagner-Fischer edit distance) before any character comparison, so an omitted or added word doesn't cascade into false errors on every following word. TextNormalizer handles non-breaking spaces, typographic apostrophes, and decomposed-vs-composed French accented characters via an explicit table (GDScript has no built-in NFC/NFD normalization). Error classification follows the data annotations only (rule_id present -> grammatical, absent -> unclassified), never a runtime linguistic guess. scripts/pedagogy/: DifficultyManager (per-skill difficulty from recent results), AttemptFlow (3-attempt graduated aid: hint, then explanation, then reveal, matching exercise-lifecycle.md's second gap), and ReviewScheduler (the five mastery states from §9, with session-counted spaced review intervals [1, 3, 6, 12] rather than wall-clock dates, since a family device's clock isn't trustworthy). All of it is pure: no scene tree, no clock or RNG read directly, every effectful input (session number, attempt outcome) is a parameter. 65 headless tests across both layers, including the word-omission-doesn't-cascade regression and the full decouverte -> acquise -> a_reviser -> acquise path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.4 KiB
GDScript
100 lines
3.4 KiB
GDScript
class_name AnswerValidator
|
|
extends RefCounted
|
|
## Valide une reponse de choix multiple ou de correction de phrase et produit
|
|
## la structure de correction du §6 (voir CorrectionResult). Code pur : aucune
|
|
## dependance a l'arbre de scenes, aucune horloge, aucun hasard.
|
|
|
|
|
|
static func validate_multiple_choice(
|
|
exercise: Dictionary, selected_answer: int
|
|
) -> CorrectionResult:
|
|
var correct_answer: int = exercise["correct_answer"]
|
|
if selected_answer == correct_answer:
|
|
return CorrectionResult.correct(exercise["explanation"])
|
|
|
|
var answers: Array = exercise["answers"]
|
|
var found := ""
|
|
if selected_answer >= 0 and selected_answer < answers.size():
|
|
found = answers[selected_answer]
|
|
var expected: String = answers[correct_answer]
|
|
|
|
var error := {
|
|
"position": 0,
|
|
"error_type": "grammatical",
|
|
"rule_id": exercise["rule_id"],
|
|
"found": found,
|
|
"expected": expected,
|
|
}
|
|
return CorrectionResult.incorrect(
|
|
[error], exercise["explanation"], exercise["hint"], [expected]
|
|
)
|
|
|
|
|
|
## selected_indices : index des tokens que l'enfant a touches comme fautifs.
|
|
## corrections : correction saisie pour chaque token touche (cle = index du
|
|
## token, valeur = texte saisi).
|
|
static func validate_sentence_correction(
|
|
exercise: Dictionary, selected_indices: Array[int], corrections: Dictionary
|
|
) -> CorrectionResult:
|
|
var tokens: Array = exercise["tokens"]
|
|
var errors: Array[Dictionary] = []
|
|
|
|
for i in tokens.size():
|
|
var token: Dictionary = tokens[i]
|
|
var is_faulty: bool = token.get("correct", true) == false
|
|
var was_selected: bool = selected_indices.has(i)
|
|
|
|
if is_faulty and not was_selected:
|
|
errors.append(_missed_error(i, token, exercise))
|
|
elif is_faulty and was_selected:
|
|
var submitted: String = corrections.get(i, "")
|
|
if submitted != token["expected"]:
|
|
errors.append(_wrong_correction_error(i, submitted, token, exercise))
|
|
elif not is_faulty and was_selected:
|
|
errors.append(_false_positive_error(i, token, exercise))
|
|
|
|
if errors.is_empty():
|
|
return CorrectionResult.correct(exercise["explanation"])
|
|
|
|
var remediation: Array[String] = []
|
|
for token: Dictionary in tokens:
|
|
if token.get("correct", true) == false:
|
|
remediation.append(token["expected"])
|
|
return CorrectionResult.incorrect(
|
|
errors, exercise["explanation"], exercise["hint"], remediation
|
|
)
|
|
|
|
|
|
static func _missed_error(index: int, token: Dictionary, exercise: Dictionary) -> Dictionary:
|
|
return {
|
|
"position": index,
|
|
"error_type": "grammatical",
|
|
"rule_id": token.get("rule_id", exercise["rule_id"]),
|
|
"found": token["text"],
|
|
"expected": token["expected"],
|
|
}
|
|
|
|
|
|
static func _wrong_correction_error(
|
|
index: int, submitted: String, token: Dictionary, exercise: Dictionary
|
|
) -> Dictionary:
|
|
return {
|
|
"position": index,
|
|
"error_type": "grammatical",
|
|
"rule_id": token.get("rule_id", exercise["rule_id"]),
|
|
"found": submitted,
|
|
"expected": token["expected"],
|
|
}
|
|
|
|
|
|
static func _false_positive_error(
|
|
index: int, token: Dictionary, exercise: Dictionary
|
|
) -> Dictionary:
|
|
return {
|
|
"position": index,
|
|
"error_type": "unclassified",
|
|
"rule_id": exercise["rule_id"],
|
|
"found": token["text"],
|
|
"expected": token["text"],
|
|
}
|