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()