mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
8584249d78
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
130 lines
5.0 KiB
GDScript
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"],
|
|
}
|