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