"""Modèle de données du mini-jeu Quiz (voir docs/plan/PLAN.md §3.2) — seul mini-jeu implémenté pour l'instant (les 5 autres kinds de MINIGAME_KINDS restent un simple emplacement réservé, voir element_kind_labels.py). Même convention resolve_X/sanitize_X que game_engine/rendering/quiz_box_config.py (distinct, aucun import croisé — voir docs/plan/PLAN.md, deux moteurs isolés) : sanitize_quiz_config est pure, ne lève jamais, et renvoie toujours un dict complet (jamais partiel) pour que le rendu puisse supposer chaque clé présente.""" from typing import Any MIN_CHOICES = 2 MAX_CHOICES = 4 MIN_TIMER_SECONDS = 5 MAX_TIMER_SECONDS = 300 DEFAULT_TIMER_SECONDS = 30 DEFAULT_QUIZ_CONFIG: dict[str, Any] = { "theme_color": "#ff5f2e", "timer_enabled": False, "timer_seconds": DEFAULT_TIMER_SECONDS, "questions": [], } def _sanitize_question(raw: Any) -> dict[str, Any] | None: """None si la question est invalide (texte vide, moins de 2 choix non vides) — filtrée par sanitize_quiz_config plutôt que de faire échouer tout le quiz, même convention que db/dialogue_lines.py:: _sanitize_question_line côté jeu.""" if not isinstance(raw, dict): return None text = str(raw.get("text", "")).strip() if not text: return None raw_choices = raw.get("choices") if not isinstance(raw_choices, list): return None choices = [str(c).strip() for c in raw_choices if str(c).strip()][:MAX_CHOICES] if len(choices) < MIN_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 try: points = max(0, int(raw.get("points", 0))) except (TypeError, ValueError): points = 0 return {"text": text, "choices": choices, "correct_index": correct_index, "points": points} def sanitize_quiz_config(raw_config: Any) -> dict[str, Any]: config = dict(DEFAULT_QUIZ_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 config["timer_enabled"] = bool(raw_config.get("timer_enabled")) try: timer_seconds = int(raw_config.get("timer_seconds", DEFAULT_TIMER_SECONDS)) except (TypeError, ValueError): timer_seconds = DEFAULT_TIMER_SECONDS config["timer_seconds"] = max(MIN_TIMER_SECONDS, min(MAX_TIMER_SECONDS, timer_seconds)) raw_questions = raw_config.get("questions") if isinstance(raw_questions, list): config["questions"] = [q for q in (_sanitize_question(item) for item in raw_questions) if q is not None] return config def quiz_total_points(config: dict[str, Any]) -> int: """Somme des points de toutes les questions — mirroir de db/dialogue_lines.py::sum_question_rewards côté jeu, utile le jour où un export calculera un score maximum.""" return sum(int(q.get("points", 0)) for q in config.get("questions", []))