harness.pyΒΆ
Part of Casual Crusade.
1"""Scripted-input harness: exercises menu, hover, drag-drop, scoring, save/load.
2
3Captures frames at key moments:
4 01_menu.png splash + PLAY button
5 02_idle.png autostart, hand dealt, board cross visible
6 03_hover.png hovering a hand card -> hover lift
7 04_drag.png mid-drag with legal-tile highlight
8 05_placed.png after drop -> card placed, pilgrim walking
9 06_picker_pick.png after taking a reward card from the chest picker
10 07_second_place.png after a second card is dropped left of the starter
11 08_save_load.png after S then L cycle (HUD shows SAVED/LOADED)
12
13Run:
14 uv run python examples/ports/casual_crusade/harness.py
15"""
16
17from __future__ import annotations
18
19import sys
20from pathlib import Path
21
22_PORT_DIR = Path(__file__).parent
23if str(_PORT_DIR) not in sys.path:
24 sys.path.insert(0, str(_PORT_DIR))
25
26import random # noqa: E402
27
28from main import HEIGHT, WIDTH, CasualCrusadeRoot # noqa: E402
29
30from simvx.core.input.enums import Key, MouseButton # noqa: E402
31from simvx.core.input.state import Input # noqa: E402
32from simvx.graphics import App, save_png # noqa: E402
33
34OUT = _PORT_DIR / "screenshots"
35
36
37def capture_sequence() -> None:
38 OUT.mkdir(exist_ok=True)
39 random.seed(7) # deterministic-ish deck
40
41 app = App(width=WIDTH, height=HEIGHT, title="Casual Crusade Harness", visible=False)
42
43 captures: dict[int, str] = {}
44 actions: dict[int, callable] = {}
45
46 # Hand-card 0 should be at roughly: centre x - 110*1.05, y = HEIGHT - 110
47 hand0_x = WIDTH * 0.5 - 110 * 1.05 - 110 / 2
48 hand0_centre = (hand0_x + 55, HEIGHT - 70)
49 # Starter is at grid (1,1) which corresponds to centre of board
50 # Tile (1,2) = directly below starter, accepts u/d cards
51 starter_centre = (WIDTH * 0.5, HEIGHT * 0.5 - 75)
52 right_tile_centre = (starter_centre[0], starter_centre[1] + 80)
53
54 # 0: menu visible (autostart=False during harness)
55 captures[10] = "01_menu.png"
56
57 # Click PLAY button
58 actions[20] = lambda: (
59 Input._on_mouse_move(WIDTH / 2, HEIGHT / 2 + 120),
60 Input._on_mouse_button(int(MouseButton.LEFT), True),
61 )
62 actions[22] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), False)
63
64 # Idle after start
65 captures[60] = "02_idle.png"
66
67 # Hover hand card 0
68 actions[70] = lambda: Input._on_mouse_move(hand0_centre[0], hand0_centre[1])
69 captures[90] = "03_hover.png"
70
71 # Press + drag toward right-of-starter tile
72 actions[100] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), True)
73 sx0, sy0 = hand0_centre
74 sx1, sy1 = right_tile_centre
75 drag_start, drag_end = 105, 145
76 span = drag_end - drag_start
77 for k, f in enumerate(range(drag_start, drag_end)):
78 u = (k + 1) / span
79
80 def make_move(x=sx0 + (sx1 - sx0) * u, y=sy0 + (sy1 - sy0) * u):
81 return lambda: Input._on_mouse_move(x, y)
82
83 actions[f] = make_move()
84 captures[140] = "04_drag.png"
85
86 # Release -> placement
87 actions[150] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), False)
88 captures[200] = "05_placed.png"
89
90 # Click reward picker option (centre)
91 actions[210] = lambda: (
92 Input._on_mouse_move(WIDTH / 2, HEIGHT / 2),
93 Input._on_mouse_button(int(MouseButton.LEFT), True),
94 )
95 actions[212] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), False)
96 captures[230] = "06_picker_pick.png"
97
98 # Drag a horizontal card onto the (0,1) tile (left of starter)
99 # Hand l/r card is at slot 1 (index 1) after picker pick
100 hand1_x = WIDTH * 0.5 - 110 / 2 # slot 1 is at the centre
101 hand1_centre = (hand1_x + 55, HEIGHT - 70)
102 left_tile_centre = (starter_centre[0] - 110, starter_centre[1])
103
104 # Wait for picker to clear, then start second drag
105 actions[238] = lambda: Input._on_mouse_move(hand1_centre[0], hand1_centre[1])
106 actions[240] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), True)
107 bx0, by0 = hand1_centre
108 bx1, by1 = left_tile_centre
109 for k, f in enumerate(range(245, 280)):
110 u = (k + 1) / (280 - 245)
111
112 def make_move(x=bx0 + (bx1 - bx0) * u, y=by0 + (by1 - by0) * u):
113 return lambda: Input._on_mouse_move(x, y)
114
115 actions[f] = make_move()
116 actions[285] = lambda: Input._on_mouse_button(int(MouseButton.LEFT), False)
117 captures[320] = "07_second_place.png"
118
119 # Save then load
120 actions[340] = lambda: Input._on_key(int(Key.S), True)
121 actions[342] = lambda: Input._on_key(int(Key.S), False)
122 actions[360] = lambda: Input._on_key(int(Key.L), True)
123 actions[362] = lambda: Input._on_key(int(Key.L), False)
124 captures[400] = "08_save_load.png"
125
126 total_frames = 420
127 saved: list[str] = []
128
129 def on_frame(idx, t):
130 fn = actions.get(idx)
131 if fn is not None:
132 fn()
133
134 def capture_fn(idx):
135 return idx in captures
136
137 frames = app.run_headless(
138 CasualCrusadeRoot(),
139 frames=total_frames,
140 on_frame=on_frame,
141 capture_fn=capture_fn,
142 )
143
144 capture_indices = sorted(captures.keys())
145 for fi, img in zip(capture_indices, frames, strict=False):
146 out = OUT / captures[fi]
147 save_png(img, out)
148 saved.append(str(out))
149 print(f"saved {out}")
150 print(f"{len(saved)} screenshots written")
151
152
153if __name__ == "__main__":
154 capture_sequence()