Implemente le mini-jeu Memory (retournement de cartes, mode paire/single)

document_engine/labels/memory_config.py (nouveau) : modele de donnees,
meme convention resolve_X/sanitize_X que quiz_config.py/
association_config.py - DEFAULT_MEMORY_CONFIG, sanitize_memory_config.
Chaque carte a un recto ET un verso, chacun avec image/texte
independants et tous deux optionnels ; seul le verso doit avoir au
moins l'un des deux non vide (rien a reveler/apparier sinon) - le
recto peut rester entierement vide (dos de carte generique "?" par
defaut). Liste tronquee a MAX_CARDS=8.

Cote serveur, routes/document/document_element_update.py revalide
desormais aussi memory avant persistance. Le rendu (_render_memory)
affiche un resume reel (nombre de cartes, mode) et, des qu'au moins
une carte existe, un plateau de retournement REELEMENT interactif en
Mode Apercu (_render_memory_player) : en mode "paire", chaque carte
definie est DUPLIQUEE en deux instances partageant le meme card_index
(appariement classique) ; en mode "single", une seule instance par
carte (simple retournement, sans appariement - "c'est donc un
retourner de carte classique plus un jeu memory"). Les instances sont
melangees (random.shuffle, documente dans CODE_QUALITY.md) puis
embarquees en JSON dans data-memory-config.

Cote editeur, le panneau Proprietes propose un bascule segmentee
Paire/Simple et une liste de cartes repetable, chaque carte avec ses
deux faces (recto/verso) editables independamment (image + texte).
Extrait au passage forgeDocEscapeHtml (ex-forgeDocEscapeForTextarea,
generalise pour couvrir aussi les attributs) reutilise pour les deux
mini-jeux. Le plateau jouable (static/document/js/document-editor.js)
est une vraie carte-retournement CSS 3D (perspective/rotateY), contenu
de chaque face construit via DOM (textContent/img.src, jamais
innerHTML avec le texte du createur - meme precaution que le plateau
Association) : bon appariement verrouille en vert, mauvais reinitialise
apres un delai, ecran de resultat une fois le jeu termine (les deux
modes), et un "Recommencer" qui remelange reellement les cartes
(Fisher-Yates cote client).

Tests : 11 tests purs (tests/document/test_memory_config.py, sans
Flask, dont un qui verifie explicitement la duplication en mode paire
vs son absence en mode single) + 1 test de route verifiant la
sanitization a l'ecriture.

SKIP=djlint : backlog H021 pre-existant, aucun template touche ici.
ruff/mypy --strict/vulture/bandit/import-linter/eslint/stylelint tous
verts ; 62 tests document verifies frais. Verification manuelle live
complete : ajout, sanitization sur carte invalide, rendu du plateau,
et simulation DOM du gameplay reel dans les DEUX modes (mode paire :
mauvais appariement puis bon appariement puis jeu complet ; mode
single : retournement puis jeu complet) - script de diagnostic non
conserve dans le depot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-20 14:13:28 +02:00
co-authored by Claude Sonnet 5
parent cf46da3388
commit 4111081e1a
12 changed files with 732 additions and 39 deletions
@@ -250,6 +250,64 @@ def _render_association(el: dict[str, Any], _children_by_parent: dict[int | None
)
def _render_memory_player(config: dict[str, Any]) -> str:
"""Plateau de Memory RÉELLEMENT interactif — affiché uniquement en
Mode Aperçu, même principe que _render_quiz_player/
_render_association_player. En mode "paire", chaque carte définie par
le créateur est dupliquée en deux instances partageant le même
card_index (l'appariement se fait dessus) ; en mode "single", une
seule instance par carte (simple retournement, sans appariement).
Les instances sont mélangées une seule fois ici (jamais recalculées
à chaque rendu répété d'un même Aperçu, voir la remarque dans
static/document/js/document-editor.js sur la ré-init au
rafraîchissement du canevas) puis embarquées en JSON."""
cards = config["cards"]
mode = config["mode"]
instances = []
for i, card in enumerate(cards):
instances.append({"card_index": i, "recto": card["recto"], "verso": card["verso"]})
if mode == "paire":
instances.append({"card_index": i, "recto": card["recto"], "verso": card["verso"]})
random.shuffle(instances) # NOSONAR python:S2245 - melange d'affichage, pas un usage cryptographique
config_json = html_lib.escape(json.dumps({"mode": mode, "cards": instances}), quote=True)
return (
f'<div class="docMemoryPlayer" data-memory-config="{config_json}">'
f'<div class="docMemoryCardWrap">'
f'<div class="docQuizKicker">Memory</div>'
f'<div class="docAssocTitle docMemoryTitle"></div>'
f'<div class="docAssocMeta"><span class="docMemoryProg"></span></div>'
f'<div class="docMemoryGrid"></div>'
f"</div>"
f'<div class="docAssocResultCard docMemoryResultCard" style="display:none;">'
f'<div class="docAssocResultBig">✓</div>'
f'<div class="docAssocResultSub docMemoryResultSub"></div>'
f'<button type="button" class="docAssocRestartBtn docMemoryRestartBtn">Recommencer</button>'
f"</div></div>"
)
def _render_memory(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
from ..labels.memory_config import sanitize_memory_config
config = sanitize_memory_config(el["attributes"])
theme_color = html_lib.escape(str(config["theme_color"]))
card_count = len(config["cards"])
card_label = "carte" if card_count <= 1 else "cartes"
mode_label = "mode paire" if config["mode"] == "paire" else "mode simple"
subtitle = f"{card_count} {card_label} · {mode_label}"
player_html = _render_memory_player(config) if config["cards"] else ""
return (
f'<div class="docMinigame" data-element-id="{el["id"]}" data-kind="memory" '
f'style="border-color:{theme_color};">'
f'<div class="docMinigameBadge">'
f'<span class="docMinigameLabel">Memory</span>'
f'<span class="docMinigamePlaceholder">{html_lib.escape(subtitle)}</span>'
f"</div>"
f"{player_html}"
f"</div>"
)
def _render_unknown(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
return f'<div class="docUnknown" data-element-id="{el["id"]}">Type inconnu : {html_lib.escape(el["kind"])}</div>'
@@ -266,7 +324,7 @@ _RENDERERS = {
"bouton": _render_button,
"quiz": _render_quiz,
"association": _render_association,
"memory": _render_minigame_placeholder,
"memory": _render_memory,
"mots": _render_minigame_placeholder,
"scenario": _render_minigame_placeholder,
"zones": _render_minigame_placeholder,
+14 -2
View File
@@ -58,7 +58,19 @@ regroupement à chaque appel.
`CODE_QUALITY.md`) puis embarquées en JSON dans un attribut
`data-assoc-config`, échappé pour l'HTML — même principe que le Quiz,
aucun aller-retour serveur pendant qu'on joue.
- **Autres mini-jeux** (`memory`/`mots`/`scenario`/`zones`) : carte
placeholder portant le libellé du type (voir
- **Memory** : toujours une carte résumant la config réelle (nombre de
cartes définies, mode paire/simple) — sanitizée
(`sanitize_memory_config`) avant lecture. Si au moins une carte existe,
s'y ajoute (fonction privée `_render_memory_player`) le plateau de
retournement RÉEL et interactif : en mode `"paire"`, chaque carte
définie est DUPLIQUÉE en deux instances partageant le même
`card_index` (l'appariement se fait dessus, classique Memory) ; en mode
`"single"`, une seule instance par carte (simple retournement, sans
appariement). Les instances sont mélangées (`random.shuffle`, mélange
d'affichage — voir `CODE_QUALITY.md`) puis embarquées en JSON dans un
attribut `data-memory-config`, échappé pour l'HTML — même principe que
le Quiz/l'Association, aucun aller-retour serveur pendant qu'on joue.
- **Autres mini-jeux** (`mots`/`scenario`/`zones`) : carte placeholder
portant le libellé du type (voir
`document_engine/labels/element_kind_labels.py`) — emplacement réservé,
formulaire de contenu dédié hors périmètre de cette passe.