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 # ------------------------------------------------------------------ tick
88 def on_process(self, dt: float) -> None:
89 # Layout buttons across the bottom strip
90 vw, vh = self.viewport_size.x, self.viewport_size.y
91 n = len(self.buttons)
92 total_w = n * BUTTON_W + (n - 1) * BUTTON_GAP
93 x0 = (vw - total_w) * 0.5 + BUTTON_W * 0.5
94 y = vh - STRIP_HEIGHT * 0.5
95 for i, b in enumerate(self.buttons):
96 b.x = x0 + i * (BUTTON_W + BUTTON_GAP)
97 b.y = y
98
99 mp = Input.mouse_position
100 for b in self.buttons:
101 b.hovered = b.contains(mp)
102 b.pressed = b.hovered and Input.is_mouse_button_pressed(MouseButton.LEFT) and self._press_target is b
103 if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
104 for b in self.buttons:
105 if b.contains(mp):
106 self._press_target = b
107 break
108 if Input.is_mouse_button_just_released(MouseButton.LEFT):
109 if self._press_target and self._press_target.contains(mp):
110 self.button_pressed(self._press_target.action)
111 self._press_target = None
112
113 # Hotkeys
114 if Input.is_key_just_pressed(Key.R):
115 self._on_button("restart")
116 if Input.is_key_just_pressed(Key.ESCAPE):
117 self._on_button("quit")
118
119 # ------------------------------------------------------------------ draw
120 def on_draw(self, renderer) -> None:
121 # Dark background
122 renderer.draw_rect(
123 (0, 0), (WIDTH, HEIGHT), colour=theme.DARK_BG, filled=True
124 )
125 # Bottom strip background
126 vw, vh = self.viewport_size.x, self.viewport_size.y
127 strip_y = vh - STRIP_HEIGHT
128 renderer.draw_rect(
129 (0, strip_y), (vw, STRIP_HEIGHT), colour=(0.78, 0.80, 0.83, 1.0), filled=True
130 )
131 renderer.draw_rect(
132 (0, strip_y), (vw, 1), colour=(0.55, 0.58, 0.62, 1.0), filled=True
133 )
134 for b in self.buttons:
135 base = (0.92, 0.94, 0.96, 1.0)
136 if b.pressed:
137 base = (0.55, 0.65, 0.85, 1.0)
138 elif b.hovered:
139 base = (0.86, 0.92, 0.99, 1.0)
140 renderer.draw_rect(
141 (b.x - b.w * 0.5, b.y - b.h * 0.5), (b.w, b.h), colour=base, filled=True
142 )
143 renderer.draw_rect(
144 (b.x - b.w * 0.5, b.y - b.h * 0.5),
145 (b.w, b.h),
146 colour=(0.3, 0.35, 0.4, 1.0),
147 filled=False,
148 )
149 # Centre label approx (no measure helper)
150 approx_w = len(b.label) * 8.5 * 0.85
151 renderer.draw_text(
152 b.label,
153 (b.x - approx_w * 0.5, b.y - 8),
154 scale=0.85,
155 colour=(0.18, 0.22, 0.28, 1.0),
156 )
157
158 # Game over overlay
159 if self.stage.state.game_over and self.stage.state.game_over_at_ms is not None:
160 elapsed_ms = self.stage.state.now_ms - self.stage.state.game_over_at_ms
161 if elapsed_ms > 1000:
162 # Dim the playfield
163 renderer.draw_rect(
164 (0, 0), (WIDTH, HEIGHT), colour=(0, 0, 0, 0.7), filled=True
165 )
166 # Banner
167 renderer.draw_text(
168 "SHUTDOWN",
169 (WIDTH * 0.5 - 100, HEIGHT * 0.45),
170 scale=3.0,
171 colour=(1.0, 0.4, 0.4, 1.0),
172 )
173 renderer.draw_text(
174 f"Score: {self.stage.state.stats.score} processes",
175 (WIDTH * 0.5 - 130, HEIGHT * 0.55),
176 scale=1.2,
177 colour=(1.0, 1.0, 1.0, 1.0),
178 )
179 renderer.draw_text(
180 "Press R to restart",
181 (WIDTH * 0.5 - 90, HEIGHT * 0.6),
182 scale=1.0,
183 colour=(0.85, 0.92, 0.95, 1.0),
184 )
185
186 # ---------------------------------------------------------------- glue
187 def _on_button(self, action: str) -> None:
188 if action == "restart":
189 self._restart()
190 elif action == "quit":
191 self.app.quit()
192
193 def _on_game_over(self) -> None:
194 # Overlay rendered in on_draw when game_over is true
195 pass
196
197 def _restart(self) -> None:
198 # Replace the stage with a fresh one
199 self.stage.queue_free()
200 self.stage = StageNode(viewport_size=self.viewport_size)
201 self.add_child(self.stage)
202 self.stage.game_over_changed.connect(self._on_game_over)
203
204
205def main() -> None:
206 headless = "--test" in sys.argv
207 if headless:
208 from harness import run_harness
209 run_harness()
210 return
211 app = App(width=WIDTH, height=HEIGHT, title="You're the OS (SimVX)")
212 app.run(OsRoot())
213
214
215if __name__ == "__main__":
216 main()