Fil directeur du plan : chaque variable globale et chaque objet de
données gagne un player_id (sentinelle PLAYER_SHARED='__shared__' par
défaut partout — l'aperçu créateur et tous les tests existants
continuent de fonctionner À L'IDENTIQUE, aucune signature appelée sans
argument explicite ne change de comportement), plus un réglage
per_player choisi une fois à la création :
- per_player=1 (par défaut) : chaque joueur a sa propre valeur/ses
propres lignes.
- per_player=0 : valeur/lignes partagées par tous les joueurs (ex. un
compteur de visiteurs global, un catalogue commun).
db/global_vars/ : _global_variables passe de UNIQUE(name) à
UNIQUE(name, player_id) — SQLite ne permet pas de modifier une
contrainte UNIQUE via ALTER TABLE, reconstruction de la table détectée
et faite une seule fois (ensure_global_vars_schema.py) pour les jeux
créés avant cette phase. La ligne "modèle" (player_id=PLAYER_SHARED,
créée par le créateur) porte le réglage per_player et sert de valeur
PAR DÉFAUT : la première écriture d'un joueur sur une variable
per_player crée paresseusement SA propre ligne (copiée depuis le
modèle) ; une lecture sans ligne encore écrite retombe sur le modèle
(nouveau resolve_player_key.py). list_global_variables() (tableau de
bord) ne montre toujours que les lignes modèles ; nouveau
list_global_variables_for_player() expose la valeur EFFECTIVE d'un
joueur au runtime (full_game_payload.py).
db/definitions/ + db/rows/ : chaque table d'objet généré
(create_definition.py) gagne une colonne player_id (ADD COLUMN simple,
pas de contrainte UNIQUE en jeu ici) ; _definitions gagne per_player.
Contrairement aux variables, PAS de repli sur une ligne "modèle" pour
les lignes d'un objet per_player — une LISTE n'a pas de valeur par
défaut unique à copier comme un scalaire, un nouvel objet per_player
démarre VIDE pour chaque joueur (nouveau resolve_row_player_key.py).
get_row/update_row/update_row_field/delete_row filtrent aussi par
player_id (pas seulement id) : garde-fou contre un row_id d'un AUTRE
joueur, nécessaire dès qu'un objet per_player sera exposé sur la future
route publique /jouer/<slug>. Migration : nouveau
ensure_player_id_column(slug, table_name), appelé avant toute requête
sur une table d'objet créée avant cette phase.
Chaîne de rendu (screens/elements/list_elements.py ->
screens/rendering/render_element_html.py -> render_repeater.py/
render_jauge.py/resolve_bound_row.py/visibility_condition.py/
filter_repeater_rows.py) : player_id transite dans le ctx déjà utilisé
partout pour "champ en cours" (ctx["_forge_player_id"], même patron que
ctx["_forge_play_mode"], posé une seule fois par list_elements quand
enforce_visibility=True) — pas de nouveau paramètre positionnel à
threader dans chaque fonction, juste une clé de plus dans un mécanisme
déjà en place.
Nouveau tests/test_player_state.py : verrouille à la fois le nouveau
comportement (deux joueurs => valeurs/lignes indépendantes ; per_player=0
=> partagé ; nouveau joueur => valeur par défaut pour une variable, liste
VIDE pour un objet ; get_row ne fuite jamais vers un autre joueur) et la
non-régression de l'aperçu créateur (comportement historique inchangé).
Vérifié : 232 tests passent (9 nouveaux). Reste à faire (prochains
commits) : route publique /jouer/<slug>, identité visiteur (cookie),
bascule "Publier en ligne" dans le tableau de bord.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute la vraie route hébergée multi-joueurs qui manquait totalement
(voir le constat d'exploration : /game/<slug>/play est réservé au
créateur connecté, "Publier" ne génère qu'un exécutable mono-joueur) —
un visiteur anonyme peut maintenant jouer un jeu explicitement publié en
ligne, avec sa propre partie (variables/objets per_player posés dans le
commit précédent).
core/player_identity.py : identité visiteur via un cookie NON SIGNÉ
(forge_player_id, secrets.token_urlsafe(16), 1 an) — une simple clé de
partition, jamais un jeton d'autorisation.
routes/public_play/ : 4 routes, miroirs des routes existantes de l'aperçu
créateur mais threadées avec le vrai player_id du cookie au lieu du
sentinel PLAYER_SHARED :
- GET /jouer/<slug> (game_play_public.py)
- GET /jouer/<slug>/runtime-payload
- POST /jouer/<slug>/flow/nodes/<id>/run-data
- POST /jouer/<slug>/flow/nodes/<id>/run-variable
Chacune vérifie elle-même db.is_public_played(slug) (404 sinon) — un jeu
n'est exposé publiquement que si le créateur l'a explicitement basculé
"Publier en ligne" (nouveau db/games/is_public_played.py, réutilise la
table générique _meta, comme game_meta.py pour 'name').
core/auth_guard.py : les 4 endpoints publics ajoutés à _PUBLIC_ENDPOINTS
— la garde générique de connexion les laisse passer sans session, mais
chaque vue vérifie quand même is_public_played elle-même (défense en
profondeur, pas seulement une liste d'exceptions). CSRF (core/csrf_guard.py)
n'a besoin d'AUCUN changement : le jeton est déjà lié à la session Flask,
qui existe pour n'importe quel visiteur (connecté ou non).
templates/play.html : FORGE_PLAY_URLS (posé en Phase -1) ne construit
plus ses URLs via des noms de endpoint fixes (url_for('runtime_payload',
...)) mais reçoit des URLs déjà résolues par la route elle-même
(runtime_payload_url/flow_node_run_data_url/flow_node_run_variable_url)
— nécessaire puisque ce même template sert maintenant DEUX familles de
routes (aperçu créateur ET partie publique), chacune avec ses propres
noms de endpoint. routes/play/game_play.py (aperçu créateur, INCHANGÉ
comportement) et game_play_public.py passent chacun ses propres URLs.
templates/base.html : bascule "🌐 Publier en ligne" dans la barre de
navigation du jeu, à côté de "📦 Publier" (export .zip) — deux
fonctionnalités distinctes. db/games/game_meta.py expose maintenant
is_public_played, disponible partout où `game` est dans le contexte.
Vérifié : 237 tests passent (5 nouveaux dans test_public_play.py, dont un
bout-en-bout via HTTP avec deux VRAIS clients de test anonymes — deux
cookies forge_player_id différents — qui obtiennent des valeurs de
variable indépendantes, et un qui verrouille que l'aperçu créateur reste
inchangé). Syntaxe JS validée sur les deux variantes de play.html rendu
(aperçu créateur et partie publique).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dernier morceau du plan d'état par joueur : le créateur choisit, à la
création d'une variable globale ou d'un objet, si chaque joueur aura sa
propre valeur/ses propres lignes (coché par défaut) ou si elle est
explicitement PARTAGÉE par tous les joueurs (ex. un compteur de visiteurs
global, un catalogue commun) — voir db/global_vars/create_global_variable.py
et db/definitions/create_definition.py (Phase 1, 1er commit).
routes/global_vars/create_global_var.py, routes/objects/object_new.py :
lisent la case à cocher "per_player" du formulaire (absente => reste
per_player=1, comportement par défaut). templates/game_dashboard.html :
case à cocher sur les deux panneaux de création + colonne "Par joueur"
dans les deux tableaux existants, pour que ce réglage (immuable après
création, comme le nom d'une variable) reste visible.
db/definitions/list_definitions.py appelait _definitions directement
sans jamais migrer son schéma — un tableau de bord ouvert avant la toute
première création/modification d'objet aurait affiché "Non — partagé"
pour un objet en réalité per_player=1 (colonne absente => Undefined,
donc faux en Jinja) : corrigé en appelant ensure_field_bounds_schema()
ici aussi, comme le fait déjà create_definition.py/get_definition.py.
Vérifié : 239 tests passent (2 nouveaux, dont un qui aurait détecté le
bug ci-dessus). Phase 1 (état par joueur) est maintenant complète :
couche db/, route publique /jouer/<slug>, et ce réglage créateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le job "test" échouait ("Could not open requirements file:
requirements-dev.txt") : docker run -v "$PWD":/app lancé DEPUIS un
runner qui exécute déjà le job dans son propre conteneur (Docker-
outside-of-Docker) ne peut pas monter "$PWD" — ce chemin vit dans le
conteneur du job, pas sur l'hôte où tourne le vrai démon Docker sollicité
par ce docker run imbriqué ; /app se retrouvait donc vide dans le
conteneur imbriqué.
Corrigé en utilisant la clé "container" (standard Gitea/GitHub Actions) :
le job tourne DIRECTEMENT dans l'image voulue, le checkout dépose les
fichiers dans son propre système de fichiers, aucun montage de volume à
faire. Un seul job "test" ne peut avoir qu'UNE image : scindé en
test-python (python:3.13-slim) et test-js (node:20-slim, pour les tests
node:test de static/js/play/__tests__/), build-and-push dépend des deux.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"container: image: python:3.13-slim" (tentative précédente) casse
actions/checkout@v4 : c'est une action Node.js, qui a besoin de Node
dans l'environnement d'exécution des steps — "container:" remplace CET
environnement en entier par l'image donnée, qui n'a pas Node
("command not found", nektos/act#107), pas seulement l'environnement
des commandes qu'on y lance soi-même.
Nouvelle approche : le job tourne sur le runner par défaut (checkout
fonctionne normalement, Node y est déjà disponible), et les tests
s'exécutent PENDANT un `docker build` (Dockerfile jetable passé par
stdin, jamais commité, un par langage) plutôt que dans un conteneur
lancé après coup — le transfert du contexte de build vers le démon
Docker passe par le protocole API (tar), jamais par un chemin hôte à
monter, donc insensible au problème Docker-outside-of-Docker qui avait
fait échouer le tout premier essai (docker run -v "$PWD":/app).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"no tests ran ... file or directory not found: tests/" : .dockerignore
(à la racine, pensé pour l'image de PROD buildée par build-and-push)
exclut tests/ du contexte de build — COPY . . dans le Dockerfile jetable
de test-python ne l'incluait donc jamais, quel que soit le Dockerfile
utilisé (.dockerignore s'applique au contexte entier envoyé au démon,
pas à un -f en particulier).
Renomme .dockerignore avant ce build précis (le checkout de ce job est
jetable, propre à lui, jamais repoussé vers le dépôt réel) — test-js n'a
pas besoin du même correctif, il ne copie que static/js/play/, jamais
exclu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Ajoute la vraie route hébergée multi-joueurs qui manquait totalement (voir le constat d'exploration : /game/<slug>/play est réservé au créateur connecté, "Publier" ne génère qu'un exécutable mono-joueur) — un visiteur anonyme peut maintenant jouer un jeu explicitement publié en ligne, avec sa propre partie (variables/objets per_player posés dans le commit précédent). core/player_identity.py : identité visiteur via un cookie NON SIGNÉ (forge_player_id, secrets.token_urlsafe(16), 1 an) — une simple clé de partition, jamais un jeton d'autorisation. routes/public_play/ : 4 routes, miroirs des routes existantes de l'aperçu créateur mais threadées avec le vrai player_id du cookie au lieu du sentinel PLAYER_SHARED : - GET /jouer/<slug> (game_play_public.py) - GET /jouer/<slug>/runtime-payload - POST /jouer/<slug>/flow/nodes/<id>/run-data - POST /jouer/<slug>/flow/nodes/<id>/run-variable Chacune vérifie elle-même db.is_public_played(slug) (404 sinon) — un jeu n'est exposé publiquement que si le créateur l'a explicitement basculé "Publier en ligne" (nouveau db/games/is_public_played.py, réutilise la table générique _meta, comme game_meta.py pour 'name'). core/auth_guard.py : les 4 endpoints publics ajoutés à _PUBLIC_ENDPOINTS — la garde générique de connexion les laisse passer sans session, mais chaque vue vérifie quand même is_public_played elle-même (défense en profondeur, pas seulement une liste d'exceptions). CSRF (core/csrf_guard.py) n'a besoin d'AUCUN changement : le jeton est déjà lié à la session Flask, qui existe pour n'importe quel visiteur (connecté ou non). templates/play.html : FORGE_PLAY_URLS (posé en Phase -1) ne construit plus ses URLs via des noms de endpoint fixes (url_for('runtime_payload', ...)) mais reçoit des URLs déjà résolues par la route elle-même (runtime_payload_url/flow_node_run_data_url/flow_node_run_variable_url) — nécessaire puisque ce même template sert maintenant DEUX familles de routes (aperçu créateur ET partie publique), chacune avec ses propres noms de endpoint. routes/play/game_play.py (aperçu créateur, INCHANGÉ comportement) et game_play_public.py passent chacun ses propres URLs. templates/base.html : bascule "🌐 Publier en ligne" dans la barre de navigation du jeu, à côté de "📦 Publier" (export .zip) — deux fonctionnalités distinctes. db/games/game_meta.py expose maintenant is_public_played, disponible partout où `game` est dans le contexte. Vérifié : 237 tests passent (5 nouveaux dans test_public_play.py, dont un bout-en-bout via HTTP avec deux VRAIS clients de test anonymes — deux cookies forge_player_id différents — qui obtiennent des valeurs de variable indépendantes, et un qui verrouille que l'aperçu créateur reste inchangé). Syntaxe JS validée sur les deux variantes de play.html rendu (aperçu créateur et partie publique). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Le job "test" échouait ("Could not open requirements file: requirements-dev.txt") : docker run -v "$PWD":/app lancé DEPUIS un runner qui exécute déjà le job dans son propre conteneur (Docker- outside-of-Docker) ne peut pas monter "$PWD" — ce chemin vit dans le conteneur du job, pas sur l'hôte où tourne le vrai démon Docker sollicité par ce docker run imbriqué ; /app se retrouvait donc vide dans le conteneur imbriqué. Corrigé en utilisant la clé "container" (standard Gitea/GitHub Actions) : le job tourne DIRECTEMENT dans l'image voulue, le checkout dépose les fichiers dans son propre système de fichiers, aucun montage de volume à faire. Un seul job "test" ne peut avoir qu'UNE image : scindé en test-python (python:3.13-slim) et test-js (node:20-slim, pour les tests node:test de static/js/play/__tests__/), build-and-push dépend des deux. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>"container: image: python:3.13-slim" (tentative précédente) casse actions/checkout@v4 : c'est une action Node.js, qui a besoin de Node dans l'environnement d'exécution des steps — "container:" remplace CET environnement en entier par l'image donnée, qui n'a pas Node ("command not found", nektos/act#107), pas seulement l'environnement des commandes qu'on y lance soi-même. Nouvelle approche : le job tourne sur le runner par défaut (checkout fonctionne normalement, Node y est déjà disponible), et les tests s'exécutent PENDANT un `docker build` (Dockerfile jetable passé par stdin, jamais commité, un par langage) plutôt que dans un conteneur lancé après coup — le transfert du contexte de build vers le démon Docker passe par le protocole API (tar), jamais par un chemin hôte à monter, donc insensible au problème Docker-outside-of-Docker qui avait fait échouer le tout premier essai (docker run -v "$PWD":/app). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>