Skip to content

Architecture

The Quantum Forge Framework is built on three laws that keep games maintainable, testable, and fast.

The Three Laws

  1. Pure functions for logic — Engine coordinates, functions compute
  2. State is the source of truth — Rendering derives, never computes
  3. Engine is a state coordinator — No animation timing, no game logic in engine methods

Four-Layer Pattern

Every game has four layers:

┌──────────────────────────────────┐
│  Controller                       │  Wires everything, handles timing
├──────────────────────────────────┤
│  Renderer                         │  Derives visuals from state
├──────────────────────────────────┤
│  Engine                           │  State coordinator with getHelpers()
├──────────────────────────────────┤
│  Pure Functions                   │  Game rules, no side effects
└──────────────────────────────────┘

Layer 1: Pure Functions

All game logic lives in separate modules as pure functions. Given the same input, they always return the same output. No side effects, no dependencies on engine or renderer.

typescript
// logic/GameRules.ts
export function canMove(player: Player, dx: number, dy: number, bounds: Bounds): boolean {
  return player.x + dx >= 0 && player.x + dx < bounds.width;
}

export function calculateDamage(attacker: Entity, defender: Entity): number {
  return Math.max(0, attacker.power - defender.defense);
}

Layer 2: Engine

The engine is a state coordinator. It holds game state and exposes getHelpers() — functions that update state using the pure logic functions.

typescript
class MyEngine extends Engine<GameState> {
  constructor(logger?: any) {
    super({ player: { x: 0, y: 0 }, score: 0 }, { logger });
  }

  getHelpers() {
    return {
      movePlayer: (dx: number, dy: number) => {
        const state = this.getState();
        if (canMove(state.player, dx, dy, state.bounds)) {
          state.player.x += dx;
          state.player.y += dy;
          this.setState({ ...state });
        }
      },
    };
  }
}

Layer 3: Renderer

Rendering reads state and draws. It never computes positions, caches state, or makes decisions.

typescript
class MyRenderer extends PixiRenderer {
  protected draw(state: GameState) {
    this.graphics.circle(state.player.x, state.player.y, 10).fill("#fff");
    this.drawText("score", `Score: ${state.score}`, 10, 10, { fill: "#fff", fontSize: 16 });
  }
}

Layer 4: Controller

The controller wires engine, renderer, input, and game loop together. Animation timing lives here, not in the engine.

typescript
class MyGame {
  constructor() {
    this.engine = new MyEngine(logger);
    this.renderer = new MyRenderer({ canvas, backgroundColor: 0x000000, logger });
    this.input = new InputManager({ logger });
    this.loop = new GameLoop({
      update: (dt) => this.update(dt),
      render: () => this.renderer.render(this.engine.getState()),
      targetFps: 60,
      logger,
    });
  }

  async start() {
    await this.renderer.init();
    this.loop.start();
  }

  update(dt: number) {
    this.input.poll();
    if (this.input.isActionDown("move-up")) {
      this.engine.getHelpers().movePlayer(0, -100 * dt);
    }
  }
}

Dependency Flow

Game Layer  →  Systems Layer  →  Core Layer  →  External Layer
(your code)    (rendering,       (Engine,        (Quantum Forge,
                input)            EventBus)       Browser APIs)

Top-down only. Core never knows about Game. Systems never know about specific game logic.

Module Structure

DirectoryPurposeRule
core/Engine, EventBus, Logger, QuantumPropertyManagerNo game-specific logic
systems/PixiRenderer, CanvasRenderer, GameLoop, CameraGeneric, game-agnostic
packages/Input, collision, audio, particles, animation, etc.Self-contained, opt-in
src/ (your game)Engine subclass, logic, rendering, controllerUses everything above

Anti-Patterns

typescript
// ❌ Logic in engine methods
class BadEngine extends Engine<State> {
  calculateScore() { /* extract to pure function */ }
}

// ❌ Animation in engine
class BadEngine extends Engine<State> {
  async animateMove() { await sleep(1000); }
}

// ❌ Renderer computes state
class BadRenderer {
  private cache = {};
  render(state) { /* derives from cache, not state */ }
}

// ❌ Math.random() instead of quantum
const result = Math.random() < 0.5 ? 0 : 1; // use QuantumPropertyManager

// ❌ console.log instead of logger
console.log("debug"); // use logger?.debug?.("msg", "Context")

Architecture Validation

Run the validator to check your game follows the patterns:

bash
npm run validate examples/my-game

Checks: engine pattern, pure functions, logging, quantum integration, TypeScript config. Scores 0-100%.

Powered by Quantum Forge