sim: Pitchwell match foundation (open/seed/T1/HQ) + smoke
This commit is contained in:
parent
7358d1ce1a
commit
bd5ade8ec4
7 changed files with 708 additions and 41 deletions
103
scripts/smoke/match_smoke.py
Executable file
103
scripts/smoke/match_smoke.py
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Match open → seed → seep → T1 → HQ kill — startable foundation (GAMEPLAY.md v0)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from game.sim.catalog import FACTION_HOST, FACTION_STAFF, HZ, START_PITCH
|
||||
from game.sim.match import MatchPhase, open_match, seed_v0_path, train_t1_melee
|
||||
|
||||
|
||||
def main() -> int:
|
||||
m = open_match()
|
||||
staff = m.side(FACTION_STAFF)
|
||||
host = m.side(FACTION_HOST)
|
||||
|
||||
if staff.pitch != START_PITCH or host.pitch != START_PITCH:
|
||||
print(f"FAIL: start pitch {staff.pitch}/{host.pitch}")
|
||||
return 1
|
||||
if len(staff.units) != 4 or len(host.units) != 4:
|
||||
print("FAIL: want 4 workers per side")
|
||||
return 1
|
||||
if not staff.hq_alive or not host.hq_alive:
|
||||
print("FAIL: both HQs must be alive at open")
|
||||
return 1
|
||||
|
||||
notes = seed_v0_path(m, FACTION_STAFF)
|
||||
if any(not n.endswith(":ok") for n in notes):
|
||||
print(f"FAIL: seed {notes}")
|
||||
return 1
|
||||
|
||||
# Serial queue: Font 15 + Cot 12 + Yard 20 = 47s; start 500 - 380 = 120 left
|
||||
m.tick_hz(int(47 * HZ) + 2)
|
||||
if not staff.has_building("pitch_font"):
|
||||
print("FAIL: pitch_font missing after seed")
|
||||
return 1
|
||||
if 0 not in staff.claimed_wells or not m.wells[0].claimed:
|
||||
print("FAIL: well not claimed by Staff Font")
|
||||
return 1
|
||||
if not staff.has_producer():
|
||||
print("FAIL: coven_yard missing")
|
||||
return 1
|
||||
if staff.pop_cap != 18:
|
||||
print(f"FAIL: pop_cap want 18 (start 10 + cot 8) got {staff.pop_cap}")
|
||||
return 1
|
||||
|
||||
pitch_before_seep = staff.pitch
|
||||
m.tick_hz(int(10 * HZ)) # Staff harvest −15% on seep
|
||||
if staff.pitch <= pitch_before_seep:
|
||||
print(f"FAIL: seep did not raise pitch ({pitch_before_seep} -> {staff.pitch})")
|
||||
return 1
|
||||
|
||||
# Afford one Acolyte from leftover start pitch + seep
|
||||
one = train_t1_melee(m, FACTION_STAFF, n=1)
|
||||
if one != ["acolyte:ok"]:
|
||||
print(f"FAIL: train one {one} pitch={staff.pitch}")
|
||||
return 1
|
||||
m.tick_hz(int(8 * HZ) + 2)
|
||||
if not any(u.spec_id == "acolyte" and u.alive for u in staff.units):
|
||||
print("FAIL: acolyte not trained")
|
||||
return 1
|
||||
|
||||
# Offline cheat for army mass (GAMEPLAY.md cheats offline-only)
|
||||
m.grant_pitch(FACTION_STAFF, 500.0)
|
||||
train = train_t1_melee(m, FACTION_STAFF, n=4)
|
||||
if any(not t.endswith(":ok") for t in train):
|
||||
print(f"FAIL: train army {train}")
|
||||
return 1
|
||||
m.tick_hz(int(4 * 8 * HZ) + 2)
|
||||
acolytes = [u for u in staff.units if u.spec_id == "acolyte" and u.alive]
|
||||
if len(acolytes) < 5:
|
||||
print(f"FAIL: want >=5 acolytes got {len(acolytes)}")
|
||||
return 1
|
||||
|
||||
m.order_attack_hq(FACTION_STAFF)
|
||||
# 5 * 4 dps = 20 HP/s → 5000 HP ≈ 250s
|
||||
for _ in range(HZ * 300):
|
||||
if m.phase == MatchPhase.ENDED:
|
||||
break
|
||||
m.tick_hz(1)
|
||||
|
||||
if m.phase != MatchPhase.ENDED or m.winner != FACTION_STAFF:
|
||||
print(f"FAIL: expected staff win got phase={m.phase} winner={m.winner}")
|
||||
print(m.snapshot())
|
||||
return 1
|
||||
if host.hq_alive:
|
||||
print("FAIL: host HQ still alive")
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"PASS: open+seed+seep+T1 HQ kill t={m.sim_t:.1f}s "
|
||||
f"staff_pitch={staff.pitch:.1f} ticks={m.tick_i}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue