harness.py

Part of Tiny Yurts.

  1"""Scripted-input harness for Tiny Yurts.
  2
  3Captures key gameplay stages:
  4
  5  01_menu       : title screen
  6  02_game       : fresh game scene (right after pressing ENTER)
  7  03_dragging   : mid-drag while drawing a path
  8  04_paths_done : complete path between goat farm and goat yurt
  9  05_settler    : settler en route on the new path
 10  06_score      : settler delivered, score advanced
 11  07_lost       : an unfed farm has overflowed, loss banner up
 12"""
 13
 14from __future__ import annotations
 15
 16import sys
 17from pathlib import Path
 18
 19_PORT_DIR = Path(__file__).parent
 20if str(_PORT_DIR) not in sys.path:
 21    sys.path.insert(0, str(_PORT_DIR))
 22
 23from nodes import iso  # noqa: E402
 24
 25from simvx.core.input.controls import Key  # noqa: E402
 26from simvx.core.testing.input_sim import InputSimulator  # noqa: E402
 27from simvx.graphics import App, save_png  # noqa: E402
 28
 29WIDTH = 1280
 30HEIGHT = 720
 31
 32
 33def _cell_to_screen(cell: tuple[int, int]) -> tuple[float, float]:
 34    return iso.world_to_screen(*cell)
 35
 36
 37def main() -> None:
 38    from main import TinyYurtsRoot  # noqa
 39
 40    captures: dict[int, str] = {}
 41
 42    # We schedule actions by frame index. Each tuple (frame, fn).
 43    # We give the app some warm-up to register the InputMap and render the menu.
 44    schedule: list[tuple[int, callable]] = []
 45
 46    # Frame 30: capture menu
 47    captures[30] = "01_menu.png"
 48
 49    # Frame 40: press ENTER to start game
 50    def press_enter() -> None:
 51        sim.press_key(Key.ENTER)
 52
 53    schedule.append((40, press_enter))
 54
 55    def release_enter() -> None:
 56        sim.release_key(Key.ENTER)
 57
 58    schedule.append((42, release_enter))
 59
 60    # Frame 70: capture fresh game (after scene change settles)
 61    captures[70] = "02_game.png"
 62
 63    # Now: draw goat-farm (9, 2) -> goat-yurt (7, 4) along (8,2)(8,3)(7,3)(7,4)
 64    drag_cells = [(9, 2), (8, 2), (8, 3), (7, 3), (7, 4)]
 65
 66    # Frame 80: start drag at (9, 2)
 67    start_cell = drag_cells[0]
 68
 69    def drag_start() -> None:
 70        sx, sy = _cell_to_screen(start_cell)
 71        sim.move_mouse(sx, sy)
 72        sim.press_mouse(button=0, position=(sx, sy))
 73
 74    schedule.append((80, drag_start))
 75
 76    # Frames 84, 88, 92, 96: move through cells (one per step)
 77    for i, cell in enumerate(drag_cells[1:], start=1):
 78        f = 80 + i * 4
 79
 80        def make_move(c=cell):
 81            def fn() -> None:
 82                sx, sy = _cell_to_screen(c)
 83                sim.move_mouse(sx, sy)
 84
 85            return fn
 86
 87        schedule.append((f, make_move()))
 88
 89    # Frame 102: capture mid-drag (cursor on penultimate cell)
 90    captures[100] = "03_dragging.png"
 91
 92    # Frame 104: release mouse
 93    def drag_end() -> None:
 94        sim.release_mouse(button=0)
 95
 96    schedule.append((104, drag_end))
 97
 98    # Frame 110: capture completed paths
 99    captures[110] = "04_paths_done.png"
100
101    # Frame 200: settler should be moving by now
102    captures[200] = "05_settler.png"
103
104    # Frame 360: give farms time to keep ticking, settlers should have delivered
105    captures[360] = "06_score.png"
106
107    # Frame 900: let unfed fish farm overflow → loss screen
108    captures[900] = "07_lost.png"
109
110    # Run simulation long enough to see the loss
111    total_frames = 920
112
113    def on_frame(idx: int, _t: float) -> None:
114        for f, fn in schedule:
115            if f == idx:
116                fn()
117
118    capture_indices = sorted(captures.keys())
119
120    app = App(width=WIDTH, height=HEIGHT, title="Tiny Yurts (harness)", visible=False)
121    root = TinyYurtsRoot()
122    sim = InputSimulator(root)
123    frames = app.run_headless(
124        root,
125        frames=total_frames,
126        on_frame=on_frame,
127        capture_frames=capture_indices,
128    )
129
130    out_dir = _PORT_DIR / "screenshots"
131    out_dir.mkdir(exist_ok=True)
132    for idx, img in zip(capture_indices, frames, strict=False):
133        name = captures[idx]
134        out_path = out_dir / name
135        save_png(img, out_path)
136        print(f"saved {out_path}")
137
138
139if __name__ == "__main__":
140    main()