Implemente le mini-jeu Scenario (situation, choix, consequences)
Build and deploy / test-python (push) Failing after 59s
Build and deploy / test-js (push) Successful in 50s
Build and deploy / lint-python (push) Failing after 59s
Build and deploy / lint-js (push) Failing after 1m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 59s

Cinquieme mini-jeu du support de formation : le createur ecrit une
situation initiale, definit 2 a 4 choix, indique lequel est le bon, et
redige la consequence de chaque choix. Plusieurs scenarios peuvent etre
crees, joues dans l'ordre d'ecriture (jamais melanges, contrairement a
Association/Memory/Mots meles : ce sont des mises en situation
sequentielles). En Apercu, l'apprenant lit la situation, choisit une
option, decouvre la consequence de SON choix et si c'etait le bon, puis
passe au scenario suivant. Comme Association/Memory/Mots meles, le
dernier scenario reste affiche une fois repondu : seul le bouton
Recommencer apparait, jamais un ecran de resultat separe (reserve au
Quiz). Reutilise les classes visuelles du Quiz (docQuizOptions/
docQuizFeedback/docQuizNextBar) plutot que de dupliquer ces regles.

Verifie via simulation DOM reelle (jsdom) : progression entre plusieurs
scenarios, choix correct/incorrect avec revelation de la bonne reponse,
comportement de fin de partie, panneau Proprietes (ajout/suppression de
scenario, changement du nombre de choix, selection du bon choix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-20 16:50:10 +02:00
co-authored by Claude Sonnet 5
parent d8be80ebd1
commit b2292de122
10 changed files with 669 additions and 23 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Modèle de données du mini-jeu Scénario (voir docs/plan/PLAN.md §3.2) —
l'apprenant lit une situation initiale, choisit une option parmi 2 à 4,
puis découvre la conséquence de SON choix ainsi que s'il s'agissait du
bon choix (voir document_engine/rendering/render_document_element.py::
_render_scenario_player). Le créateur peut définir plusieurs scénarios,
joués les uns après les autres dans l'ordre d'écriture. Même convention
resolve_X/sanitize_X que quiz_config.py/association_config.py/
memory_config.py/mots_config.py (aucun import croisé) : sanitize_scenario_
config est pure, ne lève jamais, et renvoie toujours un dict complet."""
from typing import Any
# Nommés explicitement "SCENARIO" (pas juste MIN/MAX_CHOICES) : document_
# engine/__init__.py ré-exporte l'intégralité de l'API publique du paquet
# à plat (voir CLAUDE.md, "1 fichier = 1 fonction publique") — un nom
# générique entrerait en collision avec MIN_CHOICES/MAX_CHOICES de
# quiz_config.py, qui borne un concept différent (le nombre de réponses
# d'une question de quiz, pas le nombre d'options d'une décision).
MIN_SCENARIO_CHOICES = 2
MAX_SCENARIO_CHOICES = 4
DEFAULT_SCENARIO_CONFIG: dict[str, Any] = {
"theme_color": "#ff5f2e",
"scenarios": [],
}
def _sanitize_scenario_choice(raw: Any) -> dict[str, str] | None:
"""None si le choix est invalide (texte vide) — filtrée par
_sanitize_scenario plutôt que de faire échouer tout le scénario,
même convention que quiz_config.py::_sanitize_question. La
conséquence, elle, peut rester vide (un créateur peut vouloir la
renseigner plus tard sans que ça invalide le choix)."""
if not isinstance(raw, dict):
return None
text = str(raw.get("text", "")).strip()
if not text:
return None
consequence = str(raw.get("consequence", "")).strip()
return {"text": text, "consequence": consequence}
def _sanitize_scenario(raw: Any) -> dict[str, Any] | None:
"""None si le scénario est invalide (situation vide, ou moins de
MIN_SCENARIO_CHOICES choix non vides) — même convention que
quiz_config.py::_sanitize_question."""
if not isinstance(raw, dict):
return None
situation = str(raw.get("situation", "")).strip()
if not situation:
return None
raw_choices = raw.get("choices")
if not isinstance(raw_choices, list):
return None
choices = [c for c in (_sanitize_scenario_choice(item) for item in raw_choices) if c is not None]
choices = choices[:MAX_SCENARIO_CHOICES]
if len(choices) < MIN_SCENARIO_CHOICES:
return None
correct_index = raw.get("correct_index")
if not isinstance(correct_index, int) or isinstance(correct_index, bool) or not (0 <= correct_index < len(choices)):
correct_index = 0
return {"situation": situation, "choices": choices, "correct_index": correct_index}
def sanitize_scenario_config(raw_config: Any) -> dict[str, Any]:
config = dict(DEFAULT_SCENARIO_CONFIG)
if not isinstance(raw_config, dict):
return config
theme_color = raw_config.get("theme_color")
if isinstance(theme_color, str) and theme_color:
config["theme_color"] = theme_color
raw_scenarios = raw_config.get("scenarios")
if isinstance(raw_scenarios, list):
config["scenarios"] = [s for s in (_sanitize_scenario(item) for item in raw_scenarios) if s is not None]
return config