Imported from williamcotton/space-trader (
AGENTS.md). Install upstream withnpx skills add williamcotton/space-trader. Copyright stays with the author.
Space Trader Development Notes
Stack
- Electron
- React + TypeScript
- Vite
- Three.js + WebGL canvas +
requestAnimationFrame
Project Layout
Electron Shell
electron/main.tselectron/preload.ts
App Shell
src/main.tsxsrc/App.tsx- thin app controller
- currently resolves to
MatchScreenduring Phase 0
src/App.csssrc/app/types.ts- app-level screen, boot-flow, and result-summary types
src/app/boot.ts- boot-flow parsing and initial-screen resolution
src/app/profileStore.ts- local profile/preferences seam for future menu UX
src/app/resultSummary.ts- future results-screen seam
Match Screen
src/screens/MatchScreen.tsx- extracted gameplay shell
src/GameCanvas.tsx- Three.js canvas mount point
- RAF loop owner
- explicit runtime-ready and renderer-settled signals for automation
Core Runtime
src/game/runtime.ts- lazy runtime accessor via
getGameRuntime() - authoritative mutable game state
- active content-selection + runtime-profile context
- facade over focused runtime controllers in
src/game/runtime/** - HMR-backed runtime persistence after first creation
- lazy runtime accessor via
src/game/runtime/content.ts- content/profile/map driven initial-state creation
src/game/runtime/store.ts- state/transient subscriptions and derived-state cache
src/game/runtime/transients.ts- viewport, hover, animations, and pending targeting state
src/game/runtime/commandExecutor.ts- local command dispatch, rejection handling, and animation synthesis
src/game/runtime/networkSession.ts- network command submission and authoritative command application
src/game/runtime/targeting.ts- board clicks, card targeting, attack targeting, and action helpers
src/game/runtime/automation.ts- auto-flow, bot autoplay, priority stops, and bot worker lifecycle
src/game/runtime/renderFrame.ts- render-frame assembly and renderer stepping
src/game/runtime/devControls.ts- developer/debug commands and screenshot automation controls
src/game/runtime/hotRuntime.ts- dev-window binding and HMR singleton helpers
src/game/systems.tsupdateGame
src/game/types.ts- frame, viewport, animation, and renderer contract types
src/game/derived.ts- cached derived state keyed off runtime version
src/game/presentation.ts- player/resource/faction presentation helpers
Model Layer
src/game/model/state.ts- canonical
GameState - entity shapes
- zones
- initial-state creation
- dynamic player-order bootstrap
- elimination tracking
- resource and runtime-profile aware defaults
- deterministic generated-id counter
- canonical
src/game/model/enums.ts- phases, factions, resources, unit roles
- note: factions/resources are dynamic string ids populated by loaded content
src/game/model/ids.ts- typed IDs and player constants
src/game/model/hex.ts- axial hex math
src/game/model/queries.ts- state query helpers
src/game/model/selectors.ts- UI selectors
src/game/model/migrations.ts- state migration / hot-state repair
src/game/random/seeded.ts- seeded RNG helpers for deterministic match setup
Content System
src/game/content/loader.ts- explicit set loading
- registry reset / reload lifecycle
- default built-in content initialization via
initializeDefaultContent()
src/game/content/sets/catalog.ts- built-in set manifest catalog
- built-in set id lookup
- default built-in selection (
alpha, which depends onfoundation)
src/game/content/registry.ts- registered sets, cards, stack effects, factions, resources, maps, deck recipes, runtime profiles
src/game/content/cards/builders.ts- shared card authoring helpers used across sets
src/game/content/sets/types.ts- set manifests
- faction/resource/runtime-profile module types
src/game/content/sets/foundation/**- cardless shared gameplay foundation
- shared stack effects, play effects, AI scoring, and runtime installers
src/game/content/sets/alpha/**- first real playable set content and installers
- depends on
foundation
Content Facades
src/game/content/cards/catalog.ts- registry-backed card access
- card metadata helpers
src/game/content/cards/types.ts- generic card definition types
- generic play-effect config / modifier shapes
src/game/content/stackEffects.ts- registry-backed stack-effect access
src/game/content/stackEffects/types.ts- generic stack behavior types
src/game/content/decks/starterDecks.ts- starter deck access + validation
src/game/content/maps/catalog.ts- registry-backed map access
src/game/content/mechanics/stateAccess.ts- helper for namespaced mechanic state
Mechanics
src/game/mechanics/index.ts- generic mechanic-state lifecycle
src/game/content/sets/alpha/mechanics/**- current live Alpha mechanics
stealthsproutrelaysurgebloomsalvagebastionpredationemplaceduncounterable
Registries
src/game/registries/triggerConditions.tssrc/game/registries/autoTargets.tssrc/game/registries/instructionHandlers.tssrc/game/registries/playEffects.tssrc/game/registries/cardPlayModifiers.tssrc/game/registries/cardCounterability.tssrc/game/registries/cardResolveAnimations.tssrc/game/registries/directInteraction.tssrc/game/registries/cascadeBranches.tssrc/game/registries/combatHooks.tssrc/game/registries/unitDeployment.tssrc/game/registries/unitStatHooks.tssrc/game/registries/mechanicInstructions.tssrc/game/registries/mechanicAnimations.tssrc/game/registries/mechanicState.tssrc/game/registries/mechanicApis.tssrc/game/registries/spellScoring.tssrc/game/registries/stackEffectMagnitudes.tssrc/game/registries/stackResolveAnimations.tssrc/game/registries/stackPreviews.tssrc/game/registries/boardBlastEffects.tssrc/game/registries/debugStackResponses.tssrc/game/registries/presentation.ts
Actions Pipeline
src/game/actions/commands.tssrc/game/actions/events.tssrc/game/actions/reducers.tssrc/game/actions/instructions.tssrc/game/actions/instructionHandlers.tssrc/game/actions/handlers/cards.tssrc/game/actions/handlers/combat.tssrc/game/actions/handlers/phase.tssrc/game/actions/handlers/selection.ts
Rules
src/game/rules/validators.ts- command legality
src/game/rules/cardPlayOptions.ts- shared legal-target enumeration
src/game/rules/cardPlayLegality.ts- playability/stack targeting helpers
Turn Management
src/game/turn/playerOrder.tssrc/game/turn/phaseMachine.tssrc/game/turn/stack.tssrc/game/turn/autoFlow.tssrc/game/turn/priorityStops.ts
Systems
src/game/systems/combat.tssrc/game/systems/nodeControl.tssrc/game/systems/harvesting.tssrc/game/systems/victory.tssrc/game/systems/keywords.tssrc/game/systems/continuousEffects.tssrc/game/systems/unitStats.tssrc/game/systems/cascade.tssrc/game/systems/replacementEngine.tssrc/game/systems/triggerEngine.ts
AI
src/game/ai/minimaxBot.ts- current default bot entry point
src/game/ai/minimax/**- search, evaluation, generation, and simulation helpers
src/game/ai/minimaxBot.worker.ts- background worker wrapper for live renderer bot decisions
src/game/ai/botDecisionWorkerProtocol.ts- worker message contract
src/game/ai/mvpBot.ts- legacy heuristic bot entry point
src/game/ai/mvpBot/**- shared heuristic helpers and tactical/card-choice modules
Networking
src/network/client.ts- multiplayer session state
- queue / command transport
- SSE subscription and resync
src/network/protocol.ts- shared client-side transport shapes
src/network/useMultiplayerSnapshot.ts- React subscription hook
Server
server/src/index.ts- Node server entry point
server/src/matchmaker.ts- format-aware queues and match creation
server/src/matchRoom.ts- authoritative room state, player-order fanout, and command handling
server/src/createMatchState.ts- seeded initial match creation
server/src/sessionStore.ts- reconnectable player sessions
server/src/roomStore.ts- live room lookup
server/src/protocol.ts- server transport types
Render
src/game/render/animations.ts- renderer-agnostic animation synthesis and animation-capture helpers
src/game/render3d/layout3d.ts- axial hex to Three.js world-space projection helpers
- orthographic camera layout used by renderer and screenshot automation
src/game/render3d/renderer.ts- sole board renderer
- Three.js scene, camera, picking, board, entities, overlays, and 3D animation drawing
- renderer-owned camera intro/victory effects and renderer-settled automation state
UI
src/ui/GameHudPanels.tsxsrc/ui/GameTopBar.tsxsrc/ui/HandTray.tsxsrc/ui/CommandStackPanel.tsxsrc/ui/ResourceIcon.tsxsrc/ui/useGameSnapshot.ts
Docs
game-design.mdarchitecture.mdinstructions.mdlaunch-screen-plan.mdnetworked-multiplayer-feature.mdfour-player-refactor.mdthree-player-feature.mdattack-refactor.mdlayers-refactor.mdfaction-identity.mdnew-cards.mdfaction-refactor.md
Commands
npm run devnpm run dev:direct-matchnpm run dev:screenshotsnpm run buildnpm run previewnpm run typechecknpm testnpm run test:watchnpm run server:buildnpm run server:start
Introduction Screenshots
To regenerate the annotated tutorial screenshots in docs/introduction/:
- Start the screenshot dev server in one terminal:
npm run dev:screenshots- boots directly into gameplay without enabling developer controls
npm run dev:direct-matchis still available for debug/dev gameplay and enables developer controls such as bot toggles, forced wins, and unit kills
- In another terminal:
npx tsx scripts/capture-introduction-screenshots.ts
The script launches a Playwright Chromium browser, injects specific game states for each tutorial step, draws SVG arrow annotations pointing to key UI elements, and saves screenshots to docs/introduction/.
Automation contract:
- the game runtime is exposed on
window.__gameRuntimein dev mode - screenshot/dev automation controls are exposed on
window.__gameRuntimeDevControlsin dev mode - gameplay readiness is exposed on
window.__spaceTraderRuntimeReady - Three.js renderer settled state is exposed on
window.__spaceTraderRendererSettled - screenshot automation should wait for the explicit ready and renderer-settled markers, not only for
__gameRuntimeexistence
If the UI layout, hex grid rendering, or HUD components change, re-run the script and verify the 12 output images.
Current Architecture Decisions
- App-level boot and screen state now live above gameplay in:
src/App.tsxsrc/app/boot.tssrc/app/types.ts
src/screens/MatchScreen.tsxowns the current gameplay shell.- Phase 0 intentionally preserves the old player-visible behavior:
- app still lands directly in a match
- current multiplayer controls still live in-match
- menu/setup/results screens are planned but not shipped yet
- Future boot-policy changes should flow through
src/app/boot.ts, not scattered env checks in gameplay components. - Canonical gameplay state lives in
src/game/runtime.ts, not React state. - Gameplay mutations flow through:
- commands
- events
- instructions
- triggers
- Live phase loop:
starteconomymaintacticalenddiscard
- Current live economy defaults:
- the starting player starts with
2 currency + 2 primary - non-starting players start with
5 currency + 2 primary - deposits are
2 - passive economy is
+1 currency
- the starting player starts with
- Player identity is runtime-seat based:
state.playerOrderdefines turn / priority seatingstate.eliminatedPlayerIdsremoves players from live rotation- 1v1 remains
player_1,player_2 - 3-player FFA uses
player_1throughplayer_3 - 4-player FFA uses
player_1throughplayer_4
- Continuous effects are layered and authoritative for stat/keyword changes.
- Mechanic-owned state lives under
state.mechanicStatein:matchturnresolution
- Content is explicitly loaded through
content/loader.ts. - Built-in set manifests are selected through
content/sets/catalog.ts. - Default built-in content is
alpha, which pulls in the cardlessfoundationdependency. - Shared reusable gameplay primitives live in
foundation; cards, decks, maps, and runtime profiles live in real sets such asalpha. - Runtime can now be created or reset from explicit content bundles through:
loadConfiguredContentSets(...)createConfiguredRuntime(...)GameRuntime.resetWithContent(...)
- Runtime creation is lazy:
- importing
src/game/runtime.tsno longer creates a live match - the first
getGameRuntime()call instantiates the runtime - this avoids hidden-match boot when non-gameplay app code imports runtime-adjacent modules
- importing
- Runtime defaults come from registered runtime profiles, not kernel constants.
- Networked multiplayer is server-authoritative command replay, not client-authoritative state sync.
- Online matchmaking is format-aware:
pvp_1v1usesalpha_defaultffa_3pusesalpha_three_playerffa_4pusesalpha_four_player
- Current live Alpha runtime profiles:
alpha_defaultonfrontier_beltalpha_three_playeronfrontier_triadalpha_four_playeronfrontier_crossroads
- Resource modules now own:
kindsuch as currency vs primary- display order
- glyph data
- theme data
- Card resolution is data-driven through:
- card play metadata in
catalog.ts - generic stack behavior definitions
- generic play-effect registries
- set-owned installers for AI/animation/preview/debug behaviors
- card play metadata in
StackResolutionRulesis not the mental model anymore.- Generated ids that affect sync must come from stable sources:
- stable card-instance ids where possible
- otherwise
state.nextGeneratedIdCounter
- Never derive authoritative ids from mutable state like
log.length. - Current state version is
26.
Current Faction Mechanics Snapshot
- Alloy:
bastionsalvageemplaced- formation / siege / damaged-matters shell
- Flux:
relaysurge- stack / spellchain / spatial combo shell
- Biomass:
sproutbloompredation- swarm / growth / board-to-resource shell
getGameRuntime Contract
getGameRuntime()returns the same runtime instance for the life of the renderer session.getGameRuntime()is the lazy runtime boundary; do not depend on import-time side effects.- React should subscribe to runtime snapshots, not copy gameplay state into component state.
GameCanvasowns the RAF loop and calls runtime step plumbing with the Three.jsGameRenderer.GameCanvasalso owns the explicit gameplay-ready marker:window.__spaceTraderRuntimeReady = falsebefore mount work beginswindow.__spaceTraderRuntimeReady = trueonce gameplay is mounted and rendering
GameCanvaskeeps RAF active while runtime animations or renderer-owned camera effects are active.- New authoritative gameplay state belongs in
state.tsplusmigrations.ts.
HMR Workflow
- Runtime instance persists through HMR once it exists.
- HMR should not force runtime creation before gameplay mounts.
- Simulation update logic and Three renderer code can be hot-swapped without wiping the match.
- State schema changes should always be accompanied by migration updates.
- Registry-backed content can be reset and reloaded deterministically.
- Server/client multiplayer bugs are often determinism bugs; check ids, seeds, and content parity before assuming transport failure.
- Online hidden information is still trust-based: clients can reconstruct hidden zones from deterministic setup and command replay. Secure hidden-state views are a separate future project.
Current Extension Points
- new content sets through
CardSetmanifests - new runtime installers through
installers - new mechanics through set-owned mechanic modules
- new resources/factions/maps/decks/runtime profiles through the content registry
- new effect families through generic play-effect / stack-effect registrations
- new AI/animation/preview behaviors through registries instead of kernel switches
Architectural Pressure Points
These still imply meaningful engine work, not just content:
- graveyard / reanimation / recursion UX and rules
- true token support
- multi-target choice cards
- explicit support for deterministic infinite combos
- secure online hidden-information networking
- full content-context isolation beyond the current process-global registries
- any major change to spell-damage-vs-armor rules