"""Modèle de données du mini-jeu Association (voir docs/plan/PLAN.md §3.2) — l'apprenant relie chaque carte de gauche à sa correspondance de droite par glisser-déposer (voir document_engine/rendering/render_document_element.py ::_render_association_player). Même convention resolve_X/sanitize_X que quiz_config.py et game_engine/rendering/quiz_box_config.py côté jeu (aucun import croisé).""" from typing import Any MIN_PAIRS = 2 MAX_PAIRS = 8 DEFAULT_ASSOCIATION_CONFIG: dict[str, Any] = { "theme_color": "#ff5f2e", "pairs": [], } def _sanitize_pair(raw: Any) -> dict[str, str] | None: """None si la paire est invalide (un des deux côtés vide) — filtrée par sanitize_association_config plutôt que de faire échouer tout le mini-jeu, même convention que quiz_config.py::_sanitize_question.""" if not isinstance(raw, dict): return None left = str(raw.get("left", "")).strip() right = str(raw.get("right", "")).strip() if not left or not right: return None return {"left": left, "right": right} def sanitize_association_config(raw_config: Any) -> dict[str, Any]: config = dict(DEFAULT_ASSOCIATION_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_pairs = raw_config.get("pairs") if isinstance(raw_pairs, list): pairs = [p for p in (_sanitize_pair(item) for item in raw_pairs) if p is not None] config["pairs"] = pairs[:MAX_PAIRS] return config