feat: engine extraction & multiplayer vertical slice (Phases 0–3)#30
Merged
Conversation
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()
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the full engine/view split and a minimal multiplayer vertical slice, as planned in
docs/multiplayer-engine-split.md.Phases completed
src/game/engine/directory,EngineResult/EngineEventtypes,applyAction()dispatchweb/src/game/engine/becomes re-export shiminitAiFactionTurn(),activateAiUnit(),runFactionPhaseAndAdvance()exported; 12 parity testspreviewCombat()/applyCombat()wrappers,projectStateForPlayer(),validateCommand(),EngineCommandauthoritative envelope, discovery tracking, architecture boundary testssrc/server/),MultiplayerRoomwith AI drain, fog-filtered broadcasts, reconnect-by-session-token, 6 integration testsKey files added/modified
src/game/engine/(11 files) — canonical server-importable enginesrc/server/(5 files) — WebSocket server, room, protocol, wire codecweb/src/game/engine/— thin re-export shims tosrc/game/engine/web/src/game/controller/GameSession.ts— now delegates to engine; no direct combat/AI importssrc/game/types.ts—playerDiscoveryfield for authoritative discovery stateReview fixes (latest commit)
ws.on("error")race: error handler was deleting client metadata before close handler could runcancelPendingCombat, leaving reconnecting players permanently stuckclients.clear()fromserver.close()EngineActionWithoutUndotype into canonical locationaddSeenCommand(snapshot Set before iterating)EngineCommandconstruction inhandleCommandTests
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.