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¶
Run it with the example library installed; the export runs in a copy made by simvx examples copy ports_youre_the_os:
simvx examples run ports_youre_the_os # interactive
simvx export web main.py \
-o 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.pressed("primary")and friends, declared once in the root’sinput_contextclass attribute, so they are in force wherever the scene runs, web export included.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).
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 emoji
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. |
285 |
|
Scripted-input harness: drives the port headlessly and captures seven stages. |
132 |
|
Port of You’re the OS: node modules. |
1 |
|
StageNode: the playfield. |
392 |
|
Pure-Python game-state model. |
754 |
|
Colour palette and process-face glyph map. |
80 |
Source¶
1# /// simvx
2# tags = ["port", "tier-2"]
3# upstream = "https://github.com/plbrault/youre-the-os"
4# web = { width = 1280, height = 720, responsive = true }
5# ///
6"""You're the OS: schedule processes, swap memory pages, and keep the user calm.
7
8A port of Pier-Luc Brault's "You're the OS!". You are the operating system: put
9processes on one of four CPUs before they starve, swap their memory pages
10between RAM and disk to clear page faults, and deliver queued I/O events. Ten
11starved processes and the user rage-quits: shutdown.
12
13The simulation is plain Python with no engine imports (``nodes/state.py``), and
14one dynamic ``Node2D`` draws all 42 process slots and ~180 page slots in
15``on_draw``. Input is polled through named input actions, the title screen is
16just ``tree.paused`` plus an ``UpdateMode``, and game-over reaches the root as a
17Signal.
18
19Controls: click a process to move it on or off a CPU, click or drag across pages
20to request and cancel swaps, Space or a click on the I/O box delivers events, R
21restarts, Escape quits.
22
23Run:
24 simvx examples run ports_youre_the_os
25"""
26
27from __future__ import annotations
28
29from nodes import theme # noqa: E402
30from nodes.stage import DESIGN_HEIGHT, DESIGN_WIDTH, StageNode # noqa: E402
31
32from simvx.core import Node2D, Signal, UpdateMode # noqa: E402
33from simvx.core.input.controls import Key, Mouse
34from simvx.core.input.state import Input # noqa: E402
35from simvx.core.math.types import Vec2 # noqa: E402
36from simvx.core.text import measure_text_width # noqa: E402
37from simvx.graphics import App # noqa: E402
38
39# Bottom controls strip: port-UX baseline
40STRIP_HEIGHT = 70
41BUTTON_W = 130
42BUTTON_H = 44
43BUTTON_GAP = 18
44
45HINT = "Click a process to schedule it - Space delivers I/O"
46HINT_SCALE = 0.75
47HINT_MARGIN = 16
48
49SHUTDOWN_DELAY_S = 1.0 # let the last ragequit land before the overlay covers it
50
51PANEL_PAD = 36
52PANEL_LINE_GAP = 22
53TITLE_LINES = [
54 ("YOU'RE THE OS", 3.0, (0.6, 0.85, 1.0, 1.0)),
55 ("Click a process to put it on a CPU. Click it again to take it off.", 1.0, theme.WHITE),
56 ("Click or drag across memory pages to swap them between RAM and disk.", 1.0, theme.WHITE),
57 ("Press Space (or click the I/O box) to deliver waiting I/O events.", 1.0, theme.WHITE),
58 ("Let ten processes starve and the user rage-quits.", 1.0, (0.85, 0.75, 0.55, 1.0)),
59 ("Click anywhere or press Space to boot", 1.3, (0.6, 0.95, 0.8, 1.0)),
60]
61
62
63class _Button:
64 def __init__(self, label: str, action: str) -> None:
65 self.label = label
66 self.action = action
67 self.x = 0.0
68 self.y = 0.0
69 self.w = BUTTON_W
70 self.h = BUTTON_H
71 self.hovered = False
72 self.pressed = False
73
74 def contains(self, mp: Vec2) -> bool:
75 return (
76 self.x - self.w * 0.5 <= mp.x <= self.x + self.w * 0.5
77 and self.y - self.h * 0.5 <= mp.y <= self.y + self.h * 0.5
78 )
79
80
81class _OverlayCard(Node2D):
82 """Dimmed panel of centred text: the title card and the shutdown notice.
83
84 Added to the root *after* the stage so it paints on top of it. A node's own
85 on_draw runs before any child that keeps a non-negative ``z_index``, so the
86 root itself can only paint what belongs underneath the playfield.
87 """
88
89 dynamic = True
90
91 def __init__(self, **kwargs) -> None:
92 super().__init__(**kwargs)
93 self.lines: list[tuple[str, float, tuple[float, float, float, float]]] = []
94
95 def show(self, lines) -> None:
96 self.lines = list(lines)
97
98 def hide(self) -> None:
99 self.lines = []
100
101 def on_draw(self, renderer) -> None:
102 if not self.lines:
103 return
104 vw, vh = self.tree.screen_size
105 renderer.rect((0, 0), (vw, vh), fill=(0.0, 0.0, 0.0, 0.86))
106
107 # Panel sized to the text it holds, so the copy stays legible over the
108 # playfield at any viewport size.
109 line_heights = [26 * scale + PANEL_LINE_GAP for _, scale, _ in self.lines]
110 panel_w = max(measure_text_width(text, scale) for text, scale, _ in self.lines) + 2 * PANEL_PAD
111 panel_h = sum(line_heights) - PANEL_LINE_GAP + 2 * PANEL_PAD
112 top = (vh - panel_h) * 0.5 + PANEL_PAD
113 panel = ((vw - panel_w) * 0.5, top - PANEL_PAD)
114 renderer.rect(panel, (panel_w, panel_h), fill=(0.04, 0.05, 0.08, 0.98))
115 renderer.rect(panel, (panel_w, panel_h), stroke=(0.30, 0.42, 0.58, 1.0))
116
117 y = top
118 for (text, scale, colour), line_h in zip(self.lines, line_heights, strict=True):
119 _draw_centred_text(renderer, text, vw * 0.5, y, scale=scale, colour=colour)
120 y += line_h
121
122
123class OsRoot(Node2D):
124 """Root scene: background, the Stage, the overlay card, and the bottom controls strip."""
125
126 input_context = {
127 "primary": [Mouse.LEFT],
128 "secondary": [Mouse.RIGHT],
129 "deliver_io": [Key.SPACE],
130 "restart": [Key.R],
131 "quit": [Key.ESCAPE],
132 }
133
134 button_pressed = Signal()
135
136 def on_ready(self) -> None:
137 # The title card sits on a paused tree, so the root has to keep ticking
138 # to notice the click that boots the machine. StageNode opts back into
139 # PAUSABLE so only this node stays live behind the card.
140 self.update_mode = UpdateMode.ALWAYS
141
142 self.stage = StageNode()
143 self.add_child(self.stage)
144 self.stage.game_over_changed.connect(self._on_game_over)
145 self._shutdown_at: float | None = None
146
147 # Boot into the title card: the playfield is laid out behind it but its
148 # simulation stays frozen until the player starts the machine.
149 self.started = False
150 self.overlay = self.add_child(_OverlayCard(name="Overlay"))
151 self.overlay.show(TITLE_LINES)
152 self.tree.paused = True
153
154 # Bottom controls
155 self.buttons: list[_Button] = [
156 _Button("Restart", "restart"),
157 _Button("Quit", "quit"),
158 ]
159 self._press_target: _Button | None = None
160 self.button_pressed.connect(self._on_button)
161
162 # The root's on_draw renders button hover and press state, which lives in
163 # plain attributes mutated from on_update rather than in Properties, so
164 # none of those writes dirty this node and its retained draws would go
165 # stale. Mark the root dynamic: its small on_draw (background, strip and
166 # two buttons) re-captures every frame while the heavy stage stays
167 # retained.
168 self.dynamic = True
169
170 # ------------------------------------------------------------------ tick
171 def on_update(self, dt: float) -> None:
172 if Input.just_pressed("quit"):
173 self._on_button("quit")
174 return
175
176 if not self.started:
177 if Input.just_pressed("primary") or Input.just_pressed("deliver_io"):
178 self._start()
179 return
180
181 # Layout buttons across the bottom strip
182 vw, vh = self.viewport_size.x, self.viewport_size.y
183 n = len(self.buttons)
184 total_w = n * BUTTON_W + (n - 1) * BUTTON_GAP
185 x0 = (vw - total_w) * 0.5 + BUTTON_W * 0.5
186 y = vh - STRIP_HEIGHT * 0.5
187 for i, b in enumerate(self.buttons):
188 b.x = x0 + i * (BUTTON_W + BUTTON_GAP)
189 b.y = y
190
191 mp = Input.mouse_position
192 for b in self.buttons:
193 b.hovered = b.contains(mp)
194 b.pressed = b.hovered and Input.pressed("primary") and self._press_target is b
195 if Input.just_pressed("primary"):
196 for b in self.buttons:
197 if b.contains(mp):
198 self._press_target = b
199 break
200 if Input.just_released("primary"):
201 if self._press_target and self._press_target.contains(mp):
202 self.button_pressed.emit(self._press_target.action)
203 self._press_target = None
204
205 if Input.just_pressed("restart"):
206 self._on_button("restart")
207
208 # Let the last rage-quit land on screen before the notice covers it.
209 if self._shutdown_at is not None and self.tree.now - self._shutdown_at > SHUTDOWN_DELAY_S:
210 self._show_shutdown()
211
212 # ------------------------------------------------------------------ draw
213 def on_draw(self, renderer) -> None:
214 vw, vh = self.viewport_size.x, self.viewport_size.y
215
216 # Dark background
217 renderer.rect((0, 0), (vw, vh), fill=theme.DARK_BG)
218
219 if not self.started:
220 # The title card child paints over the playfield; the controls strip
221 # would only show through it.
222 return
223
224 # Bottom strip background
225 strip_y = vh - STRIP_HEIGHT
226 renderer.rect((0, strip_y), (vw, STRIP_HEIGHT), fill=(0.78, 0.80, 0.83, 1.0))
227 renderer.rect((0, strip_y), (vw, 1), fill=(0.55, 0.58, 0.62, 1.0))
228 # Hint sits left of the centred buttons, and yields to them on a window
229 # too narrow to hold both.
230 hint_end = HINT_MARGIN + measure_text_width(HINT, HINT_SCALE) + HINT_MARGIN
231 if hint_end < min(b.x - b.w * 0.5 for b in self.buttons):
232 renderer.text(HINT, (HINT_MARGIN, strip_y + 28), fill=(0.06, 0.08, 0.12, 1.0), scale=HINT_SCALE)
233 for b in self.buttons:
234 base = (0.92, 0.94, 0.96, 1.0)
235 if b.pressed:
236 base = (0.55, 0.65, 0.85, 1.0)
237 elif b.hovered:
238 base = (0.86, 0.92, 0.99, 1.0)
239 renderer.rect((b.x - b.w * 0.5, b.y - b.h * 0.5), (b.w, b.h), fill=base)
240 renderer.rect((b.x - b.w * 0.5, b.y - b.h * 0.5), (b.w, b.h), stroke=(0.3, 0.35, 0.4, 1.0))
241 _draw_centred_text(renderer, b.label, b.x, b.y - 8, scale=0.85, colour=(0.05, 0.07, 0.11, 1.0))
242
243 # ---------------------------------------------------------------- glue
244 def _start(self) -> None:
245 self.started = True
246 self.overlay.hide()
247 self.tree.paused = False
248
249 def _on_game_over(self) -> None:
250 """The stage announced a shutdown: note when, so the notice can follow it."""
251 self._shutdown_at = self.tree.now
252
253 def _show_shutdown(self) -> None:
254 stats = self.stage.state.stats
255 finished = f"{stats.score} process{'' if stats.score == 1 else 'es'} finished happily"
256 self.overlay.show(
257 [
258 ("SHUTDOWN", 3.0, (1.0, 0.4, 0.4, 1.0)),
259 (finished, 1.2, theme.WHITE),
260 (f"{stats.ragequits} rage-quits", 1.2, (0.95, 0.7, 0.55, 1.0)),
261 ("Press R to boot again", 1.0, (0.6, 0.95, 0.8, 1.0)),
262 ]
263 )
264
265 def _on_button(self, action: str) -> None:
266 if action == "restart":
267 self._shutdown_at = None
268 self.overlay.hide()
269 self.stage.restart()
270 elif action == "quit":
271 self.app.quit()
272
273
274def _draw_centred_text(renderer, text: str, cx: float, y: float, *, scale: float, colour) -> None:
275 """Draw *text* horizontally centred on *cx*, measured from the font's own advances."""
276 renderer.text(text, (cx - measure_text_width(text, scale) * 0.5, y), fill=colour, scale=scale)
277
278
279def main() -> None:
280 app = App(width=DESIGN_WIDTH, height=DESIGN_HEIGHT, title="You're the OS (SimVX)")
281 app.run(OsRoot())
282
283
284if __name__ == "__main__":
285 main()