Build and deploy / test-python (push) Failing after 1m0s
Build and deploy / test-js (push) Failing after 50s
Build and deploy / lint-python (push) Failing after 1m2s
Build and deploy / lint-js (push) Failing after 1m7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 1m1s
Quatrieme mini-jeu du support de formation : le createur ecrit 5 a 10 mots (recommandation souple, comme MIN_PAIRS/MIN_CARDS), places par le serveur dans une grille carree horizontalement, verticalement, ou en diagonale (deux sens seulement, jamais a l'envers) via un vrai algorithme de placement avec retry/agrandissement de grille en cas de conflit. La grille ET la position exacte de chaque mot sont calculees cote serveur puis embarquees en JSON ; le client valide chaque selection (glisser ou cliquer-cliquer) par comparaison de coordonnees exactes, jamais une simple comparaison de texte (qui se tromperait sur des lettres partagees entre deux mots qui se croisent). Comme Association/Memory, la grille reste affichee une fois tous les mots trouves : seul le bouton Recommencer (.docMinigameRestartBar, partage) apparait. Verifie via simulation DOM reelle (jsdom) : selection au glisser ET au clic-clic, mot invalide sans crash, barre de fin qui bascule, panneau Proprietes (ajout/suppression de mot avec revalidation serveur). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
2.2 KiB
Python
60 lines
2.2 KiB
Python
"""Modèle de données du mini-jeu Mots mêlés (voir docs/plan/PLAN.md §3.2)
|
|
— l'apprenant retrouve chaque mot caché dans une grille de lettres, placé
|
|
horizontalement, verticalement, ou en diagonale (haut-gauche vers
|
|
bas-droite, ou haut-droite vers bas-gauche), jamais à l'envers ni en
|
|
diagonale inversée (voir document_engine/rendering/render_document_element.py
|
|
::_render_mots_player, qui construit la grille elle-même). Même
|
|
convention resolve_X/sanitize_X que quiz_config.py/association_config.py/
|
|
memory_config.py (aucun import croisé)."""
|
|
|
|
import unicodedata
|
|
from typing import Any
|
|
|
|
MIN_WORDS = 5
|
|
MAX_WORDS = 10
|
|
MIN_WORD_LENGTH = 2
|
|
MAX_WORD_LENGTH = 20
|
|
|
|
DEFAULT_MOTS_CONFIG: dict[str, Any] = {
|
|
"theme_color": "#ff5f2e",
|
|
"words": [],
|
|
}
|
|
|
|
|
|
def _sanitize_word(raw: Any) -> str | None:
|
|
"""None si, une fois nettoyé, il ne reste aucune lettre exploitable —
|
|
filtré par sanitize_mots_config plutôt que de faire échouer toute la
|
|
grille, même convention que quiz_config.py::_sanitize_question. Les
|
|
accents sont retirés (décomposition NFKD puis filtrage ASCII) : deux
|
|
mots qui se croisent sur une même case doivent pouvoir partager
|
|
exactement la même lettre, ce qu'un "É" et un "E" ne permettraient
|
|
pas."""
|
|
if not isinstance(raw, str):
|
|
return None
|
|
decomposed = unicodedata.normalize("NFKD", raw.upper())
|
|
letters = "".join(ch for ch in decomposed if ch.isalpha() and ch.isascii())
|
|
if len(letters) < MIN_WORD_LENGTH:
|
|
return None
|
|
return letters[:MAX_WORD_LENGTH]
|
|
|
|
|
|
def sanitize_mots_config(raw_config: Any) -> dict[str, Any]:
|
|
config = dict(DEFAULT_MOTS_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_words = raw_config.get("words")
|
|
if isinstance(raw_words, list):
|
|
words: list[str] = []
|
|
seen: set[str] = set()
|
|
for item in raw_words:
|
|
word = _sanitize_word(item)
|
|
if word is None or word in seen:
|
|
continue
|
|
seen.add(word)
|
|
words.append(word)
|
|
config["words"] = words[:MAX_WORDS]
|
|
return config
|