Files

262 lines
9.9 KiB
Python

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/<mode>/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/<mode>/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/<mode>/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/<mode>/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})