Architecture¶
Mini Arcade is a monorepo with independently published packages that evolve together.
The core idea is a simulation-first engine with backend adapters, scene-driven gameplay, and an explicit render/capture pipeline.
Mental model¶
At runtime:
GameConfigdefines the initial scene, backend, fps, virtual resolution, and postfx.SceneRegistrydiscovers and registers scene factories.Gamebuilds managers and runtime services around a selected backend.EngineRunnerexecutes the frame loop.Scenes produce
RenderPacketobjects; render passes consumeFramePacketwrappers.Capture records input streams (replay) and optional video frames.
Package map¶
mini-arcadeUser-facing package: CLI + runner modules for launching games/examples.mini-arcade-coreEngine runtime: scenes, systems, loop, commands, rendering, services, spaces.mini-arcade-pygame-backendBackendprotocol implementation using pygame.mini-arcade-native-backendBackendprotocol implementation using native SDL2 (pybind11).
flowchart TD
subgraph Content[Apps and Content]
G[Games]
E[Examples]
end
subgraph Product[mini-arcade]
CLI[CLI and Runner]
end
subgraph Core[mini-arcade-core]
CFG[GameConfig]
GAME[Game]
MGR[Managers]
SVC[RuntimeServices]
LOOP[EngineRunner]
SCN[SceneAdapter + SceneRegistry]
RP[RenderPipeline]
CAP[CaptureService]
end
subgraph Backends[Backends]
PYG[pygame]
NAT[native SDL2]
end
G --> CLI
E --> CLI
CLI --> CFG
CFG --> GAME
GAME --> MGR
GAME --> SVC
GAME --> LOOP
MGR --> SCN
LOOP --> SCN
LOOP --> RP
LOOP --> CAP
GAME --> PYG
GAME --> NAT
PYG --> SVC
NAT --> SVC
Core runtime objects¶
Game¶
Game is the composition root for a running session. It wires:
managers (
cheats,command_queue,scenes)runtime services (
window,audio,files,capture,input,render,scenes)postfx registry and stack
loop hooks (
DefaultGameHooks)
Managers¶
SceneAdapter: stack operations (change,push,pop,remove_scene,quit)CommandQueue: tick-level command outboxCheatManager: key-sequence matcher that enqueues commands
Runtime services¶
RuntimeServices exposes ports/adapters for:
window
audio
files
capture
input
render
scene queries
Scenes and systems¶
Scenes are SimScene subclasses. A scene tick:
builds a typed tick context (
BaseTickContext-derived)runs
SystemPipeline.step(ctx)must set
ctx.packetand return aRenderPacket
The world state is scene-owned (scene.world), while side effects are emitted through commands/services.
Frame lifecycle (actual loop order)¶
EngineRunner.run() performs this order per frame:
Poll backend events.
Apply loop hooks (
DefaultGameHooks) for resize/debug hotkeys.Build
InputFramefrom events, or consume replay input if replay playback is active.If quit is requested, stop loop.
Resolve input-focused scene (
input_entry).Tick update scenes:
input scene receives full
InputFrameother updating scenes receive neutral input for this frame
Build
CommandContext(services + managers + settings + resolved world).Process cheats and enqueue commands.
Drain and execute command queue.
Build visible
FramePacketlist from scene stack.Build
RenderContextand runRenderPipelinepasses.Record video frame if capture is active.
Sleep to honor target fps.
Emit profiler report (if enabled) and increment
frame_index.
On exit, scene stack is cleaned (scenes.clean()).
sequenceDiagram
participant CLI as mini-arcade
participant G as Game
participant ER as EngineRunner
participant BK as Backend
participant SC as SceneAdapter
participant CQ as CommandQueue
participant RP as RenderPipeline
participant CP as CaptureService
CLI->>G: build config + registry + backend
G->>ER: run loop
loop each frame
ER->>BK: poll events
ER->>ER: hooks.on_events(events)
ER->>ER: build InputFrame (or replay input)
ER->>SC: tick update entries
ER->>CQ: cheats enqueue commands
ER->>CQ: drain and execute
ER->>SC: collect visible FramePackets
ER->>RP: render_frame(backend, context, packets)
ER->>CP: record_video_frame(frame_index)
end
ER->>SC: clean scene stack
Scene stack policy¶
Scene stack behavior is policy-driven (ScenePolicy on each stack entry):
render visibility: render from highest opaque scene upward
update propagation: top-down until a scene blocks updates
input routing: top-most eligible scene receives input, respecting blockers
This gives overlays (pause/menu/debug) explicit control over update/input/render behavior.
Render model¶
Rendering is packet-based:
scene tick returns
RenderPacket(ops, meta)runner wraps packets into
FramePacket(scene_id, is_overlay, packet)pipeline executes ordered passes:
BeginFramePassWorldPassLightingPassUIPassPostFXPassEndFramePass
Layered rendering can be driven via packet.meta["pass_ops"] with keys like
world, lighting, ui, effects.
Input, intent, commands¶
The engine separates concerns:
InputAdapterturns backend events intoInputFramesnapshots.scene input systems map raw input to scene-specific intent.
systems mutate world state using intent.
side effects (scene transitions, capture toggles, quit, effect toggles) are emitted as commands.
This keeps gameplay logic testable and mostly backend-agnostic.
Capture and replay¶
CaptureService handles:
screenshots
replay record/playback (
InputFramestream)video frame capture + optional async encoding
Video capture is invoked after render in the frame loop.
Repo layout¶
mini-arcade/
|- packages/
|- games/
|- examples/
`- docs/
Why monorepo¶
one architecture across core, backends, examples, and games
shared tooling and CI
coordinated versioning and releases
docs tied directly to implementation changes