From 96d2df92b263634ec03994e936d5f0ee21eb8d57 Mon Sep 17 00:00:00 2001 From: Corback Date: Mon, 15 Jun 2026 23:54:40 +0000 Subject: [PATCH] feat: dashboard VAULTCOM v3 + config.py TICK_SLEEP_SEC + sim_config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dashboard/app.py: Nouveau dashboard Flask (schéma DB migration_003) Affiche PNJ par tier (0/1/2/3), events, rencontres, world_state, faction_relations, inventaire boss. Auto-refresh 30s. Connecté via tunnel SSH interne vers Vigile:5432 - src/engine/config.py: Fix TICK_SLEEP_SEC depuis env (était hardcodé 0) - src/config/sim_001.json: Paramètres simulation centralisés (tick_speed, difficulty presets, LLM config, multi-session) Accessible: fallout.coyoteos.ovh --- dashboard/app.py | 431 ++++++++++++++++++++++++++++++++++++++++ src/config/sim_001.json | 45 +++++ src/engine/config.py | 2 +- 3 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 dashboard/app.py create mode 100644 src/config/sim_001.json diff --git a/dashboard/app.py b/dashboard/app.py new file mode 100644 index 0000000..59f343a --- /dev/null +++ b/dashboard/app.py @@ -0,0 +1,431 @@ +""" +VAULTCOM — Fallout: Venice of Wasteland +Dashboard simulation. Schéma DB actuel (migration_003). +""" + +import os, json +import psycopg2 +import psycopg2.extras +from flask import Flask, render_template_string, jsonify + +app = Flask(__name__) + +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = int(os.getenv("DB_PORT", 5432)) +DB_NAME = os.getenv("DB_NAME", "fallout") +DB_USER = os.getenv("DB_USER", "fallout") +DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!") + +def get_conn(): + return psycopg2.connect( + host=DB_HOST, port=DB_PORT, dbname=DB_NAME, + user=DB_USER, password=DB_PASS + ) + +def query(sql, params=None): + conn = get_conn() + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(sql, params) + return [dict(r) for r in cur.fetchall()] + finally: + conn.close() + +def query_one(sql, params=None): + rows = query(sql, params) + return rows[0] if rows else None + +# --------------------------------------------------------------------------- +# HTML template +# --------------------------------------------------------------------------- + +TEMPLATE = r""" + + + + + +VAULTCOM — Fallout: Venice of Wasteland + + + +

⚡ VAULTCOM — VENICE OF WASTELAND ⚡

+ +{% if session %} +
+ SESSION #{{ session.id }} — {{ session.name }} + JOUR {{ session.current_day }} / TICK {{ session.current_tick }} + MODE: {{ session.mode | upper }} | STATUS: {{ session.status | upper }} + SEED: {{ session.seed_global }} + 🔄 Actualisé toutes les 30s +
+{% endif %} + +
+ + +
+

👥 PNJ ACTIFS ({{ alive_count }}/{{ total_count }})

+
+ + + {% for c in characters %} + + + + + + + + + {% endfor %} +
TIERNOMPVFACTIONZONECAPS
+ {% if c.tier == 0 %}★BOSS + {% elif c.tier == 2 %}◈ T2 + {% elif c.tier == 3 %}· T3 + {% else %}● T1{% endif %} + {{ c.name }} + {% if c.is_alive %} + {% set pct = (c.hp / c.max_hp * 100) | int if c.max_hp else 0 %} +
+ {{ c.hp }}/{{ c.max_hp }} + {% else %}†{% endif %} +
+ {% set f = c.faction_slug or 'sans' %} + + {{ f[:12] }} + + {{ (c.location_slug or '—')[:14] }}{{ c.caps }}¢
+
+
+ + +
+

📡 ÉVÉNEMENTS RÉCENTS (J{{ session.current_day if session else '?' }})

+
+ + + {% for ev in events %} + + + + + + {% endfor %} + {% if not events %}{% endif %} +
J/TTYPEDESCRIPTION
{{ ev.day }}/{{ ev.tick }}{{ ev.event_type }}{{ ev.description[:80] }}{% if ev.description|length > 80 %}…{% endif %}
Aucun événement
+
+
+ +
+ +
+ + +
+

🗺 ÉTAT DU MONDE

+
+ {% for ws in world_state %} +
+ {{ ws.location_slug }} + + 🍖
+ 💧
+ 🛡
+ 👤{{ ws.pop_count or 0 }} +
+
+ {% endfor %} + {% if not world_state %}

Pas de données monde

{% endif %} +
+
+ + +
+

⚔ RELATIONS FACTIONS

+
+ + + {% for r in faction_relations %} + + + + + + + {% endfor %} + {% if not faction_relations %}{% endif %} +
FACTION AFACTION BSCOREÉTAT
{{ r.faction_a[:12] }}{{ r.faction_b[:12] }} + {{ r.relation_score }} + + {{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }} +
Pas de données
+
+
+ + +
+

📊 STATS SIMULATION

+ + + {% for k, v in stats.items() %} + + {% endfor %} +
MÉTRIQUEVALEUR
{{ k }}{{ v }}
+ +
+

🎭 PNJ PAR TIER

+ + + {% for row in tier_stats %} + + + + + + + {% endfor %} +
TIERLABELVIVANTSMORTS
{{ row.tier }}{{ row.label }}{{ row.alive }}{{ row.dead }}
+ +
+

🏙 PNJ PAR ZONE (TOP 5)

+ + + {% for row in zone_stats %} + + {% endfor %} +
ZONEPNJ
{{ row.location_slug }}{{ row.cnt }}
+
+ +
+ + +
+

⚡ RENCONTRES RÉCENTES

+
+ + + {% for ev in encounters %} + + + + + + + + {% endfor %} + {% if not encounters %}{% endif %} +
J/TTYPEACTEURZONEDESCRIPTION
{{ ev.day }}/{{ ev.tick }}{{ ev.event_type }}{{ ev.actor_name or '—' }}{{ (ev.location_slug or '—')[:14] }}{{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}
Aucune rencontre
+
+
+ + +{% if boss_inventory %} +
+

★ ÉQUIPEMENT CHEFS DE FACTION

+ + + {% for row in boss_inventory %} + + + + + + + + {% endfor %} +
BOSSITEMTYPEQTÉÉQUIPÉ
{{ row.char_name }}{{ row.item_name }}{{ row.item_type }}{{ row.quantity }}{{ '✓' if row.is_equipped else '' }}
+
+{% endif %} + + + +""" + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.route("/") +def index(): + session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") + sid = session["id"] if session else 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"]) + total_count = len(characters) + + 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') + 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,)) + + events_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,)) + combat_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,)) + deaths_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": events_total["n"] if events_total else 0, + "Combats": combat_total["n"] if combat_total else 0, + "Morts": deaths_total["n"] if deaths_total else 0, + "Rencontres": enc_total["n"] if enc_total else 0, + "PNJ vivants": alive_count, + "PNJ morts": total_count - 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 render_template_string(TEMPLATE, + session=session, + characters=characters, + alive_count=alive_count, + total_count=total_count, + 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, + ) + +@app.route("/api/state") +def api_state(): + session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") + sid = session["id"] if session else 1 + 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}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False) diff --git a/src/config/sim_001.json b/src/config/sim_001.json new file mode 100644 index 0000000..b25ab10 --- /dev/null +++ b/src/config/sim_001.json @@ -0,0 +1,45 @@ +{ + "_comment": "Configuration de la simulation session 1 — Venice of Wasteland", + "session": { + "session_id": 1, + "name": "Session 001 — Louisiane post-apo", + "seed_global": 42, + "mode": "sandbox" + }, + "tick": { + "tick_sleep_sec": 60, + "ticks_per_day": 24, + "day_start_tick": 6, + "night_start_tick": 20, + "llm_tick_interval": 6 + }, + "difficulty": { + "preset": "normal", + "survival_drain_food": 1, + "survival_drain_water": 1, + "survival_drain_sleep": 1, + "encounter_rate_multiplier": 1.0, + "combat_lethality": 1.0, + "resource_scarcity": 1.0 + }, + "difficulty_presets": { + "easy": { "survival_drain_food": 0, "survival_drain_water": 0, "survival_drain_sleep": 0, "encounter_rate_multiplier": 0.5, "combat_lethality": 0.7 }, + "normal": { "survival_drain_food": 1, "survival_drain_water": 1, "survival_drain_sleep": 1, "encounter_rate_multiplier": 1.0, "combat_lethality": 1.0 }, + "hard": { "survival_drain_food": 2, "survival_drain_water": 2, "survival_drain_sleep": 1, "encounter_rate_multiplier": 1.5, "combat_lethality": 1.3 }, + "nightmare": { "survival_drain_food": 3, "survival_drain_water": 3, "survival_drain_sleep": 2, "encounter_rate_multiplier": 2.0, "combat_lethality": 1.8 } + }, + "llm": { + "enabled": false, + "model_mj": "qwen2.5:14b", + "model_pnj": "qwen2.5:7b", + "ollama_url": "http://localhost:11434", + "chroma_url": "http://localhost:8000", + "chroma_collection": "fallout_vst" + }, + "multi_session": { + "allow_parallel": true, + "max_sessions": 4, + "session_db_host": "localhost", + "session_db_port": 15432 + } +} diff --git a/src/engine/config.py b/src/engine/config.py index 9eeaec0..0f4b374 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -11,7 +11,7 @@ DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!") TICKS_PER_DAY = 24 # 1 tick = 1 heure in-game DAY_START_TICK = 6 # 06h00 NIGHT_START_TICK = 20 # 20h00 -TICK_SLEEP_SEC = 0 # 0 = bot-speed (pré-sim), sinon secondes entre ticks +TICK_SLEEP_SEC = float(os.getenv("TICK_SLEEP_SEC", 0)) # 0=bot-speed, 60=1tick/min # --- Session --- SESSION_ID = int(os.getenv("SESSION_ID", 1))