Phase 1 (1/3) : état par joueur — couche db/ et chaîne de rendu
Build and deploy / test (push) Failing after 8s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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>
This commit is contained in:
william
2026-08-30 15:31:08 +02:00
co-authored by Claude Sonnet 5
parent bb84b7b377
commit 236d6b4b46
33 changed files with 469 additions and 107 deletions
+14 -3
View File
@@ -7,18 +7,28 @@ from .get_definition import get_definition
from .ensure_field_bounds_schema import ensure_field_bounds_schema
def create_definition(slug, name, fields):
def create_definition(slug, name, fields, per_player=True):
"""Feature 2 : crée une définition d'objet (comme une table de BDD) et
exécute le vrai CREATE TABLE correspondant, avec les bons types de
colonne, y compris les colonnes de clé étrangère pour les relations
vers d'autres définitions déjà créées dans CE jeu (une définition peut
donc "en contenir" une autre, au sens relationnel)."""
donc "en contenir" une autre, au sens relationnel).
per_player (état par joueur, Phase 1) : True (par défaut) = chaque
joueur a ses propres lignes (une table player_id-scopée qui démarre
VIDE pour chaque nouveau joueur — pas de valeur par défaut à copier,
contrairement à une variable globale, voir
db/global_vars/update_global_variable_value.py) ; False = lignes
partagées par tous les joueurs (ex. un catalogue/une liste de
questions), toujours player_id=PLAYER_SHARED quel que soit le joueur
qui consulte."""
ensure_field_bounds_schema(slug)
conn = connect(slug)
tname = table_name_for(name)
conn.execute(
"INSERT INTO _definitions (name, table_name) VALUES (?, ?)", (name, tname)
"INSERT INTO _definitions (name, table_name, per_player) VALUES (?, ?, ?)",
(name, tname, 1 if per_player else 0),
)
definition_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
@@ -50,6 +60,7 @@ def create_definition(slug, name, fields):
(definition_id, f["name"], ftype, relation_definition_id, required, pos, min_value, max_value),
)
columns_sql.append("player_id TEXT NOT NULL DEFAULT '__shared__'")
columns_sql.append("created_at TEXT DEFAULT CURRENT_TIMESTAMP")
create_sql = f"CREATE TABLE {tname} ({', '.join(columns_sql)})"
conn.execute(create_sql)
+8 -2
View File
@@ -4,13 +4,19 @@ from ..connection import connect
def ensure_field_bounds_schema(slug):
"""Migration légère (voir screens/screens_repo/ensure_schema.py pour le
même principe) : ajoute les colonnes min_value/max_value à _fields pour
les jeux créés avant le bornage automatique (2.2) — aucune migration
manuelle nécessaire, la prochaine lecture d'une définition suffit."""
les jeux créés avant le bornage automatique (2.2), et per_player à
_definitions pour les jeux créés avant l'état par joueur (Phase 1) —
aucune migration manuelle nécessaire, la prochaine lecture d'une
définition suffit. per_player=1 par défaut (comportement le plus
proche de l'ancien objet-par-défaut : voir create_definition.py)."""
conn = connect(slug)
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_fields)").fetchall()}
if "min_value" not in cols:
conn.execute("ALTER TABLE _fields ADD COLUMN min_value REAL")
if "max_value" not in cols:
conn.execute("ALTER TABLE _fields ADD COLUMN max_value REAL")
definition_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_definitions)").fetchall()}
if "per_player" not in definition_cols:
conn.execute("ALTER TABLE _definitions ADD COLUMN per_player INTEGER NOT NULL DEFAULT 1")
conn.commit()
conn.close()