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