mumbai-local / backend /render.py
geekwrestler's picture
Deploy Mumbai Local Panic — Nemotron(ZeroGPU) dispatcher + VoxCPM2 voice
d4a1b20 verified
Raw
History Blame Contribute Delete
5.34 kB
"""
render.py — turn engine state into the terse prompt strings the templates expect. Pure view
layer: reads state + the turn's legal actions, returns strings. No model calls, no rules
decisions. Keeps the LLM contract in one place so prompt tweaks don't leak into the engine.
"""
from __future__ import annotations
import os
from engine import STATIONS, MAJORS, INTERCHANGES, CARDS, B
from rules import legal_cards, card_available
PROMPT_DIR = os.path.join(os.path.dirname(__file__), "prompts")
def load_prompts(prompt_dir=PROMPT_DIR):
# MLP_DISPATCHER_SYSTEM lets a run swap in a variant system prompt without editing the file.
disp_sys = os.environ.get("MLP_DISPATCHER_SYSTEM", "dispatcher_system.txt")
def rd(name):
path = name if os.path.isabs(name) else os.path.join(prompt_dir, name)
with open(path, encoding="utf-8") as f:
return f.read()
return {
"dispatcher_system": rd(disp_sys),
"dispatcher_user": rd("dispatcher_user_template.txt"),
"chaos_system": rd("chaos_system.txt"),
"chaos_user": rd("chaos_user_template.txt"),
}
def _dir(d):
return "DOWN" if d == +1 else "UP" # +1 increases index toward Virar = DOWN
def _fill(template, mapping):
out = template
for k, v in mapping.items():
out = out.replace("{{" + k + "}}", str(v))
return "\n".join(l for l in out.splitlines() if not l.lstrip().startswith("#"))
# ---------------------------------------------------------------- action description
def describe_action(a):
t = a["type"]
if t.startswith("deploy_"):
return f"{t} {a['unit']} -> {a['to']} ({a['target']}, resolves in {a['resolves_in']}t)"
if t == "move_train":
return f"move_train {a['train']} -> {a['to']}"
if t == "hold_train":
return f"hold_train {a['train']} @{a['at']}"
if t in ("switch_to_fast", "switch_to_slow"):
return f"{t} {a['train']} @{a['at']}"
if t == "prioritize_train":
return f"prioritize_train {a['train']}"
if t == "clear_platform_priority":
return f"clear_platform_priority @{a['at']}"
if t == "issue_announcement":
return f"issue_announcement {a.get('scope', 'line')}-wide"
extra = " ".join(f"{k}={v}" for k, v in a.items() if k not in ("action_id", "type"))
return f"{t} {extra}".rstrip()
# ---------------------------------------------------------------- blocks
def _stations_block(g):
out = []
for s in g.stations:
pct = s.pct()
flag = "!!" if pct > 100 else "!" if pct > 85 else ""
tag = " [MAJOR]" if s.name in MAJORS else ""
out.append(f"- {s.name}{tag} {int(s.crowd)}/{s.cap} ({pct:.0f}%{flag})")
return "\n".join(out)
def _incidents_block(g):
if not g.incidents:
return "- none"
return "\n".join(f"- [{i.itype}] {i.location} {i.track} sev{i.severity} age{i.age}t"
+ (" (unit en route)" if i.assigned else "") for i in g.incidents)
def _trains_block(g):
return "\n".join(f'- {t.id} "{t.name}" [{t.ttype}] @{STATIONS[t.pos]} {_dir(t.direction)} {t.track} '
f'delay+{t.delay} ' + ("held" if t.held else (f"stuck{t.stuck_turns}t" if t.stuck_turns else "running"))
for t in g.trains)
def _personnel_block(g):
return "\n".join(f"- {u.id} {u.utype} @{STATIONS[u.loc]} {'busy' if u.busy else 'available'} "
f"used {u.used}/{B['personnel_max']}" for u in g.units)
def _legal_actions_block(A):
return "\n".join(f"{a['action_id']} {describe_action(a)}" for a in A)
def _legal_cards_block(g):
cards = legal_cards(g)
if not cards:
return "- (no playable cards — hoard energy)"
return "\n".join(f"{c['card']} cost {c['cost']}" for c in cards)
def _peak_banner(g):
if g.phase != "peak":
return ""
return (">>> PEAK RUSH (the escalator): inflow +40%, anger growth +30%, a SIGNAL FAILURE now "
"blocks ALL tracks, TRAIN TROUBLE card unlocks (+2 trains), monsoon flood drops to 8, "
"rain won't quit <<<")
# ---------------------------------------------------------------- public renderers
def render_dispatcher_user(template, g, A):
return _fill(template, {
"turn": g.turn + 1, "total_turns": B["turns_to_win"], "phase": g.phase,
"peak_rush_banner": _peak_banner(g),
"score": f"{g.score:.0f}", "delay": f"{g.delay:.0f}", "safety": f"{g.safety:.0f}",
"anger": f"{g.anger:.0f}", "pressure": f"{g.pressure:.0f}", "patience": f"{g.patience:.0f}",
"incidents_block": _incidents_block(g), "stations_block": _stations_block(g),
"trains_block": _trains_block(g), "personnel_block": _personnel_block(g),
"legal_actions_block": _legal_actions_block(A),
})
def render_chaos_user(template, g):
return _fill(template, {
"turn": g.turn + 1, "total_turns": B["turns_to_win"], "phase": g.phase,
"peak_rush_banner": _peak_banner(g),
"safety": f"{g.safety:.0f}", "anger": f"{g.anger:.0f}", "pressure": f"{g.pressure:.0f}",
"delay": f"{g.delay:.0f}", "stations_block": _stations_block(g),
"incidents_block": _incidents_block(g), "energy": g.energy,
"regen": B["chaos_energy_regen"], "legal_cards_block": _legal_cards_block(g),
})