Dev #7
@@ -19,9 +19,18 @@ from .flask_app import app
|
||||
# Endpoints accessibles SANS être connecté — tout le reste exige une
|
||||
# session valide. "static" (CSS/JS/images) doit rester public : la page
|
||||
# de connexion elle-même en a besoin pour s'afficher.
|
||||
#
|
||||
# game_play_public/runtime_payload_public/flow_node_run_data_public/
|
||||
# flow_node_run_variable_public (routes/public_play/) : la route PUBLIQUE
|
||||
# /jouer/<slug>, pour un vrai visiteur anonyme (état par joueur, Phase 1)
|
||||
# — chaque vue vérifie ELLE-MÊME db.is_public_played(slug) (404 sinon),
|
||||
# cette garde générique ne fait qu'autoriser l'accès sans connexion, elle
|
||||
# ne dispense d'aucune autre vérification.
|
||||
_PUBLIC_ENDPOINTS = {
|
||||
"static", "login", "login_2fa", "register", "register_2fa", "logout",
|
||||
"forgot_password", "reset_password",
|
||||
"game_play_public", "runtime_payload_public",
|
||||
"flow_node_run_data_public", "flow_node_run_variable_public",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Identité d'un visiteur anonyme de la route publique /jouer/<slug> (voir
|
||||
routes/public_play/) — état par joueur, Phase 1. Un cookie NON SIGNÉ (pas
|
||||
besoin de itsdangerous/app.secret_key ici) : c'est une simple clé de
|
||||
PARTITION des données (quelles lignes/valeurs appartiennent à ce visiteur),
|
||||
jamais un jeton d'autorisation — la contrefaçon la plus grave possible
|
||||
serait de se faire passer pour un AUTRE joueur anonyme du même jeu, pas de
|
||||
franchir une frontière de sécurité (l'aperçu créateur, lui, reste protégé
|
||||
par core/auth_guard.py, complètement indépendant de ce mécanisme)."""
|
||||
import secrets
|
||||
|
||||
from flask import request
|
||||
|
||||
PLAYER_ID_COOKIE = "forge_player_id"
|
||||
_PLAYER_ID_MAX_AGE = 60 * 60 * 24 * 365 # 1 an
|
||||
|
||||
|
||||
def get_or_create_player_id():
|
||||
"""Renvoie (player_id, is_new) — is_new indique s'il faut poser le
|
||||
cookie sur la réponse avant de la renvoyer (voir
|
||||
set_player_id_cookie ci-dessous) : à utiliser aussi bien depuis une
|
||||
route qui rend une page (GET /jouer/<slug>) qu'une route API JSON
|
||||
(runtime-payload, run-data, run-variable), dont la réponse n'est
|
||||
construite qu'APRÈS avoir déjà besoin du player_id pour agir."""
|
||||
existing = request.cookies.get(PLAYER_ID_COOKIE)
|
||||
if existing:
|
||||
return existing, False
|
||||
return secrets.token_urlsafe(16), True
|
||||
|
||||
|
||||
def set_player_id_cookie(response, player_id):
|
||||
response.set_cookie(
|
||||
PLAYER_ID_COOKIE, player_id,
|
||||
max_age=_PLAYER_ID_MAX_AGE, httponly=True, samesite="Lax",
|
||||
)
|
||||
return response
|
||||
@@ -29,6 +29,8 @@ from .connection import connect
|
||||
|
||||
from .games.list_games import list_games
|
||||
from .games.game_meta import game_meta
|
||||
from .games.is_public_played import is_public_played
|
||||
from .games.set_public_played import set_public_played
|
||||
from .games.create_game import create_game
|
||||
from .games.update_game_name import update_game_name
|
||||
from .games.delete_game import delete_game
|
||||
@@ -72,6 +74,7 @@ __all__ = [
|
||||
"PROJECTS_DIR", "FIELD_TYPES", "GLOBAL_VARIABLE_TYPES", "PLAYER_SHARED",
|
||||
"slugify", "table_name_for", "game_dir", "db_path", "connect",
|
||||
"list_games", "game_meta", "create_game", "update_game_name", "delete_game", "move_game",
|
||||
"is_public_played", "set_public_played",
|
||||
"list_definitions", "get_definition", "create_definition", "rename_definition",
|
||||
"definitions_referencing", "add_field_to_definition", "update_field", "delete_field",
|
||||
"delete_definition",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from ..connection import connect
|
||||
from .is_public_played import is_public_played
|
||||
|
||||
|
||||
def game_meta(slug):
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'name'").fetchone()
|
||||
conn.close()
|
||||
return {"slug": slug, "name": row["value"] if row else slug}
|
||||
return {
|
||||
"slug": slug, "name": row["value"] if row else slug,
|
||||
# État par joueur (Phase 1) : disponible partout où `game` est
|
||||
# dans le contexte du template (base.html, la bascule "Publier en
|
||||
# ligne" en a besoin pour afficher son état actuel), sans avoir à
|
||||
# appeler is_public_played(slug) séparément dans chaque route.
|
||||
"is_public_played": is_public_played(slug),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def is_public_played(slug):
|
||||
"""Un jeu n'est jouable sur la route publique /jouer/<slug> (voir
|
||||
routes/public_play/) que si le créateur l'a explicitement basculé
|
||||
"Publier en ligne" dans son tableau de bord — jamais par défaut, pour
|
||||
ne pas exposer un jeu en cours de conception. Réutilise la table
|
||||
générique _meta (voir game_meta.py, déjà utilisée pour 'name'),
|
||||
aucune nouvelle colonne/table nécessaire."""
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT value FROM _meta WHERE key = 'is_public_played'").fetchone()
|
||||
conn.close()
|
||||
return bool(row and row["value"] == "1")
|
||||
@@ -0,0 +1,14 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def set_public_played(slug, value):
|
||||
"""Bascule "Publier en ligne" (tableau de bord) — voir
|
||||
is_public_played.py. `value` : bool."""
|
||||
conn = connect(slug)
|
||||
conn.execute(
|
||||
"INSERT INTO _meta (key, value) VALUES ('is_public_played', ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
("1" if value else "0",),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
+1
-1
@@ -1 +1 @@
|
||||
from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks
|
||||
from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks, public_play
|
||||
|
||||
@@ -1 +1 @@
|
||||
from . import index, games_new, game_dashboard, game_rename, game_delete
|
||||
from . import index, games_new, game_dashboard, game_rename, game_delete, game_toggle_public_played
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from flask import request, redirect, url_for
|
||||
|
||||
import db
|
||||
|
||||
from core.flask_app import app
|
||||
|
||||
|
||||
@app.route("/game/<slug>/toggle-public-played", methods=["POST"])
|
||||
def game_toggle_public_played(slug):
|
||||
"""Bascule "Publier en ligne" (voir db/games/is_public_played.py) —
|
||||
active/désactive la route publique /jouer/<slug> (routes/public_play/),
|
||||
jouable par n'importe quel visiteur anonyme avec sa propre partie
|
||||
(état par joueur, Phase 1). Distinct de "Publier" (export .zip
|
||||
exécutable mono-joueur, voir routes/publish/publish_game.py)."""
|
||||
db.set_public_played(slug, request.form.get("value") == "1")
|
||||
return redirect(request.referrer or url_for("game_dashboard", slug=slug))
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import render_template
|
||||
from flask import render_template, url_for
|
||||
|
||||
import db
|
||||
import screens
|
||||
@@ -10,8 +10,15 @@ from core.flask_app import app
|
||||
|
||||
@app.route("/game/<slug>/play")
|
||||
def game_play(slug):
|
||||
"""Aperçu créateur (connecté, voir core/auth_guard.py) — une seule
|
||||
partie partagée (PLAYER_SHARED, le défaut de full_game_payload), comme
|
||||
avant l'état par joueur (Phase 1). Distincte de /jouer/<slug>
|
||||
(routes/public_play/), la vraie route publique par joueur."""
|
||||
game = db.game_meta(slug)
|
||||
payload = screens.full_game_payload(slug)
|
||||
return render_template(
|
||||
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
||||
runtime_payload_url=url_for("runtime_payload", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable", slug=slug, node_id=999999),
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import game_play_public, runtime_payload_public, flow_node_run_data_public, flow_node_run_variable_public
|
||||
@@ -0,0 +1,29 @@
|
||||
from flask import jsonify, request, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/flow/nodes/<int:node_id>/run-data", methods=["POST"])
|
||||
def flow_node_run_data_public(slug, node_id):
|
||||
"""Miroir public de routes/flow/flow_node_run_data.py (aperçu créateur)
|
||||
— voir routes/public_play/game_play_public.py pour le principe
|
||||
général. `player_id` (le cookie du visiteur) est passé à
|
||||
apply_data_action pour qu'un objet per_player agisse sur SA ligne."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
|
||||
return jsonify({"ok": False}), 400
|
||||
if node.get("target_row_id") == screens.CLICKED_ROW_ID:
|
||||
body = request.get_json(silent=True) or {}
|
||||
node = {**node, "target_row_id": body.get("clicked_row_id")}
|
||||
ok = screens.apply_data_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": ok}))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask import jsonify, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/flow/nodes/<int:node_id>/run-variable", methods=["POST"])
|
||||
def flow_node_run_variable_public(slug, node_id):
|
||||
"""Miroir public de routes/flow/flow_node_run_variable.py (aperçu
|
||||
créateur) — voir routes/public_play/game_play_public.py pour le
|
||||
principe général."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_variable":
|
||||
return jsonify({"ok": False}), 400
|
||||
ok = screens.apply_variable_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": ok}))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
|
||||
from flask import render_template, url_for, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>")
|
||||
def game_play_public(slug):
|
||||
"""Route PUBLIQUE, non authentifiée (voir core/auth_guard.py) —
|
||||
distincte de /game/<slug>/play (aperçu créateur, connecté, INCHANGÉE) :
|
||||
un vrai visiteur anonyme, identifié par un cookie (voir
|
||||
core/player_identity.py), avec sa propre partie (variables/objets
|
||||
per_player, voir db/global_vars/, db/rows/). 404 si le créateur n'a
|
||||
pas explicitement basculé "Publier en ligne" dans son tableau de bord
|
||||
— jamais exposé par défaut."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
game = db.game_meta(slug)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
payload = screens.full_game_payload(slug, player_id)
|
||||
resp = app.make_response(
|
||||
render_template(
|
||||
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
||||
runtime_payload_url=url_for("runtime_payload_public", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data_public", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable_public", slug=slug, node_id=999999),
|
||||
)
|
||||
)
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,20 @@
|
||||
from flask import jsonify, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/runtime-payload")
|
||||
def runtime_payload_public(slug):
|
||||
"""Miroir public de routes/flow/runtime_payload.py (aperçu créateur) —
|
||||
voir routes/public_play/game_play_public.py pour le principe général."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
resp = app.make_response(jsonify(screens.full_game_payload(slug, player_id)))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
+15
-1
@@ -49,8 +49,22 @@
|
||||
jouable dans un nouvel onglet), pas une page de gestion de plus. -->
|
||||
<nav class="gameNavBar" aria-label="Navigation du jeu">
|
||||
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="Écrants, objets, templates, variables">📊 Tableau de bord</a>
|
||||
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu">▶️ Jouer</a>
|
||||
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu (une seule partie partagée, pour la conception)">▶️ Jouer</a>
|
||||
<button type="button" class="gameNavLink" onclick="window.forgeOpenPublishModal && window.forgeOpenPublishModal()" title="Exporte le jeu en exécutable Windows autonome (zip)">📦 Publier</button>
|
||||
<!-- "Publier en ligne" (état par joueur, Phase 1) : distinct de
|
||||
"Publier" ci-dessus (export .zip mono-joueur) — active/désactive la
|
||||
route publique /jouer/<slug>, jouable par n'importe quel visiteur
|
||||
anonyme avec SA PROPRE partie (voir routes/public_play/,
|
||||
db/games/is_public_played.py). -->
|
||||
<form method="post" action="{{ url_for('game_toggle_public_played', slug=game.slug) }}" style="display:inline;">
|
||||
<input type="hidden" name="value" value="{{ '0' if game.is_public_played else '1' }}">
|
||||
<button type="submit" class="gameNavLink" title="{{ 'Rendre le jeu privé (désactive /jouer)' if game.is_public_played else 'Rendre le jeu jouable par tous, chacun avec sa propre partie' }}">
|
||||
{{ '🌐 En ligne ✓' if game.is_public_played else '🌐 Publier en ligne' }}
|
||||
</button>
|
||||
</form>
|
||||
{% if game.is_public_played %}
|
||||
<a href="{{ url_for('game_play_public', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Ouvre la partie publique, comme un vrai visiteur">🔗 Lien public</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
+10
-4
@@ -79,13 +79,19 @@
|
||||
// le JS), qui ne peuvent pas contenir de code Jinja puisqu'ils ne
|
||||
// passent jamais par le moteur de templates. FORGE_PLAY_URLS porte donc
|
||||
// les URLs de routes dont ces fichiers ont besoin (actions.js,
|
||||
// bindings.js), construites ici une seule fois.
|
||||
// bindings.js), construites ici une seule fois — passées par la route
|
||||
// elle-même (runtime_payload_url/flow_node_run_data_url/
|
||||
// flow_node_run_variable_url) plutôt que construites ici avec des noms
|
||||
// de endpoint fixes : ce même template sert aussi bien l'aperçu
|
||||
// créateur (/game/<slug>/play, routes/play/game_play.py) que la partie
|
||||
// publique par joueur (/jouer/<slug>, routes/public_play/), deux
|
||||
// familles de routes distinctes (voir état par joueur, Phase 1).
|
||||
let gameData = {{ payload_json|safe }};
|
||||
let screensData = gameData.screens;
|
||||
var FORGE_PLAY_URLS = {
|
||||
runtimePayload: {{ url_for('runtime_payload', slug=game.slug)|tojson }},
|
||||
flowNodeRunData: {{ url_for('flow_node_run_data', slug=game.slug, node_id=999999)|tojson }},
|
||||
flowNodeRunVariable: {{ url_for('flow_node_run_variable', slug=game.slug, node_id=999999)|tojson }},
|
||||
runtimePayload: {{ runtime_payload_url|tojson }},
|
||||
flowNodeRunData: {{ flow_node_run_data_url|tojson }},
|
||||
flowNodeRunVariable: {{ flow_node_run_variable_url|tojson }},
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/play/screens.js') }}"></script>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Tests de la route publique /jouer/<slug> (routes/public_play/) — état
|
||||
par joueur, Phase 1. Contrairement aux autres tests, ceux-ci utilisent
|
||||
DIRECTEMENT flask_app.test_client() (pas la fixture `client`) pour de
|
||||
vrais visiteurs anonymes, jamais connectés — le but est justement de
|
||||
vérifier que core/auth_guard.py laisse passer ces routes sans session."""
|
||||
import json
|
||||
import re
|
||||
|
||||
import db
|
||||
|
||||
from conftest import flask_app
|
||||
|
||||
|
||||
def _create_screen(client, slug, name="Accueil"):
|
||||
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||
|
||||
|
||||
def _add_flow_node(client, slug, screen_id, **fields):
|
||||
resp = client.post(
|
||||
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
||||
data=json.dumps(fields),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
return resp.get_json()["id"]
|
||||
|
||||
|
||||
def test_public_route_404_until_explicitly_published(client, game):
|
||||
anon = flask_app.test_client()
|
||||
resp = anon.get(f"/jouer/{game}")
|
||||
assert resp.status_code == 404
|
||||
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
resp = anon.get(f"/jouer/{game}")
|
||||
assert resp.status_code == 200
|
||||
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "0"})
|
||||
resp = anon.get(f"/jouer/{game}")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_public_route_works_without_any_login_session(client, game):
|
||||
"""La garde globale (core/auth_guard.py) ne doit PAS exiger de session
|
||||
pour cette route — c'est tout le sens de "publique"."""
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
anon = flask_app.test_client()
|
||||
with anon.session_transaction() as sess:
|
||||
assert "user_id" not in sess
|
||||
resp = anon.get(f"/jouer/{game}")
|
||||
assert resp.status_code == 200
|
||||
assert b"CSRF_TOKEN" in resp.data or b"gameData" in resp.data
|
||||
|
||||
|
||||
def test_public_route_sets_player_id_cookie(client, game):
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
anon = flask_app.test_client()
|
||||
resp = anon.get(f"/jouer/{game}")
|
||||
assert "forge_player_id" in resp.headers.get("Set-Cookie", "")
|
||||
|
||||
|
||||
def test_two_anonymous_players_get_independent_variable_state(client, game):
|
||||
"""Bout en bout, via HTTP : deux visiteurs anonymes (deux clients de
|
||||
test SÉPARÉS, donc deux cookies forge_player_id différents) sur le
|
||||
même jeu publié -> chacun sa propre valeur pour une variable
|
||||
per_player, modifiée via l'action "Modifier une variable" (le vrai
|
||||
chemin d'exécution du mode jouable, routes/public_play/
|
||||
flow_node_run_variable_public.py)."""
|
||||
db.create_global_variable(game, "score", "nombre_entier", "0", per_player=True)
|
||||
screen_id = _create_screen(client, game)
|
||||
node_id = _add_flow_node(
|
||||
client, game, screen_id,
|
||||
node_type="action", action_type="modifier_variable",
|
||||
target_variable="score", data_operation="incrementer", data_value="10",
|
||||
)
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
|
||||
alice = flask_app.test_client()
|
||||
alice.get(f"/jouer/{game}") # pose le cookie
|
||||
resp = alice.post(f"/jouer/{game}/flow/nodes/{node_id}/run-variable")
|
||||
assert resp.get_json()["ok"] is True
|
||||
|
||||
bob = flask_app.test_client()
|
||||
bob.get(f"/jouer/{game}")
|
||||
resp = bob.post(f"/jouer/{game}/flow/nodes/{node_id}/run-variable")
|
||||
resp = bob.post(f"/jouer/{game}/flow/nodes/{node_id}/run-variable")
|
||||
assert resp.get_json()["ok"] is True
|
||||
|
||||
assert db.get_global_variable(game, "score", player_id="__shared__")["value"] == "0"
|
||||
|
||||
alice_payload = alice.get(f"/jouer/{game}/runtime-payload").get_json()
|
||||
bob_payload = bob.get(f"/jouer/{game}/runtime-payload").get_json()
|
||||
assert alice_payload["variables"]["score"]["value"] == "10"
|
||||
assert bob_payload["variables"]["score"]["value"] == "20"
|
||||
|
||||
|
||||
def test_creator_preview_unaffected_by_public_players(client, game):
|
||||
"""L'aperçu créateur (/game/<slug>/play, connecté) doit continuer de
|
||||
voir/modifier PLAYER_SHARED, jamais l'état d'un joueur public."""
|
||||
db.create_global_variable(game, "score", "nombre_entier", "0", per_player=True)
|
||||
screen_id = _create_screen(client, game)
|
||||
node_id = _add_flow_node(
|
||||
client, game, screen_id,
|
||||
node_type="action", action_type="modifier_variable",
|
||||
target_variable="score", data_operation="incrementer", data_value="10",
|
||||
)
|
||||
client.post(f"/game/{game}/toggle-public-played", data={"value": "1"})
|
||||
|
||||
alice = flask_app.test_client()
|
||||
alice.get(f"/jouer/{game}")
|
||||
alice.post(f"/jouer/{game}/flow/nodes/{node_id}/run-variable")
|
||||
|
||||
resp = client.get(f"/game/{game}/play")
|
||||
assert resp.status_code == 200
|
||||
creator_payload = client.get(f"/game/{game}/runtime-payload").get_json()
|
||||
assert creator_payload["variables"]["score"]["value"] == "0"
|
||||
Reference in New Issue
Block a user