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>
This commit is contained in:
Mykola
2026-02-22 15:51:05 +00:00
committed by GitHub
parent 7c6c2b1fd8
commit 097c42740c
3 changed files with 60 additions and 19 deletions
+29 -11
View File
@@ -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;
+27 -4
View File
@@ -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(
+4 -4
View File
@@ -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);
});
});