Skip to content

feat: engine extraction & multiplayer vertical slice (Phases 0–3)#30

Merged
launchswitch merged 6 commits into
mainfrom
phase-2a/ai-engine-routing
Jun 1, 2026
Merged

feat: engine extraction & multiplayer vertical slice (Phases 0–3)#30
launchswitch merged 6 commits into
mainfrom
phase-2a/ai-engine-routing

Conversation

@launchswitch
Copy link
Copy Markdown
Owner

Summary

Implements the full engine/view split and a minimal multiplayer vertical slice, as planned in docs/multiplayer-engine-split.md.

Phases completed

Phase Description Key deliverables
0 — PoC Prove the pattern with 3 actions src/game/engine/ directory, EngineResult/EngineEvent types, applyAction() dispatch
1 — Browser action extraction Route all non-combat gameplay actions through engine 24 action types as pure functions; web/src/game/engine/ becomes re-export shim
2A — AI loop routing Route AI gameplay calls through engine exports initAiFactionTurn(), activateAiUnit(), runFactionPhaseAndAdvance() exported; 12 parity tests
2B — Server boundary hardening Make engine server-importable with combat, projection, commands previewCombat()/applyCombat() wrappers, projectStateForPlayer(), validateCommand(), EngineCommand authoritative envelope, discovery tracking, architecture boundary tests
3 — Multiplayer vertical slice Prove server-authoritative play end-to-end WebSocket server (src/server/), MultiplayerRoom with AI drain, fog-filtered broadcasts, reconnect-by-session-token, 6 integration tests

Key files added/modified

  • src/game/engine/ (11 files) — canonical server-importable engine
  • src/server/ (5 files) — WebSocket server, room, protocol, wire codec
  • web/src/game/engine/ — thin re-export shims to src/game/engine/
  • web/src/game/controller/GameSession.ts — now delegates to engine; no direct combat/AI imports
  • src/game/types.tsplayerDiscovery field for authoritative discovery state
  • 6 test files (38 tests) — AI parity, combat parity, command validation, projection, architecture boundaries, multiplayer integration

Review fixes (latest commit)

  • Fix ws.on("error") race: error handler was deleting client metadata before close handler could run cancelPendingCombat, leaving reconnecting players permanently stuck
  • Remove premature clients.clear() from server.close()
  • Consolidate duplicate EngineActionWithoutUndo type into canonical location
  • Safer addSeenCommand (snapshot Set before iterating)
  • Deduplicated EngineCommand construction in handleCommand

Tests

npx vitest run tests/architectureBoundaries.test.ts tests/engineAiParity.test.ts tests/engineCombatParity.test.ts tests/engineCommandValidation.test.ts tests/engineProjection.test.ts tests/multiplayerServer.test.ts
# 6 files, 38 tests — all passing

What's next

Phase 4 (multiplayer product layer) is planned in docs/multiplayer-phase4-plan.md — lobby UI, auth, persistence, notifications, deployment.

No behavior changes

All phases are pure refactors and new infrastructure — zero gameplay mechanics changes.

Phase 2A of the multiplayer engine split. AI gameplay mutations now
flow through the engine API instead of GameSession calling src/ systems
directly.

New engine exports:
- initAiFactionTurn() — compute strategy + unit ordering
- activateAiUnit() — single-unit AI activation
- runFactionPhaseAndAdvance() — faction phases + turn advance + fog refresh

Changes:
- Add AiActivationOpts, InitAiFactionTurnResult types (types.ts)
- Fix pendingCombat type to CombatActionPreview | null instead of unknown
- Align GameEngine interface factionId params to string
- Refactor GameSession.processAiTurnChunk to call engine exports
- Remove direct imports: computeFactionStrategy, activateAiUnit (src),
  runFactionPhase, getAiUnitIds, asFactionId from GameSession
- 12 parity tests verifying behavioral equivalence (engineAiParity.test.ts)
- Update plan doc to reflect Phase 2A completion

All 1363 tests pass (0 regressions). 127 AI awareness tests pass.
Move the game engine from web/src/game/engine/ to src/game/engine/ so
a Node server can import it without any web/ dependency. All gameplay
mutations (including combat and AI turns) are now reachable through the
engine API, the client owns presentation timing only, and a per-player
fog-filtered state projection is available for server broadcasts.

Engine module (src/game/engine/):
  - engine.ts — applyAction, previewCombat, applyCombat, activateAiUnit,
    initAiFactionTurn, runFactionPhaseAndAdvance, applyCommand
  - types.ts — EngineResult, EngineEvent, EngineAction, CombatPreviewResult,
    CombatApplyResult, CommandActor, EngineCommand, EngineCommandResult
  - commandValidation.ts — actor/faction/entity ownership validation
  - discovery.ts — authoritative enemy synergy intel tracking
  - discoveryTypes.ts — EnemySynergyIntel, PlayerDiscoveryState types
  - stateProjection.ts — per-player fog-filtered state projection
  - sessionUtils.ts, movementExplorer.ts, moveQueueSession.ts — pure
    helpers moved from web/src/game/controller/

Web/ changes:
  - web/src/game/engine/*, sessionUtils, movementExplorer,
    moveQueueSession, stateAccess — converted to re-export shims
  - GameSession now calls engine combat wrappers instead of
    combatActionSystem directly
  - enemySynergyIntel moved from session private field to authoritative
    state (GameState.playerDiscovery)
  - ReachableHexView type moved to src/game/engine/types.ts

GameState:
  - Added optional playerDiscovery field (ReadonlyMap) for discovery state
  - Added getResearchProgress, isResearchNodeCompleted to stateAccess

Tests (13 new):
  - tests/engineCombatParity.test.ts (2 tests)
  - tests/engineCommandValidation.test.ts (5 tests)
  - tests/engineProjection.test.ts (5 tests)
  - Architecture boundary test: src/game/engine/ has no web/ imports

All 1377 tests pass. Zero behavior changes — pure refactor.
Server-authoritative WebSocket multiplayer with one in-memory game room.
Two human players with explicit faction assignments, AI factions drain
on the server between human turns, fog-filtered state broadcasts.

New files:
- src/server/protocol.ts — client/server WebSocket message types + guard
- src/server/wire.ts — recursive Map/Set codec for JSON transport
- src/server/room.ts — authoritative room state machine + AI drain
- src/server/wsServer.ts — WebSocket adapter + connection lifecycle
- src/server/index.ts — dev entrypoint with env defaults
- tests/multiplayerServer.test.ts — 6 integration tests

Protocol:
- Client sends commands (validated actor identity, server constructs
  EngineCommand.actor from authenticated seat)
- Two-phase combat: preview → client confirms → apply
- AI turns drain automatically after human end_turn
- Reconnect-by-session-token for dev testing

Verification: 6/6 integration tests pass, 1377+ existing tests pass,
clean build, server starts and shuts down gracefully.
…leanup + multiplayer smoke test

- Update seat's playerId on reconnect so broadcasts find the correct socket
- Clean up old playerId from playerFactionIds map
- Cancel pending combat when a player disconnects
- Add comprehensive multiplayer smoke test (Phase A: data pipeline, Phase B: command pipeline)
- Gate heavyRegenPercent in resolveStatus.ts with !ctx.attackerIsRanged
  and !ctx.isNavalAttacker checks
- Update card description to 'Infantry units regenerate 30% of combat
  damage dealt as HP.'
…ion, cleanup

- Fix ws.on('error') deleting client metadata before close handler could
  run cancelPendingCombat, which left players permanently stuck with
  pending combat on reconnect
- Remove clients.clear() from server.close() so async close events can
  complete cleanup properly
- Consolidate duplicate EngineActionWithoutUndo type into canonical
  src/game/engine/types.ts (was defined identically in room.ts and
  protocol.ts)
- Deduplicate EngineCommand construction in room.ts handleCommand
- Safer addSeenCommand (snapshot Set to array before iterating)
- Simplify getNextOpenFactionId with Array.find()
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
9tribes Error Error Jun 1, 2026 3:34am

@launchswitch launchswitch merged commit b3a51d8 into main Jun 1, 2026
2 of 3 checks passed
@launchswitch launchswitch deleted the phase-2a/ai-engine-routing branch June 1, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant