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:

  • EngineConfig defines fps, virtual resolution, postfx, and profiling.

  • SceneConfig defines initial scene and discovery packages.

  • SceneRegistry discovers and registers scene factories from configured packages.

  • Engine builds managers and runtime services around a selected backend.

  • EngineRunner executes the frame loop.

  • Scenes produce RenderPacket objects; render passes consume FramePacket wrappers.

  • Capture records input streams (replay) and optional video frames.

Package map

  • mini-arcade User-facing package: CLI + runner modules for launching games/examples.

  • mini-arcade-core Engine runtime: scenes, systems, loop, commands, rendering, services, spaces.

  • mini-arcade-pygame-backend Backend protocol implementation using pygame.

  • mini-arcade-native-backend Backend protocol 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]
    ECFG[EngineConfig]
    SCFG[SceneConfig]
    GAME[Engine]
    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 --> ECFG
  CLI --> SCFG
  ECFG --> GAME
  SCFG --> 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

Engine

Engine 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 outbox

  • CheatManager: 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:

  1. builds a typed tick context (BaseTickContext-derived)

  2. runs SystemPipeline.step(ctx)

  3. must set ctx.packet and return a RenderPacket

The world state is scene-owned (scene.world), while side effects are emitted through commands/services.

For gameplay scenes, GameScene adds a declarative shell on top of SimScene through GameSceneSystemsConfig. That lets a scene class describe:

  • controls-to-intent wiring

  • pause intent handling

  • one-shot intent-to-command bindings

  • extra gameplay systems

  • render system installation

The system pipeline now distinguishes between:

  • atomic systems: one step(ctx) responsibility

  • system bundles: composition helpers that expand into several atomic systems

For discrete or cell-based games, core also exposes a small grid-gameplay layer:

  • CadenceSystem for fixed-interval simulation ticks

  • GridCoord, GridBounds, and GridLayout for cell-space modeling

  • GridCellSpawnSystem plus occupancy helpers for free-cell spawning

For stacking puzzle games, core also exposes a falling-block layer:

  • BlockBoard for dense board occupancy

  • FallingBlockPieceSpec / FallingBlockPiece for active piece state

  • BoardRowClearSystem for filled-row collapse

  • BagRandomizer for deterministic bag-based piece sequencing

For brick-breaker games, core also exposes a paddle/brick collision layer:

  • ViewportBounceSystem for wall reflection

  • BounceCollisionSystem for ball-vs-rect reflection

  • PaddleBouncePolicy for contact-point based paddle response

  • BrickField / BrickFieldCollisionSystem for brick state and hits

For maze arcade games, core also exposes a maze/lane layer:

  • TileMap for dense maze data

  • GridNavigationSystem for buffered lane movement

  • TunnelWrapSystem for tunnel exits

  • CollectibleField / CollectibleCollisionSystem for pellets and pickups

  • ModeTimerSystem for scatter/chase/frightened style schedules

For bomb-and-arena games, core also exposes a Bomberman-style rules layer:

  • ArenaTile / arena_tile_map_from_strings(...) for arena grids

  • BombPlacementSystem and BombFuseSystem for placement and detonation timing

  • ExplosionField and ExplosionLifetimeSystem for short-lived hazard cells

  • ChainReactionSystem for bomb-to-bomb triggering

  • DestructibleTileSystem and HazardCollisionSystem for arena damage/effects

Entity access follows the same pattern:

  • semantic queries use tags and world helper methods

  • named entity-id domains are reserved for constrained allocation and cleanup

Gameplay scene code is now typically split into four layers:

  • scene.py: orchestration only

  • bootstrap.py: world builders and asset/bootstrap helpers

  • pipeline.py: ordered gameplay system registration

  • systems/ and spawn.py: reusable gameplay processors, bundles, and spawn policies

That keeps on_enter() focused on:

  • resolving runtime config

  • building the world

  • installing the ordered pipeline

Frame lifecycle (actual loop order)

EngineRunner.run() performs this order per frame:

  1. Poll backend events.

  2. Apply loop hooks (DefaultGameHooks) for resize/debug hotkeys.

  3. Build InputFrame from events, or consume replay input if replay playback is active.

  4. If quit is requested, stop loop.

  5. Resolve input-focused scene (input_entry).

  6. Tick update scenes:

    • input scene receives full InputFrame

    • other updating scenes receive neutral input for this frame

  7. Build CommandContext (services + managers + settings + resolved world).

  8. Process cheats and enqueue commands.

  9. Drain and execute command queue.

  10. Build visible FramePacket list from scene stack.

  11. Build RenderContext and run RenderPipeline passes.

  12. Record video frame if capture is active.

  13. Sleep to honor target fps.

  14. 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 Engine
  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:

    • BeginFramePass

    • WorldPass

    • LightingPass

    • UIPass

    • PostFXPass

    • EndFramePass

Layered rendering can be driven via packet.meta["pass_ops"] with keys like world, lighting, ui, effects.

Input, intent, commands

The engine separates concerns:

  • InputAdapter turns backend events into InputFrame snapshots.

  • 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 (InputFrame stream)

  • 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