You’re the OS¶

schedule processes, swap memory pages, and keep the user calm.

â–¶ Run in browser

Upstream: 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.

  • R restarts, Escape quits; 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.py is 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. StageNode is a single Node2D marked dynamic = True; its on_draw paints 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 = True and update_mode = UpdateMode.ALWAYS on itself; StageNode sets UpdateMode.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’s on_draw runs before any child that keeps a non-negative z_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’s input_actions class attribute so the web export picks them up too.

  • Signals for cross-node news. The stage emits game_over_changed once; 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.py drives the real input pipeline with InputSimulator and captures seven stages headlessly.

Files¶

  • main.py: OsRoot scene, the title/shutdown overlay card, bottom controls strip

  • nodes/state.py: the simulation (state machine, pages, swaps, I/O, scoring)

  • nodes/stage.py: StageNode, all layout, drawing and input hit-testing

  • nodes/theme.py: colour palette and the ASCII faces standing in for upstream’s emoji

  • harness.py: scripted seven-stage screenshot capture

  • screenshots/: 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

main.py

You’re the OS: schedule processes, swap memory pages, and keep the user calm.

314

harness.py

Scripted-input harness: drives the port headlessly and captures seven stages.

133

nodes/__init__.py

Port of You’re the OS: node modules.

1

nodes/stage.py

StageNode: the playfield.

425

nodes/state.py

Pure-Python game-state model.

754

nodes/theme.py

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