Files
aventuriers-des-mots/scripts/correction/answer_validator.gd
T
2026-10-12 11:41:49 +02:00

130 lines
5.0 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 not _matches_expected(submitted, token["expected"], token["text"]):
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
)
## Compare la correction saisie a celle attendue en ignorant les differences
## incidentes (majuscule/minuscule, apostrophe typographique, espaces,
## accents decomposes) : ce n'est pas la casse qui est evaluee ici, c'est
## l'accord. Quand le mot touche porte une elision (« j'achèterai »), l'enfant
## peut taper le mot entier (« j'achèterais ») ou seulement le verbe
## (« achèterais ») : une reponse ambigue n'est pas declaree fausse
## automatiquement (CLAUDE.md, invariants pedagogiques).
static func _matches_expected(submitted: String, expected: String, token_text: String = "") -> bool:
var typed := TextNormalizer.normalize(submitted).to_lower()
for form in _accepted_forms(expected, token_text):
if typed == TextNormalizer.normalize(form).to_lower():
return true
return false
## Formes acceptees : l'attendu, sans son prefixe d'elision (« j' », « l' »...),
## ou avec le prefixe du mot touche quand l'attendu n'en porte pas.
static func _accepted_forms(expected: String, token_text: String) -> Array[String]:
var forms: Array[String] = [expected]
var normalized_expected := TextNormalizer.normalize(expected)
var normalized_token := TextNormalizer.normalize(token_text)
var expected_cut := normalized_expected.find("'")
if expected_cut != -1:
forms.append(normalized_expected.substr(expected_cut + 1))
var token_cut := normalized_token.find("'")
if token_cut != -1 and expected_cut == -1:
forms.append(normalized_token.substr(0, token_cut + 1) + normalized_expected)
return forms
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"],
}