harness.pyΒΆ
Part of SNKRX.
1#!/usr/bin/env python3
2"""Scripted headless harness: drive the SNKRX port through a full run and
3capture one screenshot per stage into ``screenshots/``.
4
5The driver starts the game, forces a slow-motion hit, then clears each arena
6and confirms the shop so the run reaches the wave 5 boss inside 1400 frames.
7
8Stages
9------
101. Title screen (frame 30)
112. Wave 1 mid-combat (frame 240)
123. Slow-motion with screenshake (frame 360)
134. Shop after wave 1 (frame 540)
145. Shop after wave 2 (frame 720)
156. Shop after wave 3 (frame 900)
167. Wave 4 in progress (frame 1080)
178. Wave 5 boss round (frames 1260 and 1380)
18
19Run::
20
21 uv run python examples/ports/snkrx/harness.py
22"""
23
24from __future__ import annotations
25
26import sys
27from pathlib import Path
28
29_PORT_DIR = Path(__file__).resolve().parent
30if str(_PORT_DIR) not in sys.path:
31 sys.path.insert(0, str(_PORT_DIR))
32
33from main import WINDOW_H, WINDOW_W, SNKRXRoot # noqa: E402
34from nodes.colours import BG # noqa: E402
35
36from simvx.core import InputSimulator, Key # noqa: E402
37from simvx.graphics import App, save_png # noqa: E402
38
39
40def _save(captures, frames, root_dir):
41 root_dir.mkdir(exist_ok=True)
42 for idx, frame in zip(frames, captures, strict=False):
43 path = root_dir / f"stage_{idx:02d}.png"
44 save_png(frame, path)
45 print(f"saved {path}")
46
47
48def _force_clear_arena(root):
49 """Clear the active arena's enemies + queue so it ends next frame."""
50 if root.phase == "arena" and root._sub is not None:
51 arena = root._sub
52 arena.spawn_queue.clear()
53 for e in list(arena.enemies):
54 e.alive = False
55 e.hp = 0
56
57
58def _drive(root: SNKRXRoot, sim: InputSimulator, frame: int):
59 """Per-frame input driver: moves the harness through the staged scenes."""
60 if frame == 60:
61 sim.press_key(Key.ENTER)
62 if frame == 62:
63 sim.release_key(Key.ENTER)
64 if frame == 360:
65 # Force slow-mo
66 if root.phase == "arena" and root._sub is not None:
67 root._sub.start_slowmo()
68 if frame == 480:
69 _force_clear_arena(root)
70 # Skip the buy screens automatically to advance to wave 5 boss
71 if frame in (600, 800, 1000, 1200):
72 if root.phase == "buy":
73 sim.press_key(Key.ENTER)
74 if frame in (602, 802, 1002, 1202):
75 sim.release_key(Key.ENTER)
76 if frame in (700, 900, 1100):
77 _force_clear_arena(root)
78
79
80def main():
81 app = App(title="SNKRX Harness", width=WINDOW_W, height=WINDOW_H, visible=False, bg_colour=BG)
82 root = SNKRXRoot()
83 sim = InputSimulator()
84
85 captures_target = [30, 240, 360, 540, 720, 900, 1080, 1260, 1380]
86
87 def _per_frame(idx: int, _t: float):
88 _drive(root, sim, idx)
89 if idx in captures_target:
90 print(f"frame {idx}: phase={root.phase}")
91 return None # continue
92
93 raw = app.run_headless(
94 root,
95 frames=1400,
96 on_frame=_per_frame,
97 capture_frames=captures_target,
98 )
99 _save(raw, captures_target, _PORT_DIR / "screenshots")
100
101
102if __name__ == "__main__":
103 main()