From 2b2200c80894d1db9c50b518e85d776b371a31c9 Mon Sep 17 00:00:00 2001 From: Mykola Date: Wed, 19 Nov 2025 01:07:25 +0200 Subject: [PATCH] Improve random spawn (#2465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: After the v27 playtest, some players experienced instant death on spawn. The issue was that the human random spawn occasionally coincided with a bot’s spawn. Previously, bot spawns didn’t account for human spawn locations and could appear on the same tile, now they don’t. ## Please complete the following: - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: nikolaj_mykola --------- Co-authored-by: Evan --- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 3 +++ src/core/execution/BotSpawner.ts | 30 ++++++++++++++++++----- src/core/execution/ExecutionManager.ts | 4 +-- src/core/execution/FakeHumanExecution.ts | 19 ++++++++++++++ src/core/execution/SpawnExecution.ts | 5 ++++ src/core/execution/utils/PlayerSpawner.ts | 17 +++++-------- src/core/game/Game.ts | 2 ++ src/core/game/PlayerImpl.ts | 9 +++++++ 9 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index f1f1b03c6..02dd57b00 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -153,6 +153,7 @@ export interface Config { defensePostRange(): number; SAMCooldown(): number; SiloCooldown(): number; + minDistanceBetweenPlayers(): number; defensePostDefenseBonus(): number; defensePostSpeedBonus(): number; falloutDefenseModifier(percentOfFallout: number): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index df84be07f..2a64a4876 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -618,6 +618,9 @@ export class DefaultConfig implements Config { temporaryEmbargoDuration(): Tick { return 300 * 10; // 5 minutes. } + minDistanceBetweenPlayers(): number { + return 30; + } percentageTilesOwnedToWin(): number { if (this._gameConfig.gameMode === GameMode.Team) { diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index 134a7c666..e1712f99e 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -40,15 +40,33 @@ export class BotSpawner { if (!this.gs.isLand(tile)) { return null; } - for (const spawn of this.bots) { - if (this.gs.manhattanDist(spawn.tile, tile) < 30) { - return null; + + const isOtherPlayerSpawnedNearby = this.gs.allPlayers().some((player) => { + const spawnTile = player.spawnTile(); + + if (spawnTile === undefined) { + return false; } + + return ( + this.gs.manhattanDist(spawnTile, tile) < + this.gs.config().minDistanceBetweenPlayers() + ); + }); + + if (isOtherPlayerSpawnedNearby) { + return null; } - return new SpawnExecution( - new PlayerInfo(botName, PlayerType.Bot, null, this.random.nextID()), - tile, + + const playerInfo = new PlayerInfo( + botName, + PlayerType.Bot, + null, + this.random.nextID(), ); + this.gs.addPlayer(playerInfo).setSpawnTile(tile); + + return new SpawnExecution(playerInfo, tile); } private randomBotName(): string { diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 4a8e5df91..e58906921 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -128,11 +128,11 @@ export class Executor { } } - spawnBots(numBots: number): Execution[] { + spawnBots(numBots: number): SpawnExecution[] { return new BotSpawner(this.mg, this.gameID).spawnBots(numBots); } - spawnPlayers(): Execution[] { + spawnPlayers(): SpawnExecution[] { return new PlayerSpawner(this.mg, this.gameID).spawnPlayers(); } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 4051339fc..ec8c690eb 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -159,6 +159,7 @@ export class FakeHumanExecution implements Execution { console.warn(`cannot spawn ${this.nation.playerInfo.name}`); return; } + this.mg.addPlayer(this.nation.playerInfo).setSpawnTile(rl); this.mg.addExecution(new SpawnExecution(this.nation.playerInfo, rl)); return; } @@ -643,6 +644,24 @@ export class FakeHumanExecution implements Execution { continue; } const tile = this.mg.ref(x, y); + + const isOtherPlayerSpawnedNearby = this.mg.allPlayers().some((player) => { + const spawnTile = player.spawnTile(); + + if (spawnTile === undefined) { + return false; + } + + return ( + this.mg.manhattanDist(spawnTile, tile) < + this.mg.config().minDistanceBetweenPlayers() + ); + }); + + if (isOtherPlayerSpawnedNearby) { + continue; + } + if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) { if ( this.mg.terrainType(tile) === TerrainType.Mountain && diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 57baff6ee..3d3764ed0 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -48,6 +48,11 @@ export class SpawnExecution implements Execution { this.mg.addExecution(new BotExecution(player)); } } + + if (player.spawnTile() === undefined) { + player.setSpawnTile(this.tile); + } + player.setHasSpawned(true); } diff --git a/src/core/execution/utils/PlayerSpawner.ts b/src/core/execution/utils/PlayerSpawner.ts index 29c0fe1a6..f6bbf3a79 100644 --- a/src/core/execution/utils/PlayerSpawner.ts +++ b/src/core/execution/utils/PlayerSpawner.ts @@ -9,7 +9,6 @@ export class PlayerSpawner { private random: PseudoRandom; private players: SpawnExecution[] = []; private static readonly MAX_SPAWN_TRIES = 10_000; - private static readonly MIN_SPAWN_DISTANCE = 30; constructor( private gm: Game, @@ -41,18 +40,13 @@ export class PlayerSpawner { continue; } - let tooCloseToOtherPlayer = false; - for (const spawn of this.players) { - if ( + const isOtherPlayerSpawnedNearby = this.players.some( + (spawn) => this.gm.manhattanDist(spawn.tile, tile) < - PlayerSpawner.MIN_SPAWN_DISTANCE - ) { - tooCloseToOtherPlayer = true; - break; - } - } + this.gm.config().minDistanceBetweenPlayers(), + ); - if (tooCloseToOtherPlayer) { + if (isOtherPlayerSpawnedNearby) { continue; } @@ -75,6 +69,7 @@ export class PlayerSpawner { continue; } + player.setSpawnTile(spawnLand); this.players.push(new SpawnExecution(player.info(), spawnLand)); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 2d868b799..33f01e6df 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -547,6 +547,8 @@ export interface Player { hasSpawned(): boolean; setHasSpawned(hasSpawned: boolean): void; + setSpawnTile(spawnTile: TileRef): void; + spawnTile(): TileRef | undefined; // Territory tiles(): ReadonlySet; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index f85cc7aab..67083f656 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -102,6 +102,7 @@ export class PlayerImpl implements Player { public _outgoingLandAttacks: Attack[] = []; private _hasSpawned = false; + private _spawnTile: TileRef | undefined; private _isDisconnected = false; constructor( @@ -347,6 +348,14 @@ export class PlayerImpl implements Player { this._hasSpawned = hasSpawned; } + setSpawnTile(spawnTile: TileRef): void { + this._spawnTile = spawnTile; + } + + spawnTile(): TileRef | undefined { + return this._spawnTile; + } + incomingAllianceRequests(): AllianceRequest[] { return this.mg.allianceRequests.filter((ar) => ar.recipient() === this); }