"""player_app.py — serveur autonome généré par Forge Engine pour UN SEUL jeu (voir publish/build_package.py) : aucune installation externe requise, lancé par double-clic sur run.bat (Python portable + Flask embarqués à côté de ce fichier). Reprend telles quelles les routes utilisées par le mode jouable (voir templates/play.html) — jamais l'éditeur ni l'authentification, absents de cet export. SLUG est remplacé littéralement par build_package.py au moment de la publication (voir _SLUG_PLACEHOLDER) — ce fichier n'est jamais exécuté tel quel depuis le dépôt source.""" import json import os import socket import sys import threading import webbrowser _BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Le python311._pth de Python embeddable (voir publish/vendor_runtime.py) # ne met sur sys.path QUE le dossier de python.exe lui-même (python-embed/) # et "pylibs" — jamais le dossier de CE script, qui vit un cran au-dessus # (sibling de python-embed/). Sans cette ligne, "import core"/"db"/ # "screens" échoue avec ModuleNotFoundError bien qu'ils soient juste à # côté de ce fichier. sys.path.insert(0, _BASE_DIR) # Doit être posé AVANT tout import de db/ : db/constants.py lit # FORGE_PROJECTS_DIR à l'IMPORT, pas à l'appel — pointe ici vers le # dossier "projects/" embarqué à côté de ce fichier, jamais un chemin du # poste où le jeu a été publié. os.environ["FORGE_PROJECTS_DIR"] = os.path.join(_BASE_DIR, "projects") from flask import jsonify, redirect, render_template, request, send_from_directory, url_for # noqa: E402 from core.flask_app import app # noqa: E402 from core import jinja_filters # noqa: E402,F401 - enregistre les filtres Jinja (elstyle/eltransform/elabel/colname) import db # noqa: E402 import screens # noqa: E402 # Pas de CSRF/auth dans cet export (un seul jeu, aucune connexion) — # play.html appelle quand même csrf_token() en Jinja (voir base.html/ # play.html) : un no-op le laisse fonctionner tel quel, sans le forker. app.jinja_env.globals["csrf_token"] = lambda: "" SLUG = "_SLUG_PLACEHOLDER" @app.route("/") def index(): return redirect(url_for("game_play", slug=SLUG)) @app.route("/game//play") def game_play(slug): 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), ) @app.route("/game//runtime-payload") def runtime_payload(slug): return jsonify(screens.full_game_payload(SLUG)) @app.route("/game//flow/nodes//run-data", methods=["POST"]) def flow_node_run_data(slug, node_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) return jsonify({"ok": ok}) @app.route("/game//flow/nodes//run-variable", methods=["POST"]) def flow_node_run_variable(slug, node_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) return jsonify({"ok": ok}) @app.route("/game//uploads/") def uploaded_file(slug, filename): return send_from_directory(os.path.join(db.game_dir(SLUG), "uploads"), filename) def _open_browser(port): webbrowser.open(f"http://127.0.0.1:{port}/") if __name__ == "__main__": # Port libre choisi au hasard par l'OS (jamais le même serveur à # relancer sur un port déjà pris par une partie précédente encore # ouverte) — le navigateur est ouvert automatiquement, l'utilisateur # n'a jamais besoin de connaître ni taper le port. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) port = s.getsockname()[1] threading.Timer(1.0, _open_browser, args=(port,)).start() app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False)