import os import subprocess import threading from flask import Blueprint, render_template, jsonify, request, send_file from ..db import query, query_one, execute, get_all_sessions from ..config import SIM_CONFIGS_DIR bp = Blueprint("sim", __name__) _sim_procs = { "truth": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None}, "enriched": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None}, } _sim_lock = threading.Lock() def _run_sim_bg(mode, sid, tick_speed_s): env = {**os.environ, "SESSION_ID": str(sid), "TICK_SLEEP_SEC": str(tick_speed_s), "PYTHONUNBUFFERED": "1"} if mode == "enriched": env["USE_ENRICHED"] = "1" try: row = query_one( "INSERT INTO sim_runs (mode,session_id,tick_speed_s,status,started_at) " "VALUES (%s,%s,%s,'running',NOW()) RETURNING id", (mode, sid, tick_speed_s)) run_id = row["id"] if row else None except Exception: run_id = None with _sim_lock: _sim_procs[mode].update({"output": [], "running": True, "returncode": None, "error": None, "sid": sid}) try: proc = subprocess.Popen( ["python3", "-u", "/app/src/engine/run.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) with _sim_lock: _sim_procs[mode]["proc"] = proc for line in proc.stdout: with _sim_lock: _sim_procs[mode]["output"].append(line.rstrip()) proc.wait() with _sim_lock: _sim_procs[mode]["returncode"] = proc.returncode _sim_procs[mode]["running"] = False if run_id: try: execute("UPDATE sim_runs SET status='stopped',stopped_at=NOW() WHERE id=%s", (run_id,)) except Exception: pass except Exception as e: with _sim_lock: _sim_procs[mode]["error"] = str(e) _sim_procs[mode]["running"] = False def _get_sim_data(sid: int) -> dict: session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,)) if not session: session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") characters = query(""" SELECT c.*, COALESCE(c.tier, 1) as tier FROM characters c WHERE c.session_id = %s ORDER BY COALESCE(c.tier,1), c.is_alive DESC, c.name """, (sid,)) alive_count = sum(1 for c in characters if c["is_alive"]) events = query(""" SELECT e.*, c.name as actor_name FROM events e LEFT JOIN characters c ON c.id = e.actor_id WHERE e.session_id = %s AND e.event_type NOT IN ('encounter_lambda','encounter_group','encounter_actif','encounter_boss','cycle_tick') ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 50 """, (sid,)) encounters = query(""" SELECT e.*, c.name as actor_name FROM events e LEFT JOIN characters c ON c.id = e.actor_id WHERE e.session_id = %s AND e.event_type LIKE 'encounter%%' ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 30 """, (sid,)) world_state = query(""" SELECT DISTINCT ON (location_slug) * FROM world_state WHERE session_id = %s ORDER BY location_slug, day DESC """, (sid,)) faction_relations = query(""" SELECT * FROM faction_relations WHERE session_id = %s ORDER BY relation_score ASC """, (sid,)) tier_labels = {0: "BOSS", 1: "ACTIFS SIM", 2: "PNJ+", 3: "PASSAGE"} tier_raw = query(""" SELECT COALESCE(tier,1) as tier, COUNT(*) FILTER (WHERE is_alive) as alive, COUNT(*) FILTER (WHERE NOT is_alive) as dead FROM characters WHERE session_id = %s GROUP BY COALESCE(tier,1) ORDER BY COALESCE(tier,1) """, (sid,)) tier_stats = [{"tier": r["tier"], "label": tier_labels.get(r["tier"], "?"), "alive": r["alive"], "dead": r["dead"]} for r in tier_raw] zone_stats = query(""" SELECT location_slug, COUNT(*) as cnt FROM characters WHERE session_id = %s AND is_alive = TRUE GROUP BY location_slug ORDER BY cnt DESC LIMIT 5 """, (sid,)) ev_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,)) cb_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,)) dt_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='death'", (sid,)) enc_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type LIKE 'encounter%%'", (sid,)) stats = { "Événements total": ev_total["n"] if ev_total else 0, "Combats": cb_total["n"] if cb_total else 0, "Morts": dt_total["n"] if dt_total else 0, "Rencontres": enc_total["n"] if enc_total else 0, "PNJ vivants": alive_count, "PNJ morts": len(characters) - alive_count, } boss_inventory = query(""" SELECT c.name as char_name, i.item_name, i.item_type, i.quantity, i.is_equipped FROM inventory i JOIN characters c ON c.id = i.character_id WHERE c.session_id = %s AND COALESCE(c.tier,1) = 0 ORDER BY c.name, i.is_equipped DESC """, (sid,)) return dict( session=session, characters=characters, alive_count=alive_count, total_count=len(characters), events=events, encounters=encounters, world_state=world_state, faction_relations=faction_relations, tier_stats=tier_stats, zone_stats=zone_stats, stats=stats, boss_inventory=boss_inventory, ) @bp.route("/") def index(): all_sessions = get_all_sessions() sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1)) data = _get_sim_data(sid) return render_template("sim.html", tab="sim", current_sid=sid, all_sessions=all_sessions, flash_msg=None, flash_ok=True, **data) @bp.route("/api/sim//run", methods=["POST"]) def sim_run(mode): if mode not in ("truth", "enriched"): return jsonify({"error": "mode invalide"}), 400 with _sim_lock: if _sim_procs[mode].get("running"): return jsonify({"error": f"Simulation {mode} déjà en cours"}), 400 data = request.get_json(silent=True) or {} sid = int(data.get("sid", 1)) tick_speed_s = int(data.get("tick_speed_s", 5)) t = threading.Thread(target=_run_sim_bg, args=(mode, sid, tick_speed_s), daemon=True) t.start() return jsonify({"ok": True, "sid": sid, "mode": mode}) @bp.route("/api/sim//status") def sim_status(mode): if mode not in ("truth", "enriched"): return jsonify({"error": "mode invalide"}), 400 with _sim_lock: state = {k: v for k, v in _sim_procs[mode].items() if k != "proc"} return jsonify(state) @bp.route("/api/sim//stop", methods=["POST"]) def sim_stop(mode): if mode not in ("truth", "enriched"): return jsonify({"error": "mode invalide"}), 400 with _sim_lock: p = _sim_procs[mode].get("proc") if p: try: p.terminate() except Exception: pass return jsonify({"ok": True}) @bp.route("/api/sim//log") def sim_log(mode): if mode not in ("truth", "enriched"): return jsonify({"error": "mode invalide"}), 400 import io import json as _json import datetime as _dt run = query_one("SELECT * FROM sim_runs WHERE mode=%s ORDER BY id DESC LIMIT 1", (mode,)) if not run: return jsonify({"error": "Aucun run trouvé — lancez d'abord une simulation"}), 404 sid = run["session_id"] session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,)) events = query("SELECT * FROM events WHERE session_id=%s ORDER BY id DESC LIMIT 500", (sid,)) with _sim_lock: output_lines = list(_sim_procs[mode].get("output", [])) log_data = { "meta": { "sim_mode": mode, "session_id": sid, "session_name": session.get("name", "") if session else "", "world": "Venice of Wasteland, Louisiane post-apo", "tick_speed_s": run["tick_speed_s"], "started_at": run["started_at"].isoformat() if run.get("started_at") else None, "stopped_at": run["stopped_at"].isoformat() if run.get("stopped_at") else None, "status": run["status"], "exported_at": _dt.datetime.now().isoformat(), }, "console_output": output_lines, "recent_events": [dict(e) for e in events], } buf = io.BytesIO(_json.dumps(log_data, ensure_ascii=False, indent=2, default=str).encode("utf-8")) return send_file(buf, mimetype="application/json", as_attachment=True, download_name=f"sim_{mode}_{sid}.json") @bp.route("/api/session/set-mode", methods=["POST"]) def session_set_mode(): data = request.get_json(silent=True) or {} sid = int(data.get("sid", 1)) mode = data.get("mode", "").strip() valid = {"pacifiste", "politique", "guerre_commerciale", "guerre", "survie_extreme"} if mode not in valid: return jsonify({"error": f"Mode invalide: {mode}"}), 400 try: execute("UPDATE sessions SET mode=%s WHERE id=%s", (mode, sid)) return jsonify({"ok": True, "sid": sid, "mode": mode}) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/api/state") def api_state(): all_sessions = get_all_sessions() sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1)) session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,)) alive = query(""" SELECT id, name, hp, max_hp, tier, faction_slug, location_slug FROM characters WHERE session_id=%s AND is_alive=TRUE """, (sid,)) return jsonify({"session": session, "alive_pnj": alive})