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>
1. Duplication éliminée avant que la Phase 2 (hasard/opérations
mathématiques) n'en ajoute 6 de plus aux DEUX fichiers : la chaîne
d'opérations quasi identique entre screens/data_actions/
apply_data_action.py (champ d'objet) et apply_variable_action.py
(variable globale) est factorisée dans un nouveau
compute_operation.py::compute_new_value(operation, current, raw_value,
is_decimal), réutilisé par les deux. Nouveau tests/test_compute_operation.py
verrouille le comportement des 7 opérations existantes (dont les cas
limites : valeur invalide, type décimal vs entier, opération inconnue)
avant d'en ajouter d'autres.
2. .gitea/workflows/deploy.yml déployait en prod à chaque push sur main
sans jamais exécuter la suite de tests — rien ne bloquait
techniquement un commit cassé. Nouveau job "test" (pytest + node:test
sur la logique pure de static/js/play/, via des conteneurs officiels
plutôt que des actions du marketplace, cohérent avec le choix déjà
fait dans ce fichier) tourne sur CHAQUE push (main ET dev, utile pour
ce dépôt qui travaille sur dev) ; "build-and-push"/"deploy" gagnent un
"needs: test" et restent réservés à main (filtre sur gitea.ref) — un
push sur dev ne redéploie jamais la prod, seulement les tests.
Vérifié : 223 tests passent (8 nouveaux), YAML validé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>