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.controls import Key # noqa: E402
31from simvx.core.testing import InputSimulator # 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 root = CasualCrusadeRoot()
46 drivers: list[InputSimulator] = []
47
48 def sim() -> InputSimulator:
49 """The window the script presses through, onto the root's tree once it has one."""
50 if not drivers:
51 drivers.append(InputSimulator(root.tree))
52 return drivers[0]
53
54 # Hand-card 0 should be at roughly: centre x - 110*1.05, y = HEIGHT - 110
55 hand0_x = WIDTH * 0.5 - 110 * 1.05 - 110 / 2
56 hand0_centre = (hand0_x + 55, HEIGHT - 70)
57 # Starter is at grid (1,1) which corresponds to centre of board
58 # Tile (1,2) = directly below starter, accepts u/d cards
59 starter_centre = (WIDTH * 0.5, HEIGHT * 0.5 - 75)
60 right_tile_centre = (starter_centre[0], starter_centre[1] + 80)
61
62 # 0: menu visible (autostart=False during harness)
63 captures[10] = "01_menu.png"
64
65 # Click PLAY button
66 actions[20] = lambda: (
67 sim().move_mouse(WIDTH / 2, HEIGHT / 2 + 120),
68 sim().press_mouse(),
69 )
70 actions[22] = lambda: sim().release_mouse()
71
72 # Idle after start
73 captures[60] = "02_idle.png"
74
75 # Hover hand card 0
76 actions[70] = lambda: sim().move_mouse(hand0_centre[0], hand0_centre[1])
77 captures[90] = "03_hover.png"
78
79 # Press + drag toward right-of-starter tile
80 actions[100] = lambda: sim().press_mouse()
81 sx0, sy0 = hand0_centre
82 sx1, sy1 = right_tile_centre
83 drag_start, drag_end = 105, 145
84 span = drag_end - drag_start
85 for k, f in enumerate(range(drag_start, drag_end)):
86 u = (k + 1) / span
87
88 def make_move(x=sx0 + (sx1 - sx0) * u, y=sy0 + (sy1 - sy0) * u):
89 return lambda: sim().move_mouse(x, y)
90
91 actions[f] = make_move()
92 captures[140] = "04_drag.png"
93
94 # Release -> placement
95 actions[150] = lambda: sim().release_mouse()
96 captures[200] = "05_placed.png"
97
98 # Click reward picker option (centre)
99 actions[210] = lambda: (
100 sim().move_mouse(WIDTH / 2, HEIGHT / 2),
101 sim().press_mouse(),
102 )
103 actions[212] = lambda: sim().release_mouse()
104 captures[230] = "06_picker_pick.png"
105
106 # Drag a horizontal card onto the (0,1) tile (left of starter)
107 # Hand l/r card is at slot 1 (index 1) after picker pick
108 hand1_x = WIDTH * 0.5 - 110 / 2 # slot 1 is at the centre
109 hand1_centre = (hand1_x + 55, HEIGHT - 70)
110 left_tile_centre = (starter_centre[0] - 110, starter_centre[1])
111
112 # Wait for picker to clear, then start second drag
113 actions[238] = lambda: sim().move_mouse(hand1_centre[0], hand1_centre[1])
114 actions[240] = lambda: sim().press_mouse()
115 bx0, by0 = hand1_centre
116 bx1, by1 = left_tile_centre
117 for k, f in enumerate(range(245, 280)):
118 u = (k + 1) / (280 - 245)
119
120 def make_move(x=bx0 + (bx1 - bx0) * u, y=by0 + (by1 - by0) * u):
121 return lambda: sim().move_mouse(x, y)
122
123 actions[f] = make_move()
124 actions[285] = lambda: sim().release_mouse()
125 captures[320] = "07_second_place.png"
126
127 # Save then load
128 actions[340] = lambda: sim().press_key(Key.S)
129 actions[342] = lambda: sim().release_key(Key.S)
130 actions[360] = lambda: sim().press_key(Key.L)
131 actions[362] = lambda: sim().release_key(Key.L)
132 captures[400] = "08_save_load.png"
133
134 total_frames = 420
135 saved: list[str] = []
136
137 def on_frame(idx, t):
138 fn = actions.get(idx)
139 if fn is not None:
140 fn()
141
142 def capture_fn(idx):
143 return idx in captures
144
145 frames = app.run_headless(
146 root,
147 frames=total_frames,
148 on_frame=on_frame,
149 capture_fn=capture_fn,
150 )
151
152 capture_indices = sorted(captures.keys())
153 for fi, img in zip(capture_indices, frames, strict=False):
154 out = OUT / captures[fi]
155 save_png(img, out)
156 saved.append(str(out))
157 print(f"saved {out}")
158 print(f"{len(saved)} screenshots written")
159
160
161if __name__ == "__main__":
162 capture_sequence()