From 097c42740c559c435a65ba8d31a4ab8fc0168a60 Mon Sep 17 00:00:00 2001 From: Mykola Date: Sun, 22 Feb 2026 15:51:05 +0000 Subject: [PATCH] Random spawn. Avoid spawning near water. (#3009) ## Description: Fixing https://discord.com/channels/1359946986937258015/1360078040222142564/1463898386854973642 Now, if not all tiles on the spawn circle can be owned, the algorithm tries to select another random spawn tile. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [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: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- src/core/execution/SpawnExecution.ts | 40 +++++++++++++++------ src/core/execution/Util.ts | 31 +++++++++++++--- tests/core/execution/SpawnExecution.test.ts | 8 ++--- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 4162e85fc..d8e5586ab 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -7,6 +7,8 @@ import { BotExecution } from "./BotExecution"; import { PlayerExecution } from "./PlayerExecution"; import { getSpawnTiles } from "./Util"; +type Spawn = { center: TileRef; tiles: TileRef[] }; + export class SpawnExecution implements Execution { private random: PseudoRandom; active: boolean = true; @@ -47,15 +49,15 @@ export class SpawnExecution implements Execution { return; } - this.tile ??= this.randomSpawnLand(); + const spawn = this.getSpawn(this.tile); - if (this.tile === undefined) { + if (!spawn) { console.warn(`SpawnExecution: cannot spawn ${this.playerInfo.name}`); return; } player.tiles().forEach((t) => player.relinquish(t)); - getSpawnTiles(this.mg, this.tile).forEach((t) => { + spawn.tiles.forEach((t) => { player.conquer(t); }); @@ -66,7 +68,7 @@ export class SpawnExecution implements Execution { } } - player.setSpawnTile(this.tile); + player.setSpawnTile(spawn.center); } isActive(): boolean { @@ -77,18 +79,28 @@ export class SpawnExecution implements Execution { return true; } - private randomSpawnLand(): TileRef | undefined { + private getSpawn(center?: TileRef): Spawn | undefined { + if (center !== undefined) { + const tiles = getSpawnTiles(this.mg, center, false); + + if (!tiles.length) { + return; + } + + return { center, tiles }; + } + let tries = 0; while (tries < SpawnExecution.MAX_SPAWN_TRIES) { tries++; - const tile = this.randTile(); + const center = this.randTile(); if ( - !this.mg.isLand(tile) || - this.mg.hasOwner(tile) || - this.mg.isBorder(tile) + !this.mg.isLand(center) || + this.mg.hasOwner(center) || + this.mg.isBorder(center) ) { continue; } @@ -104,7 +116,7 @@ export class SpawnExecution implements Execution { } return ( - this.mg.manhattanDist(spawnTile, tile) < + this.mg.manhattanDist(spawnTile, center) < this.mg.config().minDistanceBetweenPlayers() ); }); @@ -113,7 +125,13 @@ export class SpawnExecution implements Execution { continue; } - return tile; + const tiles = getSpawnTiles(this.mg, center, true); + if (!tiles) { + // if some of the spawn tile is outside of the land, we want to find another spawn tile + continue; + } + + return { center, tiles }; } return; diff --git a/src/core/execution/Util.ts b/src/core/execution/Util.ts index ed33836f5..790be42c9 100644 --- a/src/core/execution/Util.ts +++ b/src/core/execution/Util.ts @@ -126,11 +126,34 @@ export function listNukeBreakAlliance( return playersToBreakAllianceWith; } +export function getSpawnTiles( + gm: GameMap, + tile: TileRef, + requireAllValid: true, +): TileRef[] | null; +export function getSpawnTiles( + gm: GameMap, + tile: TileRef, + requireAllValid?: false, +): TileRef[]; +export function getSpawnTiles( + gm: GameMap, + tile: TileRef, + requireAllValid = false, +): TileRef[] | null { + const spawnTiles = Array.from(gm.bfs(tile, euclDistFN(tile, 4, true))); -export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] { - return Array.from(gm.bfs(tile, euclDistFN(tile, 4, true))).filter( - (t) => !gm.hasOwner(t) && gm.isLand(t), - ); + const isInvalid = (t: TileRef) => gm.hasOwner(t) || !gm.isLand(t); + + if (!requireAllValid) { + return spawnTiles.filter((t) => !isInvalid(t)); + } + + if (spawnTiles.some(isInvalid)) { + return null; + } + + return spawnTiles; } export function closestTile( diff --git a/tests/core/execution/SpawnExecution.test.ts b/tests/core/execution/SpawnExecution.test.ts index d9108acea..aa9508495 100644 --- a/tests/core/execution/SpawnExecution.test.ts +++ b/tests/core/execution/SpawnExecution.test.ts @@ -98,15 +98,15 @@ describe("Spawn execution", () => { const game = await setup("half_land_half_ocean", undefined, [playerInfo]); - game.addExecution(new SpawnExecution("game_id", playerInfo, 50)); - game.addExecution(new SpawnExecution("game_id", playerInfo, 60)); + game.addExecution(new SpawnExecution("game_id", playerInfo, 10)); + game.addExecution(new SpawnExecution("game_id", playerInfo, 20)); while (game.inSpawnPhase()) { game.executeNextTick(); } - expect(game.playerByClientID("client_id")?.spawnTile()).toBe(60); + expect(game.playerByClientID("client_id")?.spawnTile()).toBe(20); // Previous territory from first spawn should be relinquished - expect(game.owner(50).isPlayer()).toBe(false); + expect(game.owner(10).isPlayer()).toBe(false); }); });