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
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:
co-authored by
Claude Sonnet 5
parent
d8be80ebd1
commit
b2292de122
@@ -9,6 +9,7 @@ from .association_config import DEFAULT_ASSOCIATION_CONFIG
|
||||
from .memory_config import DEFAULT_MEMORY_CONFIG
|
||||
from .mots_config import DEFAULT_MOTS_CONFIG
|
||||
from .quiz_config import DEFAULT_QUIZ_CONFIG
|
||||
from .scenario_config import DEFAULT_SCENARIO_CONFIG
|
||||
|
||||
SHAPE_KINDS = ("rectangle", "cercle", "triangle", "trait")
|
||||
CONTENT_KINDS = ("titre", "paragraphe", "image", "bouton")
|
||||
@@ -94,6 +95,9 @@ def element_default_attributes(kind: str) -> dict[str, Any]:
|
||||
if kind == "mots":
|
||||
# Même raison de copie que "quiz"/"association"/"memory" ci-dessus (voir mots_config.py).
|
||||
return {**DEFAULT_MOTS_CONFIG, "words": list(DEFAULT_MOTS_CONFIG["words"])}
|
||||
if kind == "scenario":
|
||||
# Même raison de copie que ci-dessus (voir scenario_config.py).
|
||||
return {**DEFAULT_SCENARIO_CONFIG, "scenarios": list(DEFAULT_SCENARIO_CONFIG["scenarios"])}
|
||||
if kind in MINIGAME_KINDS:
|
||||
# Panneau Propriétés minimal (voir PLAN.md §3.5, dernier
|
||||
# paragraphe : "état par défaut en attendant sa spécification") —
|
||||
|
||||
@@ -179,3 +179,41 @@ supprimé de la liste, de même qu'un doublon exact d'un mot déjà retenu
|
||||
liste finale est tronquée à `MAX_WORDS`.
|
||||
- **Retour** : dict complet (mêmes clés que `DEFAULT_MOTS_CONFIG`).
|
||||
- **Exceptions** : aucune.
|
||||
|
||||
## `scenario_config.py` — modèle de données du mini-jeu Scénario
|
||||
|
||||
Cinquième mini-jeu implémenté : 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 (jamais mélangés,
|
||||
contrairement à Association/Memory/Mots mêlés). Même convention que
|
||||
`quiz_config.py`.
|
||||
|
||||
### `MIN_SCENARIO_CHOICES`, `MAX_SCENARIO_CHOICES: int`
|
||||
Bornes de validation (`2`/`4` choix par scénario). Nommées explicitement
|
||||
"SCENARIO" (pas juste `MIN_CHOICES`/`MAX_CHOICES`) pour éviter toute
|
||||
collision avec les constantes de même nom de `quiz_config.py`, qui
|
||||
bornent un concept différent (le nombre de réponses d'une question de
|
||||
quiz) — `document_engine/__init__.py` ré-exporte l'intégralité de l'API
|
||||
publique du paquet à plat, un nom générique entrerait en collision.
|
||||
|
||||
### `DEFAULT_SCENARIO_CONFIG: dict[str, Any]`
|
||||
`{"theme_color": "#ff5f2e", "scenarios": []}`.
|
||||
|
||||
### `sanitize_scenario_config(raw_config: Any) -> dict[str, Any]`
|
||||
Valide/nettoie une config de scénario arbitraire (JSON venu du client) —
|
||||
jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
|
||||
`DEFAULT_SCENARIO_CONFIG`. Chaque scénario de `raw_config["scenarios"]`
|
||||
est validé indépendamment (voir `_sanitize_scenario`/
|
||||
`_sanitize_scenario_choice`, privées) : la `situation` doit être non
|
||||
vide une fois `.strip()`-ée, et il doit rester au moins
|
||||
`MIN_SCENARIO_CHOICES` choix valides (texte non vide) une fois la liste
|
||||
tronquée à `MAX_SCENARIO_CHOICES` — sinon le scénario entier est
|
||||
silencieusement supprimé de la liste. La `consequence` d'un choix, elle,
|
||||
peut rester vide (un créateur peut vouloir la renseigner plus tard sans
|
||||
que ça invalide le choix). `correct_index` retombe sur `0` s'il est hors
|
||||
bornes ou absent.
|
||||
- **Retour** : dict complet (mêmes clés que `DEFAULT_SCENARIO_CONFIG`).
|
||||
- **Exceptions** : aucune.
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user