Engineering documentation

Technical Documentation

Starwatch Commander is a browser game built for clarity and testability. Game rules live entirely apart from the UI, constants are centralized, and every cycle resolves through one deterministic function.

On this page

Tech stack

React + TypeScript

Typed component UI for menus, panels, modals, and HUD. Strict types across the whole rules layer.

Vite

Dev server and production bundler. npm run dev to play locally, npm run build to ship.

Phaser 3

Renders the full-screen galaxy map — planets, ownership rings, stationed ships, fleets in flight, and route lines.

Vitest

Fast unit tests for rules, production, travel, combat, AI, and pacing.

Zustand-style store

A single game store holds state; UI subscribes, rules mutate through resolution.

localStorage / IndexedDB

Client-side saves for the MVP — no backend required to play solo.

Architecture & layout

The golden rule: rules never live in components. The Phaser scene and React panels only read state and dispatch intents; all mutation happens in the rules layer, funneled through cycle resolution.

src/
  game/
    rules/        constants, combat, movement, production,
                  research, ai, aiDifficulty, victory,
                  turnResolution, galaxyGeneration, garrison, …
    state/        types, gameStore, createNewGame, saveLoad,
                  matchTemplates
    phaser/       GalaxyScene.ts  (map rendering only)
  ui/
    screens/      Home, New Game, Command Center, Results, …
    components/   BattleViewer, CombatModal, BottomBar,
                  BuildingSlot, planet panel, news ticker
    styles/       settings.css and theming
    audio.ts      synthesized SFX / ambient (no audio assets)
  docs/           in-repo design docs (00–10)

Constants and the planet catalog are centralized so a single edit re-balances the game everywhere. Provisional values (e.g., unverified original-game production numbers) are explicitly flagged confirmed: false so they never masquerade as canonical in UI or docs.

Core data model

The entire match is one serializable GameState object — which is what makes saves and deterministic tests trivial.

type GameState = {
  id: string;
  mode: 'standard_single_player' | 'pass_and_play'
      | 'online_private' | 'quick_match' | 'sandbox';
  worldSize: 'tiny'|'small'|'medium'|'large'|'huge';
  gamePace: 'turn_based' | 'timed_cycles';
  cycleNumber: number;
  players: Player[];
  planets: Planet[];
  travelingShips: TravelingShips[];
  newsEvents: NewsEvent[];
  winnerId?: string;
  phase: 'home'|'setup'|'playing'|'combat'|'game_over';
};

Planets carry their catalog identity (rating, multipliers, tech affinity, slots) plus live state (owner, ships, fractional ship progress, buildings, focus). Fleets in flight are first-class TravelingShips with origin, destination, ships, and cycles remaining — which is exactly why they can be drawn on the map instead of hidden in a menu.

Cycle resolution

One central function advances the whole galaxy one cycle, in a fixed order. Keeping this in a single place is what keeps the game deterministic and testable — nothing sneaks a rule into a render pass.

  1. Complete pending buildings
  2. Produce gold
  3. Produce ships (accumulating fractional progress)
  4. Produce research (Tech Progress)
  5. Advance Tech Level if the threshold is met
  6. Move traveling fleets one cycle closer
  7. Resolve arrivals
  8. Resolve captures & combat
  9. Check eliminations (homeworld captured?)
  10. Check victory / defeat
  11. Generate news events
  12. Autosave, then advance to the next cycle / player

Rules math reference

The numbers that define the game, all sourced from a central constants.ts:

ConstantValue
Starting gold1,100
Homeworld starting ships16
Factory cost200 gold
Tech Lab cost250 gold
Tech Level range0 – 15
Research per lab per cycle1 unit
Base travel range1 hop (+1 / Tech)
Base travel speed1 hop/cy (+0.5 / Tech)
Combat win-chance clamp10% – 90%
Combat tech slope0.4 / 15 per level
Homeworld defense bonus+3 effective Tech
Planet slots4 – 10
getMaxTravelRange(tech) = 1 + tech               // hops
getTravelSpeed(tech)    = 1 + tech × 0.5          // hops/cycle
travelCycles            = ceil(distanceHops / speed)
attackerWinChance = clamp(0.5 + techDiff × (0.4/15), 0.10, 0.90)

Saves & persistence

Because a match is one serializable object, saving is just persisting GameState. The MVP uses client-side storage (localStorage / IndexedDB): autosave fires after every cycle and before Save & Exit. Save cards surface world size, pace, cycle number, owned planets, total ships, Tech Level, and last-played time, with continue / rename / delete actions.

Testing

The rules layer is covered by a Vitest suite — 192 passing tests as of the latest balance pass — spanning:

The latest pass reports a clean tsc -b, a successful production build, and 192/192 green.

Engineering principles

See what's changed →