You’re the OS

OS simulation, process state machine, CPUs, RAM/disk swap, I/O queue.

▶ Run in browser

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