Implemente le mini-jeu Mots meles (grille reelle, placement 4 directions)
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>
This commit is contained in:
william
2026-09-20 15:31:46 +02:00
co-authored by Claude Sonnet 5
parent 1b1307bb85
commit d8be80ebd1
11 changed files with 789 additions and 20 deletions
@@ -7,6 +7,7 @@ from typing import Any
from .association_config import DEFAULT_ASSOCIATION_CONFIG
from .memory_config import DEFAULT_MEMORY_CONFIG
from .mots_config import DEFAULT_MOTS_CONFIG
from .quiz_config import DEFAULT_QUIZ_CONFIG
SHAPE_KINDS = ("rectangle", "cercle", "triangle", "trait")
@@ -90,6 +91,9 @@ def element_default_attributes(kind: str) -> dict[str, Any]:
if kind == "memory":
# Même raison de copie que "quiz"/"association" ci-dessus (voir memory_config.py).
return {**DEFAULT_MEMORY_CONFIG, "cards": list(DEFAULT_MEMORY_CONFIG["cards"])}
if kind == "mots":
# Même raison de copie que "quiz"/"association"/"memory" ci-dessus (voir mots_config.py).
return {**DEFAULT_MOTS_CONFIG, "words": list(DEFAULT_MOTS_CONFIG["words"])}
if kind in MINIGAME_KINDS:
# Panneau Propriétés minimal (voir PLAN.md §3.5, dernier
# paragraphe : "état par défaut en attendant sa spécification") —
+40
View File
@@ -139,3 +139,43 @@ carte invalide est silencieusement supprimée de la liste. La liste finale
est tronquée à `MAX_CARDS`.
- **Retour** : dict complet (mêmes clés que `DEFAULT_MEMORY_CONFIG`).
- **Exceptions** : aucune.
## `mots_config.py` — modèle de données du mini-jeu Mots mêlés
Quatrième mini-jeu implémenté : l'apprenant retrouve chaque mot caché
dans une grille de lettres (horizontalement, verticalement, ou en
diagonale — voir `document_engine/rendering/render_document_element.py`::
`_render_mots_player`, qui construit la grille elle-même). Même
convention que `quiz_config.py`.
### `MIN_WORDS`, `MAX_WORDS: int`
Bornes de validation (`5`/`10` mots). `MIN_WORDS` n'est pas imposé par
`sanitize_mots_config` (même logique que `MIN_PAIRS`/`MIN_CARDS`
côté Association/Memory) — recommandation pour le panneau Propriétés,
pas une contrainte technique du rendu : un seul mot valide reste
accepté, la grille se construit quand même autour de lui.
### `MIN_WORD_LENGTH`, `MAX_WORD_LENGTH: int`
Bornes de longueur d'un mot individuel une fois nettoyé (`2`/`20`
lettres) — un mot trop court n'a pas de sens à chercher, un mot trop
long compliquerait inutilement le calcul de la taille de la grille
(voir `_mots_grid_size_for_words` dans `render_document_element.py`).
### `DEFAULT_MOTS_CONFIG: dict[str, Any]`
`{"theme_color": "#ff5f2e", "words": []}`.
### `sanitize_mots_config(raw_config: Any) -> dict[str, Any]`
Valide/nettoie une config de mots mêlés arbitraire (JSON venu du
client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur
`DEFAULT_MOTS_CONFIG`. Chaque mot de `raw_config["words"]` est nettoyé
indépendamment (voir `_sanitize_word`, privée) : mis en MAJUSCULES,
les accents sont retirés (décomposition NFKD + filtrage ASCII, pour que
deux mots qui se croisent sur une même case de la grille puissent
partager exactement la même lettre), seules les lettres sont
conservées. Un mot qui ne contient plus rien d'exploitable (ou moins de
`MIN_WORD_LENGTH` lettres) une fois nettoyé est silencieusement
supprimé de la liste, de même qu'un doublon exact d'un mot déjà retenu
(un mot répété deux fois n'aurait rien de plus à faire trouver). La
liste finale est tronquée à `MAX_WORDS`.
- **Retour** : dict complet (mêmes clés que `DEFAULT_MOTS_CONFIG`).
- **Exceptions** : aucune.
+59
View File
@@ -0,0 +1,59 @@
"""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