Architecture
The Quantum Forge Framework is built on three laws that keep games maintainable, testable, and fast.
The Three Laws
- Pure functions for logic — Engine coordinates, functions compute
- State is the source of truth — Rendering derives, never computes
- 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.
// 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.
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.
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.
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
| Directory | Purpose | Rule |
|---|---|---|
core/ | Engine, EventBus, Logger, QuantumPropertyManager | No game-specific logic |
systems/ | PixiRenderer, CanvasRenderer, GameLoop, Camera | Generic, game-agnostic |
packages/ | Input, collision, audio, particles, animation, etc. | Self-contained, opt-in |
src/ (your game) | Engine subclass, logic, rendering, controller | Uses everything above |
Anti-Patterns
// ❌ 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:
npm run validate examples/my-gameChecks: engine pattern, pure functions, logging, quantum integration, TypeScript config. Scores 0-100%.