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