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:
co-authored by
Claude Sonnet 5
parent
cf46da3388
commit
4111081e1a
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user