Codebase Architecture

This guide provides an overview of the martini-kit SDK architecture to help contributors understand how the codebase is organized.

Core Philosophy

martini-kit SDK is built on four key principles:

  1. Host-Authoritative - The host (or server) runs the game logic; clients mirror the state
  2. Declarative API - Define your game’s state and actions, not the networking layer
  3. Transport-Agnostic - Works with any network backend (P2P, WebSockets, etc.)
  4. Engine-Agnostic - Core package works with any rendering engine

Package Overview

@martini-kit/core

Purpose: The foundation of the multiplayer system

Key Responsibilities:

  • State synchronization via diff/patch algorithm
  • Action system (definition, application, routing)
  • Player lifecycle management
  • Transport abstraction
  • Deterministic random number generation
  • Logging infrastructure

Main Files:

@martini-kit/phaser

Purpose: Integration layer for Phaser 3

Key Responsibilities:

  • Sprite tracking and synchronization
  • Input management (keyboard, mouse, touch)
  • Physics behavior profiles
  • Collision detection wiring
  • UI/HUD helpers
  • Scene lifecycle integration

Main Files:

@martini-kit/transport-*

Purpose: Network transport implementations

Available Transports:

  • transport-local - In-memory transport for testing (0ms latency)
  • transport-iframe-bridge - Parent-iframe communication for IDE (~1ms latency)
  • transport-trystero - P2P WebRTC using Trystero (20-100ms latency)
  • transport-ws - WebSocket transport for server-based games
  • transport-colyseus - Colyseus server integration

All transports implement the same Transport interface from @martini-kit/core.

@martini-kit/devtools

Purpose: Development and debugging tools

Key Features:

  • State snapshot capture
  • Action history tracking
  • Performance metrics
  • Memory usage monitoring

Main Files:

@martini-kit/ide

Purpose: In-browser IDE for live coding

Key Features:

  • Code editor with TypeScript support
  • Live preview with Phaser games
  • State visualization
  • Hot module reloading

Built With:

  • Svelte for UI
  • CodeMirror for code editing
  • ESBuild-WASM + import maps for code execution

@martini-kit/demos

Purpose: Documentation site and example games

Key Features:

  • Documentation (MDsveX + Shiki)
  • Example games (Fire & Ice, Paddle Battle, etc.)
  • Interactive demos
  • Recipe code snippets

Architecture Patterns

1. Host-Authoritative Pattern

┌──────────────────┐         ┌──────────────────┐
│  Host (Player 1) │         │ Client (Player 2)│
│                  │         │                  │
│ ┌──────────────┐ │         │ ┌──────────────┐ │
│ │ Game Runtime │ │         │ │ Game Runtime │ │
│ │ (Authority)  │ │         │ │  (Mirror)    │ │
│ └──────┬───────┘ │         │ └──────▲───────┘ │
│        │         │         │        │         │
│        │ Actions │         │        │ State   │
│        │ Applied │         │        │ Patches │
│        ▼         │         │        │         │
│ ┌──────────────┐ │◄────────┼────────┼─────────┤
│ │  Transport   │ │  State  │ ┌──────┴───────┐ │
│ └──────────────┘ │  Sync   │ │  Transport   │ │
└──────────────────┘         │ └──────────────┘ │
                             └──────────────────┘

Key Points:

  • Host runs game logic and applies actions
  • Host generates state diffs (patches)
  • Clients receive and apply patches
  • Clients mirror host state exactly

2. Action Flow

1. Player submits action via InputManager

2. Action sent to host via Transport

3. Host applies action to state (GameRuntime)

4. Host generates state diff (sync.ts)

5. Host broadcasts patches to all clients

6. Clients apply patches to their state

7. UI re-renders with updated state

3. State Synchronization

The diff/patch algorithm minimizes bandwidth:

// Example state change
Old State: { players: { p1: { x: 100, y: 200 } } }
New State: { players: { p1: { x: 150, y: 200 }, p2: { x: 50, y: 100 } } }

// Generated patches (minimal)
[
  { op: 'replace', path: ['players', 'p1', 'x'], value: 150 },
  { op: 'add', path: ['players', 'p2'], value: { x: 50, y: 100 } }
]

4. Sprite Tracking (Phaser)

1. Host creates sprite and calls adapter.trackSprite(sprite, id)

2. Adapter stores sprite data in state._sprites[id]

3. State sync broadcasts sprite data

4. Client adapter receives state update

5. Client creates/updates sprites from state._sprites

6. Optional interpolation for smooth movement

Build System

Turborepo

The project uses Turborepo for:

  • Dependency-aware builds - Packages build in correct order
  • Caching - Builds are cached and reused
  • Parallel execution - Independent tasks run concurrently

Configuration: turbo.json

TypeScript

Each package has its own tsconfig.json:

  • Strict mode enabled for type safety
  • ES modules output format
  • Declaration files generated for library packages

Build Tools

  • esbuild - Fast bundling for libraries
  • Vite - Dev server and bundling for demos
  • Vitest - Testing framework

Testing Strategy

Unit Tests

Located in __tests__ directories within each package:

// Example test structure
import { describe, it, expect } from 'vitest';
import { GameRuntime } from '../GameRuntime';

describe('GameRuntime', () => {
  it('should initialize state correctly', () => {
    // Test implementation
  });
});

Integration Tests

Test multiple packages working together:

  • Use LocalTransport for deterministic testing
  • Create multi-player scenarios
  • Test state synchronization

Manual Testing

Use the demos and IDE:

  • Test with real browsers
  • Multi-tab testing for multiplayer
  • Performance profiling

Release Process

The project uses semantic versioning:

MAJOR.MINOR.PATCH
e.g., 2.1.0

Version Bumps:

  • MAJOR: Breaking changes
  • MINOR: New features (backwards compatible)
  • PATCH: Bug fixes

Key Design Decisions

Why Host-Authoritative?

Pros:

  • Simpler than deterministic lockstep
  • Works with existing physics engines
  • Easier to debug and test
  • Less susceptible to cheating

Cons:

  • Host has advantage (0ms latency)
  • Requires good interpolation for smooth client experience

Why Transport-Agnostic?

Different use cases need different transports:

  • P2P for casual games (no server costs)
  • WebSocket for competitive games (lower latency, anti-cheat)
  • Local for testing and development

Why Engine-Agnostic Core?

Allows integration with any game engine:

  • Phaser (current)
  • Unity (planned)
  • Godot (planned)
  • Three.js (planned)
  • Custom engines

Code Organization Principles

  1. Separation of Concerns

    • Core has no rendering logic
    • Phaser adapter has no networking logic
    • Transports are pure communication
  2. Type Safety

    • Strict TypeScript everywhere
    • Generics for state typing
    • Explicit error handling
  3. Minimal Dependencies

    • Core has zero dependencies
    • Each package minimizes its dependency tree
  4. Progressive Enhancement

    • Core provides primitives
    • Helpers add convenience
    • Adapters add integration

Next Steps


Have questions about the architecture? Open a discussion on GitHub!