mumbai-local / backend /tests.py
geekwrestler's picture
Deploy Mumbai Local Panic — Nemotron(ZeroGPU) dispatcher + VoxCPM2 voice
d4a1b20 verified
Raw
History Blame Contribute Delete
7.03 kB
"""
tests.py — canon-rule regression tests for the 4-track engine. Pure, no model needed.
Run: python3 tests.py (exits non-zero if anything fails)
Covers: map shape, determinism, every loss/win condition, the chaos schedule + caps, no-stacking,
peak-signal-all, crossover-costs-the-turn, and headway collision avoidance.
"""
from __future__ import annotations
import sys
import engine
import rules
import simulation
from engine import Incident
from doctrine import doctrine
from chaos_players import SmartChaos
from agents import enforce_budget
_passed = _failed = 0
def check(name, cond):
global _passed, _failed
if cond:
_passed += 1; print(f" PASS {name}")
else:
_failed += 1; print(f" FAIL {name}")
def run_oracle_game(seed):
g = engine.new_game(seed); chaos = SmartChaos(); chaos.reset(seed)
while not g.over:
plays, _ = chaos.play(g, rules.legal_cards(g))
for c, loc in plays:
if engine.CARDS[c] <= g.energy and rules.card_available(g, c) and rules.station_free(g, loc):
g.energy -= engine.CARDS[c]; rules.apply_chaos(g, c, loc)
A = rules.legal_actions(g); chosen, _ = doctrine(g, A)
ann, pol = simulation.apply(g, A, chosen); simulation.advance(g, ann, pol)
return g
print("MAP")
check("8 stations", len(engine.STATIONS) == 8)
check("Nalasopara dropped", "Nalasopara" not in engine.STATIONS)
check("majors are 4", engine.MAJORS == {"Dadar", "Andheri", "Borivali", "Churchgate"})
check("interchanges from balance", engine.INTERCHANGES == {"Virar", "Churchgate", "Dadar", "Borivali"})
check("20 rounds / peak 16", engine.B["turns_to_win"] == 20 and engine.B["peak_rush_turn"] == 16)
check("crush threshold 170", engine.B["single_overflow_pct"] == 170)
print("DETERMINISM")
g1, g2 = run_oracle_game(3), run_oracle_game(3)
check("same seed => same outcome", (g1.won, g1.turn, g1.reason, round(g1.score)) == (g2.won, g2.turn, g2.reason, round(g2.score)))
print("WIN / LOSS CONDITIONS")
# win: oracle survives to 20 on a calm seed
gw = run_oracle_game(0)
check("oracle can win (survive 20)", gw.won and gw.turn == 20)
# single-station platform crush at >170
gc = engine.new_game(0); gc.stations[engine.IDX["Virar"]].crowd = gc.stations[engine.IDX["Virar"]].cap * 1.8
simulation.advance(gc, False, set())
check("platform crush fires >170%", gc.over and "platform crush" in gc.reason)
# ...but not at 145%
gc2 = engine.new_game(0); gc2.stations[engine.IDX["Virar"]].crowd = gc2.stations[engine.IDX["Virar"]].cap * 1.45
simulation.advance(gc2, False, set())
check("no crush at 145%", not (gc2.over and "platform crush" in (gc2.reason or "")))
# double overflow: two majors >130 (set to 160% so they stay >130% even after a train discharges
# ~360 and inflow lands — a train starts at each of Dadar/Andheri)
gd = engine.new_game(0)
for m in ("Dadar", "Andheri"):
gd.stations[engine.IDX[m]].crowd = gd.stations[engine.IDX[m]].cap * 1.6
simulation.advance(gd, False, set())
check("double overflow fires", gd.over and gd.reason == "double overflow")
# network lock: 4 trains stuck 3 rounds
gl = engine.new_game(0)
for t in gl.trains[:4]:
t.held = True; t.stuck_turns = 2
simulation.advance(gl, False, set())
check("network lock fires", gl.over and gl.reason == "network lock")
# safety collapse
gs = engine.new_game(0); gs.safety = 21
gs.incidents.append(Incident("i1", "signal_failure", "Dadar", "slow_up", severity=3, duration=4))
simulation.advance(gs, False, set())
check("safety collapse fires", gs.over and gs.reason == "safety collapse")
# anger collapse
ga = engine.new_game(0); ga.anger = 99
for s in ga.stations:
s.crowd = s.cap * 0.9
for k in range(3):
ga.incidents.append(Incident(f"f{k}", "festival_crowd", "Dadar", "-", severity=2, duration=3))
simulation.advance(ga, False, set())
check("anger collapse fires", ga.over and ga.reason == "city anger collapse")
print("CHAOS SCHEDULE / CAPS")
def avail(card, round_no, plays=0, consec=0):
g = engine.new_game(0); g.turn = round_no - 1
g.card_plays[card] = plays; g.consec[card] = consec
return rules.card_available(g, card)
check("flood locked before R8", not avail("monsoon_flood", 7))
check("flood unlocks R8", avail("monsoon_flood", 8))
check("flood max 3", not avail("monsoon_flood", 10, plays=3))
check("festival locked before R13", not avail("festival_crowd", 12))
check("festival unlocks R13", avail("festival_crowd", 13))
check("festival once", not avail("festival_crowd", 16, plays=1))
check("vip once", not avail("vip_special", 5, plays=1))
check("vip available from R1", avail("vip_special", 1))
check("cow/signal cap at 2 consecutive", (not avail("track_cow", 5, consec=2)) and avail("track_cow", 5, consec=1))
print("NO STACKING / PEAK SIGNAL / CROSSOVER / HEADWAY")
gst = engine.new_game(0); rules.apply_chaos(gst, "track_cow", "Dadar")
check("station_free false after a card", (not rules.station_free(gst, "Dadar")) and rules.station_free(gst, "Andheri"))
gp = engine.new_game(0); gp.phase = "peak"; rules.apply_chaos(gp, "signal_failure", "Dadar")
inc = gp.incidents[-1]
t_slow = engine.Train("x", "x", "slow", +1, engine.IDX["Dadar"], "slow")
t_fast = engine.Train("y", "y", "fast", -1, engine.IDX["Dadar"], "fast")
check("peak signal blocks all lanes", inc.track == "all" and rules.incident_blocks(inc, t_slow) and rules.incident_blocks(inc, t_fast))
gx = engine.new_game(0); tr = gx.trains[0]; tr.pos = engine.IDX["Dadar"]; tr.just_switched = True
before = tr.pos; simulation.advance(gx, False, set())
check("crossover consumes the round (no advance)", tr.pos == before)
gh = engine.new_game(0)
# two trains on slow_down: a held leader at Dadar, a follower one station behind (Mumbai Central)
lead = gh.trains[0]; lead.track = "slow"; lead.direction = +1; lead.pos = engine.IDX["Dadar"]; lead.held = True
foll = gh.trains[2]; foll.track = "slow"; foll.direction = +1; foll.pos = engine.IDX["Mumbai Central"]; foll.held = False
# park everyone else off that lane so they don't interfere
for t in gh.trains:
if t not in (lead, foll):
t.track = "fast"
c0 = gh.collisions_avoided; simulation.advance(gh, False, set())
check("headway collision avoided (follower holds)", gh.collisions_avoided > c0 and foll.pos == engine.IDX["Mumbai Central"])
print("LEGAL ACTIONS")
gA = engine.new_game(0)
gA.trains[0].pos = engine.IDX["Bandra"] # not an interchange
gA.trains[1].pos = engine.IDX["Dadar"] # interchange
A = rules.legal_actions(gA)
sw_bandra = any(a["type"].startswith("switch_to") and a.get("train") == gA.trains[0].id for a in A)
sw_dadar = any(a["type"].startswith("switch_to") and a.get("train") == gA.trains[1].id for a in A)
check("no switch at non-interchange", not sw_bandra)
check("switch offered at interchange", sw_dadar)
budget_ok = enforce_budget([a["action_id"] for a in A], {a["action_id"]: a for a in A})[0]
check("budget enforcement caps at 5", len(budget_ok) <= engine.B["budget_total"])
print(f"\n{_passed} passed, {_failed} failed")
sys.exit(1 if _failed else 0)