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.enums 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    sim = InputSimulator()
 41    captures: dict[int, str] = {}
 42
 43    # We schedule actions by frame index. Each tuple (frame, fn).
 44    # We give the app some warm-up to register the InputMap and render the menu.
 45    schedule: list[tuple[int, callable]] = []
 46
 47    # Frame 30: capture menu
 48    captures[30] = "01_menu.png"
 49
 50    # Frame 40: press ENTER to start game
 51    def press_enter() -> None:
 52        sim.press_key(Key.ENTER)
 53
 54    schedule.append((40, press_enter))
 55
 56    def release_enter() -> None:
 57        sim.release_key(Key.ENTER)
 58
 59    schedule.append((42, release_enter))
 60
 61    # Frame 70: capture fresh game (after scene change settles)
 62    captures[70] = "02_game.png"
 63
 64    # Now: draw goat-farm (9, 2) -> goat-yurt (7, 4) along (8,2)(8,3)(7,3)(7,4)
 65    drag_cells = [(9, 2), (8, 2), (8, 3), (7, 3), (7, 4)]
 66
 67    # Frame 80: start drag at (9, 2)
 68    start_cell = drag_cells[0]
 69
 70    def drag_start() -> None:
 71        sx, sy = _cell_to_screen(start_cell)
 72        sim.move_mouse(sx, sy)
 73        sim.press_mouse(button=0, position=(sx, sy))
 74
 75    schedule.append((80, drag_start))
 76
 77    # Frames 84, 88, 92, 96: move through cells (one per step)
 78    for i, cell in enumerate(drag_cells[1:], start=1):
 79        f = 80 + i * 4
 80
 81        def make_move(c=cell):
 82            def fn() -> None:
 83                sx, sy = _cell_to_screen(c)
 84                sim.move_mouse(sx, sy)
 85
 86            return fn
 87
 88        schedule.append((f, make_move()))
 89
 90    # Frame 102: capture mid-drag (cursor on penultimate cell)
 91    captures[100] = "03_dragging.png"
 92
 93    # Frame 104: release mouse
 94    def drag_end() -> None:
 95        sim.release_mouse(button=0)
 96
 97    schedule.append((104, drag_end))
 98
 99    # Frame 110: capture completed paths
100    captures[110] = "04_paths_done.png"
101
102    # Frame 200: settler should be moving by now
103    captures[200] = "05_settler.png"
104
105    # Frame 360: give farms time to keep ticking, settlers should have delivered
106    captures[360] = "06_score.png"
107
108    # Frame 900: let unfed fish farm overflow → loss screen
109    captures[900] = "07_lost.png"
110
111    # Run simulation long enough to see the loss
112    total_frames = 920
113
114    def on_frame(idx: int, _t: float) -> None:
115        for f, fn in schedule:
116            if f == idx:
117                fn()
118
119    capture_indices = sorted(captures.keys())
120
121    app = App(width=WIDTH, height=HEIGHT, title="Tiny Yurts (harness)", visible=False)
122    frames = app.run_headless(
123        TinyYurtsRoot(),
124        frames=total_frames,
125        on_frame=on_frame,
126        capture_frames=capture_indices,
127    )
128
129    out_dir = _PORT_DIR / "screenshots"
130    out_dir.mkdir(exist_ok=True)
131    for idx, img in zip(capture_indices, frames, strict=False):
132        name = captures[idx]
133        out_path = out_dir / name
134        save_png(img, out_path)
135        print(f"saved {out_path}")
136
137
138if __name__ == "__main__":
139    main()