@@ -66,6 +66,9 @@ export type CellString = string;
6666
6767export class GameImpl implements Game {
6868 private _ticks = 0 ;
69+ private singleplayerStartTick : number | null = null ;
70+ private singleplayerSpawnPhaseCompleted = false ;
71+ private spawnPhaseLockedValue : boolean | null = null ;
6972
7073 private unInitExecs : Execution [ ] = [ ] ;
7174
@@ -376,19 +379,63 @@ export class GameImpl implements Game {
376379 this . addUpdate ( { type : GameUpdateType . GamePaused , paused } ) ;
377380 }
378381
379- inSpawnPhase ( ) : boolean {
382+ private computeInSpawnPhase ( ) : boolean {
380383 if ( this . config ( ) . gameConfig ( ) . gameType === GameType . Singleplayer ) {
381- // Singleplayer has no fixed spawn countdown; game starts once the human spawns.
382- return ! this . _humans . every ( ( human ) => this . player ( human . id ) . hasSpawned ( ) ) ;
384+ if ( this . singleplayerSpawnPhaseCompleted ) {
385+ return false ;
386+ }
387+ // Singleplayer has no fixed spawn countdown; game starts once all current human
388+ // players in the game have spawned. This supports tests that add players after
389+ // game construction.
390+ const humanPlayers = this . allPlayers ( ) . filter (
391+ ( player ) => player . type ( ) === PlayerType . Human ,
392+ ) ;
393+ if ( humanPlayers . length > 0 ) {
394+ return humanPlayers . some ( ( player ) => ! player . hasSpawned ( ) ) ;
395+ }
383396 }
384397 return this . _ticks <= this . config ( ) . numSpawnPhaseTurns ( ) ;
385398 }
386399
400+ inSpawnPhase ( ) : boolean {
401+ if ( this . spawnPhaseLockedValue !== null ) {
402+ return this . spawnPhaseLockedValue ;
403+ }
404+ return this . computeInSpawnPhase ( ) ;
405+ }
406+
407+ private updateSingleplayerStartTick ( ) : void {
408+ if ( this . config ( ) . gameConfig ( ) . gameType !== GameType . Singleplayer ) {
409+ return ;
410+ }
411+
412+ const humanPlayers = this . allPlayers ( ) . filter (
413+ ( player ) => player . type ( ) === PlayerType . Human ,
414+ ) ;
415+ const hasSpawnedHuman = humanPlayers . some ( ( player ) => player . hasSpawned ( ) ) ;
416+
417+ if ( this . inSpawnPhase ( ) || ! hasSpawnedHuman ) {
418+ this . singleplayerStartTick = null ;
419+ return ;
420+ }
421+
422+ this . singleplayerStartTick ??= this . ticks ( ) ;
423+ }
424+
387425 ticks ( ) : number {
388426 return this . _ticks ;
389427 }
390428
391429 executeNextTick ( ) : GameUpdates {
430+ this . spawnPhaseLockedValue = this . computeInSpawnPhase ( ) ;
431+ if (
432+ this . config ( ) . gameConfig ( ) . gameType === GameType . Singleplayer &&
433+ ! this . spawnPhaseLockedValue
434+ ) {
435+ this . singleplayerSpawnPhaseCompleted = true ;
436+ }
437+ this . updateSingleplayerStartTick ( ) ;
438+
392439 this . updates = createGameUpdatesMap ( ) ;
393440 this . execs . forEach ( ( e ) => {
394441 if (
@@ -424,6 +471,10 @@ export class GameImpl implements Game {
424471 hash : this . hash ( ) ,
425472 } ) ;
426473 }
474+
475+ this . spawnPhaseLockedValue = null ;
476+ this . updateSingleplayerStartTick ( ) ;
477+
427478 this . _ticks ++ ;
428479 return this . updates ;
429480 }
@@ -731,6 +782,15 @@ export class GameImpl implements Game {
731782 }
732783
733784 public isSpawnImmunityActive ( ) : boolean {
785+ if ( this . config ( ) . gameConfig ( ) . gameType === GameType . Singleplayer ) {
786+ this . updateSingleplayerStartTick ( ) ;
787+ if ( this . inSpawnPhase ( ) ) {
788+ return true ;
789+ }
790+ const startTick = this . singleplayerStartTick ?? this . ticks ( ) ;
791+ return startTick + this . config ( ) . spawnImmunityDuration ( ) > this . ticks ( ) ;
792+ }
793+
734794 return (
735795 this . config ( ) . numSpawnPhaseTurns ( ) +
736796 this . config ( ) . spawnImmunityDuration ( ) >
0 commit comments