You’re the OS¶
OS simulation, process state machine, CPUs, RAM/disk swap, I/O queue.
▶ Run in browserUpstream: https://github.com/plbrault/youre-the-os
Tags: port tier-2
You’re the OS: SimVX Port (Tier 2 #26)¶
Port of plbrault/youre-the-os.
Engine test only: upstream is GPLv3, so this port is not promoted to
simvx/games/. The point is the head-to-head against pygbag for web export.
Run¶
# Interactive
uv run python ported_games/youre_the_os/simvx_port/main.py
# Headless screenshot capture
uv run python ported_games/youre_the_os/simvx_port/main.py --test
# Web export
uv run simvx export web /home/fezzik/dev/ported_games/youre_the_os/simvx_port/main.py \
-o /home/fezzik/dev/ported_games/youre_the_os/simvx_port/web/index.html
Layout¶
simvx_port/
├── main.py # OsRoot + App boot + --test mode
├── pyproject.toml # [tool.simvx] root = "OsRoot"
├── nodes/
│ ├── theme.py # Colour palette, ASCII face glyph map
│ ├── state.py # Pure-Python simulation (state machine + tick())
│ └── stage.py # StageNode: renders the simulation, polls input
├── harness.py # Scripted screenshot harness
├── screenshots/ # 6+ PNGs from --test
└── web/ # `simvx export web` output
Scope¶
Single-difficulty (Normal) gameplay loop. See PLAN.md for the IN/OUT
list and rationale.
Source¶
1"""You're the OS: OS simulation, process state machine, CPUs, RAM/disk swap, I/O queue.
2
3# /// simvx
4# tags = ["port", "tier-2"]
5# upstream = "https://github.com/plbrault/youre-the-os"
6# web = { width = 1280, height = 720, responsive = true }
7# ///
8
9Run:
10 uv run python ported_games/youre_the_os/simvx_port/main.py # interactive
11 uv run python ported_games/youre_the_os/simvx_port/main.py --test # headless capture
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 theme # noqa: E402
24from nodes.stage import StageNode # noqa: E402
25
26from simvx.core import Node2D, Signal # noqa: E402
27from simvx.core.input.enums import Key, MouseButton # noqa: E402
28from simvx.core.input.map import InputMap # noqa: E402
29from simvx.core.input.state import Input # noqa: E402
30from simvx.core.math.types import Vec2 # noqa: E402
31from simvx.graphics import App # noqa: E402
32
33WIDTH = 1280
34HEIGHT = 720
35
36# Bottom controls strip: port-UX baseline
37STRIP_HEIGHT = 70
38BUTTON_W = 130
39BUTTON_H = 44
40BUTTON_GAP = 18
41
42
43class _Button:
44 def __init__(self, label: str, action: str) -> None:
45 self.label = label
46 self.action = action
47 self.x = 0.0
48 self.y = 0.0
49 self.w = BUTTON_W
50 self.h = BUTTON_H
51 self.hovered = False
52 self.pressed = False
53
54 def contains(self, mp: Vec2) -> bool:
55 return (
56 self.x - self.w * 0.5 <= mp.x <= self.x + self.w * 0.5
57 and self.y - self.h * 0.5 <= mp.y <= self.y + self.h * 0.5
58 )
59
60
61class OsRoot(Node2D):
62 """Root scene: dark background + Stage + bottom HUD + game-over overlay."""
63
64 button_pressed = Signal() # (action: str)
65
66 def on_ready(self) -> None:
67 # Input actions live in root.on_ready (web exporter skips main()).
68 InputMap.add_action("primary", [MouseButton.LEFT])
69 InputMap.add_action("secondary", [MouseButton.RIGHT])
70 InputMap.add_action("deliver_io", [Key.SPACE])
71 InputMap.add_action("restart", [Key.R])
72 InputMap.add_action("quit", [Key.ESCAPE])
73
74 self.viewport_size = Vec2(WIDTH, HEIGHT)
75 self.stage = StageNode(viewport_size=self.viewport_size)
76 self.add_child(self.stage)
77 self.stage.game_over_changed.connect(self._on_game_over)
78
79 # Bottom controls
80 self.buttons: list[_Button] = [
81 _Button("Restart", "restart"),
82 _Button("Quit", "quit"),
83 ]
84 self._press_target: _Button | None = None
85 self.button_pressed.connect(self._on_button)
86
87 # The root's on_draw renders button hover/press state and the game-over
88 # SHUTDOWN overlay -- all from non-Property state (button flags mutated in
89 # on_update; self.stage.state.game_over owned by a sibling node). None of
90 # those writes dirty this node, and once the stage stops churning at
91 # game-over there are no incidental re-collects to refresh it, so the
92 # overlay would silently freeze out. Mark the root dynamic: its small
93 # on_draw (background + strip + two buttons + overlay) re-captures every
94 # frame while the heavy stage stays retained.
95 self.dynamic = True
96
97 # ------------------------------------------------------------------ tick
98 def on_update(self, dt: float) -> None:
99 # Layout buttons across the bottom strip
100 vw, vh = self.viewport_size.x, self.viewport_size.y
101 n = len(self.buttons)
102 total_w = n * BUTTON_W + (n - 1) * BUTTON_GAP
103 x0 = (vw - total_w) * 0.5 + BUTTON_W * 0.5
104 y = vh - STRIP_HEIGHT * 0.5
105 for i, b in enumerate(self.buttons):
106 b.x = x0 + i * (BUTTON_W + BUTTON_GAP)
107 b.y = y
108
109 mp = Input.mouse_position
110 for b in self.buttons:
111 b.hovered = b.contains(mp)
112 b.pressed = b.hovered and Input.is_mouse_button_pressed(MouseButton.LEFT) and self._press_target is b
113 if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
114 for b in self.buttons:
115 if b.contains(mp):
116 self._press_target = b
117 break
118 if Input.is_mouse_button_just_released(MouseButton.LEFT):
119 if self._press_target and self._press_target.contains(mp):
120 self.button_pressed(self._press_target.action)
121 self._press_target = None
122
123 # Hotkeys
124 if Input.is_key_just_pressed(Key.R):
125 self._on_button("restart")
126 if Input.is_key_just_pressed(Key.ESCAPE):
127 self._on_button("quit")
128
129 # ------------------------------------------------------------------ draw
130 def on_draw(self, renderer) -> None:
131 # Dark background
132 renderer.draw_rect(
133 (0, 0), (WIDTH, HEIGHT), colour=theme.DARK_BG, filled=True
134 )
135 # Bottom strip background
136 vw, vh = self.viewport_size.x, self.viewport_size.y
137 strip_y = vh - STRIP_HEIGHT
138 renderer.draw_rect(
139 (0, strip_y), (vw, STRIP_HEIGHT), colour=(0.78, 0.80, 0.83, 1.0), filled=True
140 )
141 renderer.draw_rect(
142 (0, strip_y), (vw, 1), colour=(0.55, 0.58, 0.62, 1.0), filled=True
143 )
144 for b in self.buttons:
145 base = (0.92, 0.94, 0.96, 1.0)
146 if b.pressed:
147 base = (0.55, 0.65, 0.85, 1.0)
148 elif b.hovered:
149 base = (0.86, 0.92, 0.99, 1.0)
150 renderer.draw_rect(
151 (b.x - b.w * 0.5, b.y - b.h * 0.5), (b.w, b.h), colour=base, filled=True
152 )
153 renderer.draw_rect(
154 (b.x - b.w * 0.5, b.y - b.h * 0.5),
155 (b.w, b.h),
156 colour=(0.3, 0.35, 0.4, 1.0),
157 filled=False,
158 )
159 # Centre label approx (no measure helper)
160 approx_w = len(b.label) * 8.5 * 0.85
161 renderer.draw_text(
162 b.label,
163 (b.x - approx_w * 0.5, b.y - 8),
164 scale=0.85,
165 colour=(0.18, 0.22, 0.28, 1.0),
166 )
167
168 # Game over overlay
169 if self.stage.state.game_over and self.stage.state.game_over_at_ms is not None:
170 elapsed_ms = self.stage.state.now_ms - self.stage.state.game_over_at_ms
171 if elapsed_ms > 1000:
172 # Dim the playfield
173 renderer.draw_rect(
174 (0, 0), (WIDTH, HEIGHT), colour=(0, 0, 0, 0.7), filled=True
175 )
176 # Banner
177 renderer.draw_text(
178 "SHUTDOWN",
179 (WIDTH * 0.5 - 100, HEIGHT * 0.45),
180 scale=3.0,
181 colour=(1.0, 0.4, 0.4, 1.0),
182 )
183 renderer.draw_text(
184 f"Score: {self.stage.state.stats.score} processes",
185 (WIDTH * 0.5 - 130, HEIGHT * 0.55),
186 scale=1.2,
187 colour=(1.0, 1.0, 1.0, 1.0),
188 )
189 renderer.draw_text(
190 "Press R to restart",
191 (WIDTH * 0.5 - 90, HEIGHT * 0.6),
192 scale=1.0,
193 colour=(0.85, 0.92, 0.95, 1.0),
194 )
195
196 # ---------------------------------------------------------------- glue
197 def _on_button(self, action: str) -> None:
198 if action == "restart":
199 self._restart()
200 elif action == "quit":
201 self.app.quit()
202
203 def _on_game_over(self) -> None:
204 # Overlay rendered in on_draw when game_over is true
205 pass
206
207 def _restart(self) -> None:
208 # Replace the stage with a fresh one
209 self.stage.queue_free()
210 self.stage = StageNode(viewport_size=self.viewport_size)
211 self.add_child(self.stage)
212 self.stage.game_over_changed.connect(self._on_game_over)
213
214
215def main() -> None:
216 headless = "--test" in sys.argv
217 if headless:
218 from harness import run_harness
219 run_harness()
220 return
221 app = App(width=WIDTH, height=HEIGHT, title="You're the OS (SimVX)")
222 app.run(OsRoot())
223
224
225if __name__ == "__main__":
226 main()