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