You’re the OS¶
schedule processes, swap memory pages, and keep the user calm.
â–¶ Run in browserUpstream: https://github.com/plbrault/youre-the-os
Licence: this port's own code is offered under GPL-3.0-or-later, not the SimVX Examples Licence the rest of the gallery carries. See ATTRIBUTION.md for the upstream it re-implements, the terms of anything it bundles, and the credit each one requires.
Ports live in the repository only, not in the simvx-examples distribution, because each is a derivative work licensed individually against the game it re-implements. Read it with git clone https://git.simvx.com/simvx/simvx.
Tags: port tier-2
You’re the OS: SimVX port¶
Port of plbrault/youre-the-os, a game in which you are the operating system. Processes queue up, get hungry for CPU time, fault on memory that you left on disk, and block waiting for I/O. Nothing happens unless you make it happen, and if ten processes starve to death the user rage-quits and the machine shuts down.
Licensing note: upstream is GPL-3.0-or-later, so this port is too. See
ATTRIBUTION.md and UPSTREAM_LICENSE.md.
Run¶
From the repository root:
uv run python examples/ports/youre_the_os/main.py # interactive
uv run python examples/ports/youre_the_os/main.py --test # headless capture into screenshots/
uv run simvx export web examples/ports/youre_the_os/main.py \
-o /tmp/youre_the_os.html
How to play¶
The title screen lists the essentials; the bottom strip keeps a one-line reminder while you play.
Click a process (a square in the grid) to move it onto a free CPU. Click it again to take it off. Only four processes can run at once.
A process off the CPU slowly starves: its face sinks from a smile to a grimace while its colour runs yellow, orange, red, then darker red. At the bottom of that slide it dies and lands in the ragequit row. Ten of those and the game is over. Five seconds on a CPU resets it to a green grin.
Click a memory page, or drag across several, to request a swap. Pages in RAM move to disk and pages on disk move back to RAM, one at a time. A running process whose page is on disk is page-faulting (it pulses blue) and makes no progress until you swap the page back in.
Press Space, or click the I/O EVENTS box, to deliver every queued I/O event. Processes showing an hourglass are waiting on one.
Rrestarts,Escapequits; the same two actions sit on the bottom strip as buttons for pointer-only and touch play.
What it shows about SimVX¶
A simulation with no engine in it.
nodes/state.pyis plain Python: a process state machine, page allocator, swap queues, I/O queue. It imports nothing from SimVX and exposes read-only accessors (pages(),all_processes(),dead_processes()) for whatever wants to draw it.One node draws everything.
StageNodeis a singleNode2Dmarkeddynamic = True; itson_drawpaints 42 process slots, ~180 page slots, four CPUs and the HUD every frame. At this element count that is far cheaper than a node per element, and it keeps the scene tree two nodes deep.A title screen out of the pause system. The root sets
tree.paused = Trueandupdate_mode = UpdateMode.ALWAYSon itself;StageNodesetsUpdateMode.PAUSABLE, so the simulation is frozen while the root keeps ticking to watch for the click that boots it. The title card and the shutdown notice are one small child node added after the stage: a node’son_drawruns before any child that keeps a non-negativez_index, so anything that must sit on top of the playfield is easiest to add as a later sibling rather than to paint from the parent.Named input actions. Every mouse and key read goes through
Input.is_action_pressed("primary")and friends, declared once in the root’sinput_actionsclass attribute so the web export picks them up too.Signals for cross-node news. The stage emits
game_over_changedonce; the root notes the time and raises the shutdown notice a second later, so nothing polls the simulation for it.Resize-aware layout. Both nodes track
tree.screen_size, so the background, the ragequit row and the controls strip follow the window (and the browser’s aspect ratio in the web export).Input-driven capture harness.
harness.pydrives the real input pipeline withInputSimulatorand captures seven stages headlessly.
Files¶
main.py:OsRootscene, the title/shutdown overlay card, bottom controls stripnodes/state.py: the simulation (state machine, pages, swaps, I/O, scoring)nodes/stage.py:StageNode, all layout, drawing and input hit-testingnodes/theme.py: colour palette and the ASCII faces standing in for upstream’s emojiharness.py: scripted seven-stage screenshot capturescreenshots/: output of--test
Deviations from upstream¶
Single difficulty (upstream’s Normal): four CPUs, 42 process slots, 128 RAM page slots and 48 on disk.
No image assets at all. Upstream’s OpenMoji process faces become ASCII faces and the hourglass is drawn from rectangles, so the web export carries no texture payload.
No sound, no high-score table, no in-game options screen.
Source files¶
File |
Summary |
Lines |
|---|---|---|
You’re the OS: schedule processes, swap memory pages, and keep the user calm. |
314 |
|
Scripted-input harness: drives the port headlessly and captures seven stages. |
133 |
|
Port of You’re the OS: node modules. |
1 |
|
StageNode: the playfield. |
425 |
|
Pure-Python game-state model. |
754 |
|
Colour palette and process-face glyph map. |
80 |
Source¶
1"""You're the OS: schedule processes, swap memory pages, and keep the user calm.
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
9A port of Pier-Luc Brault's "You're the OS!". You are the operating system: put
10processes on one of four CPUs before they starve, swap their memory pages
11between RAM and disk to clear page faults, and deliver queued I/O events. Ten
12starved processes and the user rage-quits: shutdown.
13
14The simulation is plain Python with no engine imports (``nodes/state.py``), and
15one dynamic ``Node2D`` draws all 42 process slots and ~180 page slots in
16``on_draw``. Input is polled through named input actions, the title screen is
17just ``tree.paused`` plus an ``UpdateMode``, and game-over reaches the root as a
18Signal.
19
20Controls: click a process to move it on or off a CPU, click or drag across pages
21to request and cancel swaps, Space or a click on the I/O box delivers events, R
22restarts, Escape quits.
23
24Run:
25 uv run python examples/ports/youre_the_os/main.py # interactive
26 uv run python examples/ports/youre_the_os/main.py --test # headless capture
27"""
28
29from __future__ import annotations
30
31import sys
32from pathlib import Path
33
34_PORT_DIR = Path(__file__).parent
35if str(_PORT_DIR) not in sys.path:
36 sys.path.insert(0, str(_PORT_DIR))
37
38from nodes import theme # noqa: E402
39from nodes.stage import DESIGN_HEIGHT, DESIGN_WIDTH, StageNode # noqa: E402
40
41from simvx.core import Node2D, Signal, UpdateMode # noqa: E402
42from simvx.core.input.enums import Key, MouseButton # noqa: E402
43from simvx.core.input.state import Input # noqa: E402
44from simvx.core.math.types import Vec2 # noqa: E402
45from simvx.core.text import measure_text_width # noqa: E402
46from simvx.graphics import App # noqa: E402
47
48# Bottom controls strip: port-UX baseline
49STRIP_HEIGHT = 70
50BUTTON_W = 130
51BUTTON_H = 44
52BUTTON_GAP = 18
53
54HINT = "Click a process to schedule it - Space delivers I/O"
55HINT_SCALE = 0.75
56HINT_MARGIN = 16
57
58SHUTDOWN_DELAY_S = 1.0 # let the last ragequit land before the overlay covers it
59
60PANEL_PAD = 36
61PANEL_LINE_GAP = 22
62TITLE_LINES = [
63 ("YOU'RE THE OS", 3.0, (0.6, 0.85, 1.0, 1.0)),
64 ("Click a process to put it on a CPU. Click it again to take it off.", 1.0, theme.WHITE),
65 ("Click or drag across memory pages to swap them between RAM and disk.", 1.0, theme.WHITE),
66 ("Press Space (or click the I/O box) to deliver waiting I/O events.", 1.0, theme.WHITE),
67 ("Let ten processes starve and the user rage-quits.", 1.0, (0.85, 0.75, 0.55, 1.0)),
68 ("Click anywhere or press Space to boot", 1.3, (0.6, 0.95, 0.8, 1.0)),
69]
70
71
72class _Button:
73 def __init__(self, label: str, action: str) -> None:
74 self.label = label
75 self.action = action
76 self.x = 0.0
77 self.y = 0.0
78 self.w = BUTTON_W
79 self.h = BUTTON_H
80 self.hovered = False
81 self.pressed = False
82
83 def contains(self, mp: Vec2) -> bool:
84 return (
85 self.x - self.w * 0.5 <= mp.x <= self.x + self.w * 0.5
86 and self.y - self.h * 0.5 <= mp.y <= self.y + self.h * 0.5
87 )
88
89
90class _OverlayCard(Node2D):
91 """Dimmed panel of centred text: the title card and the shutdown notice.
92
93 Added to the root *after* the stage so it paints on top of it. A node's own
94 on_draw runs before any child that keeps a non-negative ``z_index``, so the
95 root itself can only paint what belongs underneath the playfield.
96 """
97
98 dynamic = True
99
100 def __init__(self, **kwargs) -> None:
101 super().__init__(**kwargs)
102 self.lines: list[tuple[str, float, tuple[float, float, float, float]]] = []
103
104 def show(self, lines) -> None:
105 self.lines = list(lines)
106
107 def hide(self) -> None:
108 self.lines = []
109
110 def on_draw(self, renderer) -> None:
111 if not self.lines:
112 return
113 vw, vh = self.tree.screen_size
114 renderer.draw_rect((0, 0), (vw, vh), colour=(0.0, 0.0, 0.0, 0.86), filled=True)
115
116 # Panel sized to the text it holds, so the copy stays legible over the
117 # playfield at any viewport size.
118 line_heights = [26 * scale + PANEL_LINE_GAP for _, scale, _ in self.lines]
119 panel_w = max(measure_text_width(text, scale) for text, scale, _ in self.lines) + 2 * PANEL_PAD
120 panel_h = sum(line_heights) - PANEL_LINE_GAP + 2 * PANEL_PAD
121 top = (vh - panel_h) * 0.5 + PANEL_PAD
122 panel = ((vw - panel_w) * 0.5, top - PANEL_PAD)
123 renderer.draw_rect(panel, (panel_w, panel_h), colour=(0.04, 0.05, 0.08, 0.98), filled=True)
124 renderer.draw_rect(panel, (panel_w, panel_h), colour=(0.30, 0.42, 0.58, 1.0), filled=False)
125
126 y = top
127 for (text, scale, colour), line_h in zip(self.lines, line_heights, strict=True):
128 _draw_centred_text(renderer, text, vw * 0.5, y, scale=scale, colour=colour)
129 y += line_h
130
131
132class OsRoot(Node2D):
133 """Root scene: background, the Stage, the overlay card, and the bottom controls strip."""
134
135 input_actions = {
136 "primary": [MouseButton.LEFT],
137 "secondary": [MouseButton.RIGHT],
138 "deliver_io": [Key.SPACE],
139 "restart": [Key.R],
140 "quit": [Key.ESCAPE],
141 }
142
143 button_pressed = Signal()
144
145 def on_ready(self) -> None:
146 # The title card sits on a paused tree, so the root has to keep ticking
147 # to notice the click that boots the machine. StageNode opts back into
148 # PAUSABLE so only this node stays live behind the card.
149 self.update_mode = UpdateMode.ALWAYS
150
151 self.viewport_size = Vec2(DESIGN_WIDTH, DESIGN_HEIGHT)
152 self._sync_viewport()
153
154 self.stage = StageNode()
155 self.add_child(self.stage)
156 self.stage.game_over_changed.connect(self._on_game_over)
157 self._shutdown_at: float | None = None
158
159 # Boot into the title card: the playfield is laid out behind it but its
160 # simulation stays frozen until the player starts the machine.
161 self.started = False
162 self.overlay = self.add_child(_OverlayCard(name="Overlay"))
163 self.overlay.show(TITLE_LINES)
164 self.tree.paused = True
165
166 # Bottom controls
167 self.buttons: list[_Button] = [
168 _Button("Restart", "restart"),
169 _Button("Quit", "quit"),
170 ]
171 self._press_target: _Button | None = None
172 self.button_pressed.connect(self._on_button)
173
174 # The root's on_draw renders button hover and press state, which lives in
175 # plain attributes mutated from on_update rather than in Properties, so
176 # none of those writes dirty this node and its retained draws would go
177 # stale. Mark the root dynamic: its small on_draw (background, strip and
178 # two buttons) re-captures every frame while the heavy stage stays
179 # retained.
180 self.dynamic = True
181
182 # ------------------------------------------------------------------ tick
183 def on_update(self, dt: float) -> None:
184 self._sync_viewport()
185
186 if Input.is_action_just_pressed("quit"):
187 self._on_button("quit")
188 return
189
190 if not self.started:
191 if Input.is_action_just_pressed("primary") or Input.is_action_just_pressed("deliver_io"):
192 self._start()
193 return
194
195 # Layout buttons across the bottom strip
196 vw, vh = self.viewport_size.x, self.viewport_size.y
197 n = len(self.buttons)
198 total_w = n * BUTTON_W + (n - 1) * BUTTON_GAP
199 x0 = (vw - total_w) * 0.5 + BUTTON_W * 0.5
200 y = vh - STRIP_HEIGHT * 0.5
201 for i, b in enumerate(self.buttons):
202 b.x = x0 + i * (BUTTON_W + BUTTON_GAP)
203 b.y = y
204
205 mp = Input.mouse_position
206 for b in self.buttons:
207 b.hovered = b.contains(mp)
208 b.pressed = b.hovered and Input.is_action_pressed("primary") and self._press_target is b
209 if Input.is_action_just_pressed("primary"):
210 for b in self.buttons:
211 if b.contains(mp):
212 self._press_target = b
213 break
214 if Input.is_action_just_released("primary"):
215 if self._press_target and self._press_target.contains(mp):
216 self.button_pressed(self._press_target.action)
217 self._press_target = None
218
219 if Input.is_action_just_pressed("restart"):
220 self._on_button("restart")
221
222 # Let the last rage-quit land on screen before the notice covers it.
223 if self._shutdown_at is not None and self.tree.now - self._shutdown_at > SHUTDOWN_DELAY_S:
224 self._show_shutdown()
225
226 def _sync_viewport(self) -> None:
227 w, h = self.tree.screen_size
228 if (w, h) != (self.viewport_size.x, self.viewport_size.y):
229 self.viewport_size = Vec2(w, h)
230
231 # ------------------------------------------------------------------ draw
232 def on_draw(self, renderer) -> None:
233 vw, vh = self.viewport_size.x, self.viewport_size.y
234
235 # Dark background
236 renderer.draw_rect((0, 0), (vw, vh), colour=theme.DARK_BG, filled=True)
237
238 if not self.started:
239 # The title card child paints over the playfield; the controls strip
240 # would only show through it.
241 return
242
243 # Bottom strip background
244 strip_y = vh - STRIP_HEIGHT
245 renderer.draw_rect((0, strip_y), (vw, STRIP_HEIGHT), colour=(0.78, 0.80, 0.83, 1.0), filled=True)
246 renderer.draw_rect((0, strip_y), (vw, 1), colour=(0.55, 0.58, 0.62, 1.0), filled=True)
247 # Hint sits left of the centred buttons, and yields to them on a window
248 # too narrow to hold both.
249 hint_end = HINT_MARGIN + measure_text_width(HINT, HINT_SCALE) + HINT_MARGIN
250 if hint_end < min(b.x - b.w * 0.5 for b in self.buttons):
251 renderer.draw_text(HINT, (HINT_MARGIN, strip_y + 28), scale=HINT_SCALE, colour=(0.06, 0.08, 0.12, 1.0))
252 for b in self.buttons:
253 base = (0.92, 0.94, 0.96, 1.0)
254 if b.pressed:
255 base = (0.55, 0.65, 0.85, 1.0)
256 elif b.hovered:
257 base = (0.86, 0.92, 0.99, 1.0)
258 renderer.draw_rect((b.x - b.w * 0.5, b.y - b.h * 0.5), (b.w, b.h), colour=base, filled=True)
259 renderer.draw_rect(
260 (b.x - b.w * 0.5, b.y - b.h * 0.5),
261 (b.w, b.h),
262 colour=(0.3, 0.35, 0.4, 1.0),
263 filled=False,
264 )
265 _draw_centred_text(renderer, b.label, b.x, b.y - 8, scale=0.85, colour=(0.05, 0.07, 0.11, 1.0))
266
267 # ---------------------------------------------------------------- glue
268 def _start(self) -> None:
269 self.started = True
270 self.overlay.hide()
271 self.tree.paused = False
272
273 def _on_game_over(self) -> None:
274 """The stage announced a shutdown: note when, so the notice can follow it."""
275 self._shutdown_at = self.tree.now
276
277 def _show_shutdown(self) -> None:
278 stats = self.stage.state.stats
279 finished = f"{stats.score} process{'' if stats.score == 1 else 'es'} finished happily"
280 self.overlay.show(
281 [
282 ("SHUTDOWN", 3.0, (1.0, 0.4, 0.4, 1.0)),
283 (finished, 1.2, theme.WHITE),
284 (f"{stats.ragequits} rage-quits", 1.2, (0.95, 0.7, 0.55, 1.0)),
285 ("Press R to boot again", 1.0, (0.6, 0.95, 0.8, 1.0)),
286 ]
287 )
288
289 def _on_button(self, action: str) -> None:
290 if action == "restart":
291 self._shutdown_at = None
292 self.overlay.hide()
293 self.stage.restart()
294 elif action == "quit":
295 self.app.quit()
296
297
298def _draw_centred_text(renderer, text: str, cx: float, y: float, *, scale: float, colour) -> None:
299 """Draw *text* horizontally centred on *cx*, measured from the font's own advances."""
300 renderer.draw_text(text, (cx - measure_text_width(text, scale) * 0.5, y), scale=scale, colour=colour)
301
302
303def main() -> None:
304 if "--test" in sys.argv:
305 from harness import run_harness
306
307 run_harness()
308 return
309 app = App(width=DESIGN_WIDTH, height=DESIGN_HEIGHT, title="You're the OS (SimVX)")
310 app.run(OsRoot())
311
312
313if __name__ == "__main__":
314 main()