"""Modèle de données du mini-jeu Memory (voir docs/plan/PLAN.md §3.2) — l'apprenant retourne des cartes pour constituer des paires identiques (mode "paire") ou simplement révéler chaque carte une fois (mode "single", un retournement classique sans appariement). Même convention resolve_X/sanitize_X que quiz_config.py/association_config.py (aucun import croisé).""" from typing import Any MIN_CARDS = 2 MAX_CARDS = 8 CARD_MODES = ("paire", "single") DEFAULT_MODE = "paire" DEFAULT_MEMORY_CONFIG: dict[str, Any] = { "theme_color": "#ff5f2e", "mode": DEFAULT_MODE, "cards": [], } def _sanitize_card_face(raw: Any) -> dict[str, str]: """Une face de carte (recto ou verso) — image et texte tous deux optionnels et indépendants (le créateur peut mettre l'un, l'autre, ou les deux, voir docs/plan/PLAN.md).""" if not isinstance(raw, dict): return {"image": "", "text": ""} return { "image": str(raw.get("image", "")).strip(), "text": str(raw.get("text", "")).strip(), } def _sanitize_card(raw: Any) -> dict[str, Any] | None: """None si la carte est invalide — un verso entièrement vide (ni image ni texte) n'aurait rien à révéler/apparier, contrairement au recto qui peut légitimement rester vide (dos de carte générique par défaut).""" if not isinstance(raw, dict): return None recto = _sanitize_card_face(raw.get("recto")) verso = _sanitize_card_face(raw.get("verso")) if not verso["image"] and not verso["text"]: return None return {"recto": recto, "verso": verso} def sanitize_memory_config(raw_config: Any) -> dict[str, Any]: config = dict(DEFAULT_MEMORY_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 mode = raw_config.get("mode") config["mode"] = mode if mode in CARD_MODES else DEFAULT_MODE raw_cards = raw_config.get("cards") if isinstance(raw_cards, list): cards = [c for c in (_sanitize_card(item) for item in raw_cards) if c is not None] config["cards"] = cards[:MAX_CARDS] return config