11import { DurableObject } from 'cloudflare:workers' ;
2- import type {
3- GameState , Ship , C2S , S2C , AstrogationOrder , ShipMovement ,
4- } from '../shared/types' ;
5- import { computeCourse } from '../shared/movement' ;
2+ import type { GameState , C2S , S2C , AstrogationOrder } from '../shared/types' ;
63import { getSolarSystemMap , SCENARIOS , findBaseHex } from '../shared/map-data' ;
7- import { SHIP_STATS , INACTIVITY_TIMEOUT_MS } from '../shared/constants' ;
4+ import { INACTIVITY_TIMEOUT_MS } from '../shared/constants' ;
5+ import { createGame , processAstrogation } from '../shared/game-engine' ;
86
97export interface Env {
108 ASSETS : Fetcher ;
@@ -16,19 +14,7 @@ export class GameDO extends DurableObject {
1614 super ( ctx , env ) ;
1715 }
1816
19- // --- Helpers for WebSocket tag-based player tracking ---
20-
21- private getPlayerSockets ( ) : Map < number , WebSocket > {
22- const result = new Map < number , WebSocket > ( ) ;
23- for ( const ws of this . ctx . getWebSockets ( ) ) {
24- const tag = this . ctx . getTags ( ws ) . find ( t => t . startsWith ( 'player:' ) ) ;
25- if ( tag ) {
26- const id = parseInt ( tag . split ( ':' ) [ 1 ] ) ;
27- result . set ( id , ws ) ;
28- }
29- }
30- return result ;
31- }
17+ // --- WebSocket tag-based player tracking ---
3218
3319 private getPlayerId ( ws : WebSocket ) : number | null {
3420 const tag = this . ctx . getTags ( ws ) . find ( t => t . startsWith ( 'player:' ) ) ;
@@ -40,7 +26,7 @@ export class GameDO extends DurableObject {
4026 + this . ctx . getWebSockets ( 'player:1' ) . length ;
4127 }
4228
43- // --- State management via DO storage ---
29+ // --- State management ---
4430
4531 private async getGameState ( ) : Promise < GameState | null > {
4632 return await this . ctx . storage . get < GameState > ( 'gameState' ) ?? null ;
@@ -66,42 +52,34 @@ export class GameDO extends DurableObject {
6652 return new Response ( 'Expected WebSocket' , { status : 426 } ) ;
6753 }
6854
69- // Extract code from URL
7055 const url = new URL ( request . url ) ;
7156 const codeMatch = url . pathname . match ( / \/ w s \/ ( [ A - Z 0 - 9 ] { 5 } ) / ) ;
7257 if ( codeMatch ) {
7358 await this . setGameCode ( codeMatch [ 1 ] ) ;
7459 }
7560
76- // Count current players
7761 const playerCount = this . getPlayerCount ( ) ;
7862 if ( playerCount >= 2 ) {
7963 return new Response ( 'Game is full' , { status : 409 } ) ;
8064 }
8165
82- // Assign next available player ID
8366 const playerId = this . ctx . getWebSockets ( 'player:0' ) . length === 0 ? 0 : 1 ;
8467
8568 const pair = new WebSocketPair ( ) ;
8669 const [ client , server ] = Object . values ( pair ) ;
8770
88- // Accept with a tag so we can identify this player after hibernation
8971 this . ctx . acceptWebSocket ( server , [ `player:${ playerId } ` ] ) ;
9072
91- // Send welcome
9273 const code = await this . getGameCode ( ) ;
9374 this . send ( server , { type : 'welcome' , playerId, code } ) ;
9475
95- // Check if both players are now connected
96- // +1 because the just-accepted socket may not yet appear in getWebSockets
76+ // Both players connected — start the game
9777 if ( playerCount + 1 >= 2 ) {
9878 this . broadcast ( { type : 'matchFound' } ) ;
9979 await this . initGame ( ) ;
10080 }
10181
102- // Set inactivity alarm
10382 await this . ctx . storage . setAlarm ( Date . now ( ) + INACTIVITY_TIMEOUT_MS ) ;
104-
10583 return new Response ( null , { status : 101 , webSocket : client } ) ;
10684 }
10785
@@ -119,7 +97,6 @@ export class GameDO extends DurableObject {
11997 const playerId = this . getPlayerId ( ws ) ;
12098 if ( playerId === null ) return ;
12199
122- // Reset inactivity alarm
123100 await this . ctx . storage . setAlarm ( Date . now ( ) + INACTIVITY_TIMEOUT_MS ) ;
124101
125102 switch ( msg . type ) {
@@ -135,7 +112,7 @@ export class GameDO extends DurableObject {
135112 }
136113 }
137114
138- async webSocketClose ( ws : WebSocket ) : Promise < void > {
115+ async webSocketClose ( ) : Promise < void > {
139116 this . broadcast ( { type : 'opponentDisconnected' } ) ;
140117 }
141118
@@ -146,46 +123,14 @@ export class GameDO extends DurableObject {
146123 await this . ctx . storage . deleteAll ( ) ;
147124 }
148125
149- // --- Game logic ---
126+ // --- Game logic (delegates to engine) ---
150127
151128 private async initGame ( ) {
152129 const scenario = SCENARIOS . biplanetary ;
153130 const map = getSolarSystemMap ( ) ;
154131 const code = await this . getGameCode ( ) ;
155132
156- const ships : Ship [ ] = [ ] ;
157- for ( let p = 0 ; p < scenario . players . length ; p ++ ) {
158- for ( let s = 0 ; s < scenario . players [ p ] . ships . length ; s ++ ) {
159- const def = scenario . players [ p ] . ships [ s ] ;
160- const stats = SHIP_STATS [ def . type ] ;
161- const baseHex = findBaseHex ( map , p === 0 ? 'Mars' : 'Venus' ) ;
162- ships . push ( {
163- id : `p${ p } s${ s } ` ,
164- type : def . type ,
165- owner : p ,
166- position : baseHex ?? def . position ,
167- velocity : { ...def . velocity } ,
168- fuel : stats . fuel ,
169- landed : true ,
170- destroyed : false ,
171- } ) ;
172- }
173- }
174-
175- const gameState : GameState = {
176- gameId : code ,
177- scenario : scenario . name ,
178- turnNumber : 1 ,
179- phase : 'astrogation' ,
180- activePlayer : 0 ,
181- ships,
182- players : [
183- { connected : true , ready : true , targetBody : scenario . players [ 0 ] . targetBody } ,
184- { connected : true , ready : true , targetBody : scenario . players [ 1 ] . targetBody } ,
185- ] ,
186- winner : null ,
187- winReason : null ,
188- } ;
133+ const gameState = createGame ( scenario , map , code , findBaseHex ) ;
189134
190135 await this . saveGameState ( gameState ) ;
191136 this . broadcast ( { type : 'gameStart' , state : gameState } ) ;
@@ -195,118 +140,36 @@ export class GameDO extends DurableObject {
195140 const gameState = await this . getGameState ( ) ;
196141 if ( ! gameState ) return ;
197142
198- if ( gameState . phase !== 'astrogation' ) {
199- this . send ( ws , { type : 'error' , message : 'Not in astrogation phase' } ) ;
200- return ;
201- }
202- if ( playerId !== gameState . activePlayer ) {
203- this . send ( ws , { type : 'error' , message : 'Not your turn' } ) ;
204- return ;
205- }
206-
207143 const map = getSolarSystemMap ( ) ;
208- const movements : ShipMovement [ ] = [ ] ;
209-
210- for ( const ship of gameState . ships ) {
211- if ( ship . owner !== playerId ) continue ;
212- if ( ship . destroyed ) continue ;
213-
214- const order = orders . find ( o => o . shipId === ship . id ) ;
215- const burn = order ?. burn ?? null ;
216-
217- // Validate burn
218- if ( burn !== null ) {
219- if ( burn < 0 || burn > 5 ) {
220- this . send ( ws , { type : 'error' , message : 'Invalid burn direction' } ) ;
221- return ;
222- }
223- if ( ship . fuel <= 0 ) {
224- this . send ( ws , { type : 'error' , message : 'No fuel remaining' } ) ;
225- return ;
226- }
227- }
228-
229- const course = computeCourse ( ship , burn , map ) ;
230- movements . push ( {
231- shipId : ship . id ,
232- from : { ...ship . position } ,
233- to : course . destination ,
234- path : course . path ,
235- newVelocity : course . newVelocity ,
236- fuelSpent : course . fuelSpent ,
237- gravityEffects : course . gravityEffects ,
238- crashed : course . crashed ,
239- landedAt : course . landedAt ,
240- } ) ;
241-
242- // Update ship
243- ship . position = course . destination ;
244- ship . velocity = course . newVelocity ;
245- ship . fuel -= course . fuelSpent ;
246- ship . landed = course . landedAt !== null ;
247-
248- if ( course . landedAt ) {
249- // Landing: velocity resets to zero (ship docks / sets down)
250- ship . velocity = { dq : 0 , dr : 0 } ;
251- }
252-
253- if ( course . crashed ) {
254- ship . destroyed = true ;
255- ship . velocity = { dq : 0 , dr : 0 } ;
256- }
257- }
144+ const result = processAstrogation ( gameState , playerId , orders , map ) ;
258145
259- // Check victory: landing on target body
260- for ( const ship of gameState . ships ) {
261- if ( ship . destroyed || ! ship . landed ) continue ;
262- const targetBody = gameState . players [ ship . owner ] . targetBody ;
263- const hex = map . hexes . get ( `${ ship . position . q } ,${ ship . position . r } ` ) ;
264- if ( hex ?. base ?. bodyName === targetBody || hex ?. body ?. name === targetBody ) {
265- gameState . winner = ship . owner ;
266- gameState . winReason = `Landed on ${ targetBody } !` ;
267- gameState . phase = 'gameOver' ;
268- }
269- }
270-
271- // Check loss: all ships destroyed
272- for ( let p = 0 ; p < 2 ; p ++ ) {
273- const alive = gameState . ships . filter ( s => s . owner === p && ! s . destroyed ) ;
274- if ( alive . length === 0 ) {
275- gameState . winner = 1 - p ;
276- gameState . winReason = `Opponent's ship was destroyed!` ;
277- gameState . phase = 'gameOver' ;
278- }
146+ if ( 'error' in result ) {
147+ this . send ( ws , { type : 'error' , message : result . error } ) ;
148+ return ;
279149 }
280150
281151 // Broadcast movement results
282- this . broadcast ( { type : 'movementResult' , movements, state : gameState } ) ;
152+ this . broadcast ( { type : 'movementResult' , movements : result . movements , state : result . state } ) ;
283153
284- if ( gameState . phase === 'gameOver' ) {
154+ if ( result . state . phase === 'gameOver' ) {
285155 this . broadcast ( {
286156 type : 'gameOver' ,
287- winner : gameState . winner ! ,
288- reason : gameState . winReason ! ,
157+ winner : result . state . winner ! ,
158+ reason : result . state . winReason ! ,
289159 } ) ;
290- await this . saveGameState ( gameState ) ;
291- return ;
292160 }
293161
294- // Switch active player
295- gameState . activePlayer = 1 - gameState . activePlayer ;
296- if ( gameState . activePlayer === 0 ) {
297- gameState . turnNumber ++ ;
298- }
162+ await this . saveGameState ( result . state ) ;
299163
300- await this . saveGameState ( gameState ) ;
301- this . broadcast ( { type : 'stateUpdate' , state : gameState } ) ;
164+ if ( result . state . phase !== 'gameOver' ) {
165+ this . broadcast ( { type : 'stateUpdate' , state : result . state } ) ;
166+ }
302167 }
303168
304- // --- Messaging helpers ---
169+ // --- Messaging ---
305170
306171 private send ( ws : WebSocket , msg : S2C ) {
307- try {
308- ws . send ( JSON . stringify ( msg ) ) ;
309- } catch { }
172+ try { ws . send ( JSON . stringify ( msg ) ) ; } catch { }
310173 }
311174
312175 private broadcast ( msg : S2C ) {
0 commit comments