Score/Progression (Phase 1.1 de la feuille de route produit)
Build and deploy / test-python (push) Successful in 7m27s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Concept de premier ordre, distinct du système de variables globales —
préalable identifié aux futurs exports SCORM/xAPI (note de cadrage
.claude/Forge_Engine_Cadrage.pdf) : ces standards ont besoin d'un signal
"score"/"terminé" propre, pas d'une convention sur une variable choisie
par le créateur.

- db/scoring/ (même patron que db/global_vars/) : table _scoring, un
  score numérique + un statut (non_commence/en_cours/termine/reussi/
  echoue) par joueur (voir db.PLAYER_SHARED pour l'aperçu créateur).
- Deux nouvelles actions de flux, dans les deux éditeurs (document et
  scène) : "Modifier le score" (réutilise le vocabulaire d'opérations
  déjà là pour "Modifier une variable" — incrémenter/définir/etc., aucune
  nouvelle colonne de nœud) et "Définir le statut de la partie".
- Routes créateur (routes/flow/flow_node_run_score.py,
  flow_node_run_status.py) + miroirs publics par joueur
  (routes/public_play/) — même principe que flow_node_run_variable.py.
- GET /game/<slug>/scoring/<player_id> : lecture interne, pas exposée au
  joueur, préparée pour être consommée par le futur export Web/SCORM.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 06:23:37 +02:00
co-authored by Claude Sonnet 5
parent 76a50fa87a
commit 9776fbc057
26 changed files with 486 additions and 1 deletions
+14
View File
@@ -41,3 +41,17 @@ GLOBAL_VARIABLE_TYPES = {
"objet": {"label": "Objet (JSON)"},
"tableau": {"label": "Tableau (JSON)"},
}
# Statuts de partie (voir db/scoring/) — préalable aux exports SCORM/xAPI,
# qui ont besoin d'un signal "terminé"/"réussi" propre plutôt que d'une
# convention sur une variable choisie par le créateur (voir le plan
# "Feuille de route produit"). "non_commence" est le défaut d'un joueur
# qui n'a encore rien écrit (voir db/scoring/get_score.py).
SCORE_STATUS_CHOICES = [
("non_commence", "Non commencée"),
("en_cours", "En cours"),
("termine", "Terminée"),
("reussi", "Réussie"),
("echoue", "Échouée"),
]
SCORE_STATUS_LABELS = dict(SCORE_STATUS_CHOICES)
+6
View File
@@ -0,0 +1,6 @@
"""Score/progression (voir le plan "Feuille de route produit") : un
concept de premier ordre, distinct du système de variables globales
(db/global_vars/) — un score numérique et un statut de partie, TOUJOURS
par joueur (voir db.PLAYER_SHARED pour l'aperçu créateur), préalable aux
exports SCORM/xAPI (qui ont besoin d'un signal "score"/"terminé" propre,
pas d'une convention sur une variable choisie par le créateur)."""
+22
View File
@@ -0,0 +1,22 @@
from ..connection import connect
def ensure_scoring_schema(slug):
"""Migration légère (même principe que ensure_global_vars_schema.py) :
crée _scoring si absente. Une seule ligne par joueur (player_id, voir
db.PLAYER_SHARED pour l'aperçu créateur — même modèle que
db/global_vars/, mais pas de notion "per_player=0" ici : le score
d'UN joueur n'a de sens que pour CE joueur, jamais partagé)."""
conn = connect(slug)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS _scoring (
player_id TEXT PRIMARY KEY,
score REAL NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'non_commence',
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
)
conn.commit()
conn.close()
+17
View File
@@ -0,0 +1,17 @@
from ..connection import connect
from ..global_vars.ensure_global_vars_schema import PLAYER_SHARED
from .ensure_scoring_schema import ensure_scoring_schema
def get_score(slug, player_id=PLAYER_SHARED):
"""Score/statut de CE joueur — (0, "non_commence") tant qu'il n'a
encore rien écrit (voir set_score_value.py/set_status.py, qui créent
la ligne à la première écriture, même patron que
update_global_variable_value.py)."""
ensure_scoring_schema(slug)
conn = connect(slug)
row = conn.execute("SELECT * FROM _scoring WHERE player_id = ?", (player_id,)).fetchone()
conn.close()
if not row:
return {"player_id": player_id, "score": 0, "status": "non_commence"}
return dict(row)
+23
View File
@@ -0,0 +1,23 @@
from ..connection import connect
from ..global_vars.ensure_global_vars_schema import PLAYER_SHARED
from .ensure_scoring_schema import ensure_scoring_schema
def set_score_value(slug, new_value, player_id=PLAYER_SHARED):
"""Écrit une nouvelle valeur BRUTE (déjà calculée par l'appelant, voir
screens/data_actions/apply_score_action.py) — même patron que
update_global_variable_value.py : la ligne de ce joueur est créée à sa
toute première écriture (INSERT ... ON CONFLICT évite un aller-retour
SELECT préalable, un joueur ne peut écrire que la SIENNE)."""
ensure_scoring_schema(slug)
conn = connect(slug)
conn.execute(
"""
INSERT INTO _scoring (player_id, score) VALUES (?, ?)
ON CONFLICT(player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP
""",
(player_id, new_value),
)
conn.commit()
conn.close()
return True
+25
View File
@@ -0,0 +1,25 @@
from ..connection import connect
from ..constants import SCORE_STATUS_LABELS
from ..global_vars.ensure_global_vars_schema import PLAYER_SHARED
from .ensure_scoring_schema import ensure_scoring_schema
def set_status(slug, status, player_id=PLAYER_SHARED):
"""Voir set_score_value.py pour le patron (upsert, ligne créée à la
première écriture). `status` doit être une clé de
db.SCORE_STATUS_LABELS — un statut inconnu (ex. faute de frappe dans
une valeur de nœud de flux) est refusé plutôt qu'écrit tel quel."""
if status not in SCORE_STATUS_LABELS:
return False
ensure_scoring_schema(slug)
conn = connect(slug)
conn.execute(
"""
INSERT INTO _scoring (player_id, status) VALUES (?, ?)
ON CONFLICT(player_id) DO UPDATE SET status = excluded.status, updated_at = CURRENT_TIMESTAMP
""",
(player_id, status),
)
conn.commit()
conn.close()
return True