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

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.

  • 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.pressed("primary") and friends, declared once in the root’s input_context class attribute, so they are in force wherever the scene runs, web export included.

  • 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).

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

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.

285

harness.py

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

132

nodes/__init__.py

Port of You’re the OS: node modules.

1

nodes/stage.py

StageNode: the playfield.

392

nodes/state.py

Pure-Python game-state model.

754

nodes/theme.py

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