From 611254727362efc4bdb869e6662e98d95e8a286c Mon Sep 17 00:00:00 2001 From: Mykola Date: Sat, 20 Dec 2025 23:35:30 +0200 Subject: [PATCH] Improve random spawn (#2503) ## Description: This is a previously approved PR with an additional commit that fixes case when nations change spawn & jump around, their previous territory wasn't getting deleted. ## 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: Evan --- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 3 + src/core/execution/BotSpawner.ts | 43 ++----- src/core/execution/ExecutionManager.ts | 6 +- src/core/execution/NationExecution.ts | 54 ++------- src/core/execution/SpawnExecution.ts | 78 ++++++++++-- src/core/execution/utils/PlayerSpawner.ts | 65 +--------- src/core/game/Game.ts | 3 +- src/core/game/PlayerImpl.ts | 12 +- tests/Attack.test.ts | 16 ++- tests/AttackStats.test.ts | 10 +- tests/DeleteUnitExecution.test.ts | 14 ++- tests/Disconnected.test.ts | 10 +- tests/Donate.test.ts | 21 ++-- tests/MissileSilo.test.ts | 8 +- tests/TerritoryCapture.test.ts | 4 +- tests/core/execution/SpawnExecution.test.ts | 112 ++++++++++++++++++ .../execution/utils/PlayerSpawner.test.ts | 72 ----------- .../executions/SAMLauncherExecution.test.ts | 16 ++- tests/core/game/GameImpl.test.ts | 14 ++- tests/economy/ConstructionGold.test.ts | 6 +- tests/nukes/HydrogenAndMirv.test.ts | 6 +- 22 files changed, 320 insertions(+), 254 deletions(-) create mode 100644 tests/core/execution/SpawnExecution.test.ts delete mode 100644 tests/core/execution/utils/PlayerSpawner.test.ts diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index e29069cc2..8d12e7450 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -154,6 +154,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 26c9025c0..cd0f0544a 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -615,6 +615,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 9c77bad38..8eb82505b 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -1,5 +1,4 @@ import { Game, PlayerInfo, PlayerType } from "../game/Game"; -import { TileRef } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; import { GameID } from "../Schemas"; import { simpleHash } from "../Util"; @@ -17,46 +16,29 @@ export class BotSpawner { constructor( private gs: Game, - gameID: GameID, + private gameID: GameID, ) { this.random = new PseudoRandom(simpleHash(gameID)); } spawnBots(numBots: number): SpawnExecution[] { - let tries = 0; - while (this.bots.length < numBots) { - if (tries > 10000) { - console.log("too many retries while spawning bots, giving up"); - return this.bots; - } + for (let i = 0; i < numBots; i++) { const candidate = this.nextCandidateName(); const spawn = this.spawnBot(candidate.name); - if (spawn !== null) { - // Only use candidate name once bot successfully spawned - if (candidate.source === "list") { - this.nameIndex++; - } - this.bots.push(spawn); - } else { - tries++; + + if (candidate.source === "list") { + this.nameIndex++; } + this.bots.push(spawn); } + return this.bots; } - spawnBot(botName: string): SpawnExecution | null { - const tile = this.randTile(); - if (!this.gs.isLand(tile)) { - return null; - } - for (const spawn of this.bots) { - if (this.gs.manhattanDist(spawn.tile, tile) < 30) { - return null; - } - } + spawnBot(botName: string): SpawnExecution { return new SpawnExecution( + this.gameID, new PlayerInfo(botName, PlayerType.Bot, null, this.random.nextID()), - tile, ); } @@ -97,11 +79,4 @@ export class BotSpawner { const suffixNumber = this.random.nextInt(1, 10001); return `Elf ${suffixNumber}`; } - - private randTile(): TileRef { - return this.gs.ref( - this.random.nextInt(0, this.gs.width()), - this.random.nextInt(0, this.gs.height()), - ); - } } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index e0e93f764..5e3914011 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -69,7 +69,7 @@ export class Executor { case "move_warship": return new MoveWarshipExecution(player, intent.unitId, intent.tile); case "spawn": - return new SpawnExecution(player.info(), intent.tile); + return new SpawnExecution(this.gameID, player.info(), intent.tile); case "boat": return new TransportShipExecution( player, @@ -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/NationExecution.ts b/src/core/execution/NationExecution.ts index 98c0b4faa..4a836eec2 100644 --- a/src/core/execution/NationExecution.ts +++ b/src/core/execution/NationExecution.ts @@ -9,7 +9,6 @@ import { PlayerID, PlayerType, Relation, - TerrainType, Tick, Unit, UnitType, @@ -54,7 +53,7 @@ export class NationExecution implements Execution { private trackedTradeShips: Set = new Set(); constructor( - gameID: GameID, + private gameID: GameID, private nation: Nation, // Nation contains PlayerInfo with PlayerType.Nation ) { this.random = new PseudoRandom( @@ -72,6 +71,12 @@ export class NationExecution implements Execution { if (this.random.chance(10)) { // this.isTraitor = true } + + if (!this.mg.hasPlayer(this.nation.playerInfo.id)) { + this.player = this.mg.addPlayer(this.nation.playerInfo); + } else { + this.player = this.mg.player(this.nation.playerInfo.id); + } } tick(ticks: number) { @@ -89,23 +94,15 @@ export class NationExecution implements Execution { return; } - if (this.mg.inSpawnPhase()) { - const rl = this.randomSpawnLand(); - if (rl === null) { - console.warn(`cannot spawn ${this.nation.playerInfo.name}`); - return; - } - this.mg.addExecution(new SpawnExecution(this.nation.playerInfo, rl)); + if (this.player === null) { return; } - if (this.player === null) { - this.player = - this.mg.players().find((p) => p.id() === this.nation.playerInfo.id) ?? - null; - if (this.player === null) { - return; - } + if (this.mg.inSpawnPhase()) { + this.mg.addExecution( + new SpawnExecution(this.gameID, this.nation.playerInfo), + ); + return; } if (!this.player.isAlive()) { @@ -224,31 +221,6 @@ export class NationExecution implements Execution { } } - randomSpawnLand(): TileRef | null { - const delta = 25; - let tries = 0; - while (tries < 50) { - tries++; - const cell = this.nation.spawnCell; - const x = this.random.nextInt(cell.x - delta, cell.x + delta); - const y = this.random.nextInt(cell.y - delta, cell.y + delta); - if (!this.mg.isValidCoord(x, y)) { - continue; - } - const tile = this.mg.ref(x, y); - if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) { - if ( - this.mg.terrainType(tile) === TerrainType.Mountain && - this.random.chance(2) - ) { - continue; - } - return tile; - } - } - return null; - } - private updateRelationsFromEmbargos() { const player = this.player; if (player === null) return; diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 57baff6ee..5f0694fd8 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -1,17 +1,27 @@ import { Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game"; import { TileRef } from "../game/GameMap"; +import { PseudoRandom } from "../PseudoRandom"; +import { GameID } from "../Schemas"; +import { simpleHash } from "../Util"; import { BotExecution } from "./BotExecution"; import { PlayerExecution } from "./PlayerExecution"; import { getSpawnTiles } from "./Util"; export class SpawnExecution implements Execution { + private random: PseudoRandom; active: boolean = true; private mg: Game; + private static readonly MAX_SPAWN_TRIES = 1_000; constructor( + gameID: GameID, private playerInfo: PlayerInfo, - public readonly tile: TileRef, - ) {} + public tile?: TileRef, + ) { + this.random = new PseudoRandom( + simpleHash(playerInfo.id) + simpleHash(gameID), + ); + } init(mg: Game, ticks: number) { this.mg = mg; @@ -20,11 +30,6 @@ export class SpawnExecution implements Execution { tick(ticks: number) { this.active = false; - if (!this.mg.isValidRef(this.tile)) { - console.warn(`SpawnExecution: tile ${this.tile} not valid`); - return; - } - if (!this.mg.inSpawnPhase()) { this.active = false; return; @@ -37,6 +42,13 @@ export class SpawnExecution implements Execution { player = this.mg.addPlayer(this.playerInfo); } + this.tile ??= this.randomSpawnLand(); + + if (this.tile === undefined) { + console.warn(`SpawnExecution: cannot spawn ${this.playerInfo.name}`); + return; + } + player.tiles().forEach((t) => player.relinquish(t)); getSpawnTiles(this.mg, this.tile).forEach((t) => { player.conquer(t); @@ -48,7 +60,8 @@ export class SpawnExecution implements Execution { this.mg.addExecution(new BotExecution(player)); } } - player.setHasSpawned(true); + + player.setSpawnTile(this.tile); } isActive(): boolean { @@ -58,4 +71,53 @@ export class SpawnExecution implements Execution { activeDuringSpawnPhase(): boolean { return true; } + + private randomSpawnLand(): TileRef | undefined { + let tries = 0; + + while (tries < SpawnExecution.MAX_SPAWN_TRIES) { + tries++; + + const tile = this.randTile(); + + if ( + !this.mg.isLand(tile) || + this.mg.hasOwner(tile) || + this.mg.isBorder(tile) + ) { + continue; + } + + const isOtherPlayerSpawnedNearby = this.mg + .allPlayers() + .filter((player) => player.id() !== this.playerInfo.id) + .some((player) => { + const spawnTile = player.spawnTile(); + + if (spawnTile === undefined) { + return false; + } + + return ( + this.mg.manhattanDist(spawnTile, tile) < + this.mg.config().minDistanceBetweenPlayers() + ); + }); + + if (isOtherPlayerSpawnedNearby) { + continue; + } + + return tile; + } + + return; + } + + private randTile(): TileRef { + const x = this.random.nextInt(0, this.mg.width()); + const y = this.random.nextInt(0, this.mg.height()); + + return this.mg.ref(x, y); + } } diff --git a/src/core/execution/utils/PlayerSpawner.ts b/src/core/execution/utils/PlayerSpawner.ts index 29c0fe1a6..58f28e864 100644 --- a/src/core/execution/utils/PlayerSpawner.ts +++ b/src/core/execution/utils/PlayerSpawner.ts @@ -1,66 +1,14 @@ import { Game, PlayerType } from "../../game/Game"; -import { TileRef } from "../../game/GameMap"; -import { PseudoRandom } from "../../PseudoRandom"; import { GameID } from "../../Schemas"; -import { simpleHash } from "../../Util"; import { SpawnExecution } from "../SpawnExecution"; 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, - gameID: GameID, - ) { - this.random = new PseudoRandom(simpleHash(gameID)); - } - - private randTile(): TileRef { - const x = this.random.nextInt(0, this.gm.width()); - const y = this.random.nextInt(0, this.gm.height()); - - return this.gm.ref(x, y); - } - - private randomSpawnLand(): TileRef | null { - let tries = 0; - - while (tries < PlayerSpawner.MAX_SPAWN_TRIES) { - tries++; - - const tile = this.randTile(); - - if ( - !this.gm.isLand(tile) || - this.gm.hasOwner(tile) || - this.gm.isBorder(tile) - ) { - continue; - } - - let tooCloseToOtherPlayer = false; - for (const spawn of this.players) { - if ( - this.gm.manhattanDist(spawn.tile, tile) < - PlayerSpawner.MIN_SPAWN_DISTANCE - ) { - tooCloseToOtherPlayer = true; - break; - } - } - - if (tooCloseToOtherPlayer) { - continue; - } - - return tile; - } - - return null; - } + private gameID: GameID, + ) {} spawnPlayers(): SpawnExecution[] { for (const player of this.gm.allPlayers()) { @@ -68,14 +16,7 @@ export class PlayerSpawner { continue; } - const spawnLand = this.randomSpawnLand(); - - if (spawnLand === null) { - // TODO: this should normally not happen, additional logic may be needed, if this occurs - continue; - } - - this.players.push(new SpawnExecution(player.info(), spawnLand)); + this.players.push(new SpawnExecution(this.gameID, player.info())); } return this.players; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 20775071a..9c5ef95ff 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -550,7 +550,8 @@ export interface Player { markDisconnected(isDisconnected: boolean): void; 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 09659031e..9dab88905 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -100,7 +100,7 @@ export class PlayerImpl implements Player { public _outgoingAttacks: Attack[] = []; public _outgoingLandAttacks: Attack[] = []; - private _hasSpawned = false; + private _spawnTile: TileRef | undefined; private _isDisconnected = false; constructor( @@ -343,11 +343,15 @@ export class PlayerImpl implements Player { } hasSpawned(): boolean { - return this._hasSpawned; + return this._spawnTile !== undefined; } - setHasSpawned(hasSpawned: boolean): void { - this._hasSpawned = hasSpawned; + setSpawnTile(spawnTile: TileRef): void { + this._spawnTile = spawnTile; + } + + spawnTile(): TileRef | undefined { + return this._spawnTile; } incomingAllianceRequests(): AllianceRequest[] { diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index fceeb365a..5715565a0 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -9,11 +9,13 @@ import { UnitType, } from "../src/core/game/Game"; import { TileRef } from "../src/core/game/GameMap"; +import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; import { TestConfig } from "./util/TestConfig"; import { constructionExecution } from "./util/utils"; let game: Game; +const gameID: GameID = "game_id"; let attacker: Player; let defender: Player; let defenderSpawn: TileRef; @@ -51,8 +53,16 @@ describe("Attack", () => { attackerSpawn = game.ref(0, 10); game.addExecution( - new SpawnExecution(game.player(attackerInfo.id).info(), attackerSpawn), - new SpawnExecution(game.player(defenderInfo.id).info(), defenderSpawn), + new SpawnExecution( + gameID, + game.player(attackerInfo.id).info(), + attackerSpawn, + ), + new SpawnExecution( + gameID, + game.player(defenderInfo.id).info(), + defenderSpawn, + ), ); while (game.inSpawnPhase()) { @@ -142,7 +152,7 @@ function addPlayerToGame( tile: TileRef, ): Player { game.addPlayer(playerInfo); - game.addExecution(new SpawnExecution(playerInfo, tile)); + game.addExecution(new SpawnExecution(gameID, playerInfo, tile)); return game.player(playerInfo.id); } diff --git a/tests/AttackStats.test.ts b/tests/AttackStats.test.ts index 1955aec11..10cb236c3 100644 --- a/tests/AttackStats.test.ts +++ b/tests/AttackStats.test.ts @@ -1,10 +1,12 @@ import { AttackExecution } from "../src/core/execution/AttackExecution"; import { SpawnExecution } from "../src/core/execution/SpawnExecution"; import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game"; +import { GameID } from "../src/core/Schemas"; import { GOLD_INDEX_WAR, GOLD_INDEX_WORK } from "../src/core/StatsSchemas"; import { setup } from "./util/Setup"; let game: Game; +const gameID: GameID = "game_id"; let player1: Player; let player2: Player; @@ -18,8 +20,12 @@ describe("AttackStats", () => { player1 = game.player("player1"); player2 = game.player("player2"); - game.addExecution(new SpawnExecution(player1.info(), game.ref(50, 50))); - game.addExecution(new SpawnExecution(player2.info(), game.ref(50, 55))); + game.addExecution( + new SpawnExecution(gameID, player1.info(), game.ref(50, 50)), + ); + game.addExecution( + new SpawnExecution(gameID, player2.info(), game.ref(50, 55)), + ); while (game.inSpawnPhase()) { game.executeNextTick(); diff --git a/tests/DeleteUnitExecution.test.ts b/tests/DeleteUnitExecution.test.ts index 71444dca5..44755dd68 100644 --- a/tests/DeleteUnitExecution.test.ts +++ b/tests/DeleteUnitExecution.test.ts @@ -9,11 +9,13 @@ import { UnitType, } from "../src/core/game/Game"; import { TileRef } from "../src/core/game/GameMap"; +import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; import { executeTicks } from "./util/utils"; describe("DeleteUnitExecution Security Tests", () => { let game: Game; + const gameID: GameID = "game_id"; let player: Player; let enemyPlayer: Player; let unit: Unit; @@ -45,8 +47,16 @@ describe("DeleteUnitExecution Security Tests", () => { const enemySpawn: TileRef = game.ref(0, 15); game.addExecution( - new SpawnExecution(game.player(player1Info.id).info(), playerSpawn), - new SpawnExecution(game.player(player2Info.id).info(), enemySpawn), + new SpawnExecution( + gameID, + game.player(player1Info.id).info(), + playerSpawn, + ), + new SpawnExecution( + gameID, + game.player(player2Info.id).info(), + enemySpawn, + ), ); while (game.inSpawnPhase()) { diff --git a/tests/Disconnected.test.ts b/tests/Disconnected.test.ts index ab630eb73..ca3cfc45b 100644 --- a/tests/Disconnected.test.ts +++ b/tests/Disconnected.test.ts @@ -11,12 +11,14 @@ import { PlayerType, UnitType, } from "../src/core/game/Game"; +import { GameID } from "../src/core/Schemas"; import { toInt } from "../src/core/Util"; import { setup } from "./util/Setup"; import { UseRealAttackLogic } from "./util/TestConfig"; import { executeTicks } from "./util/utils"; let game: Game; +const gameID: GameID = "game_id"; let player1: Player; let player2: Player; let enemy: Player; @@ -46,8 +48,8 @@ describe("Disconnected", () => { player2 = game.addPlayer(player2Info); game.addExecution( - new SpawnExecution(player1Info, game.ref(1, 1)), - new SpawnExecution(player2Info, game.ref(7, 7)), + new SpawnExecution(gameID, player1Info, game.ref(1, 1)), + new SpawnExecution(gameID, player2Info, game.ref(7, 7)), ); while (game.inSpawnPhase()) { @@ -203,8 +205,8 @@ describe("Disconnected", () => { ); game.addExecution( - new SpawnExecution(player1Info, game.map().ref(coastX - 2, 1)), - new SpawnExecution(player2Info, game.map().ref(coastX - 2, 4)), + new SpawnExecution(gameID, player1Info, game.map().ref(coastX - 2, 1)), + new SpawnExecution(gameID, player2Info, game.map().ref(coastX - 2, 4)), ); while (game.inSpawnPhase()) { diff --git a/tests/Donate.test.ts b/tests/Donate.test.ts index 966f9d493..dba10084a 100644 --- a/tests/Donate.test.ts +++ b/tests/Donate.test.ts @@ -2,10 +2,12 @@ import { DonateGoldExecution } from "../src/core/execution/DonateGoldExecution"; import { DonateTroopsExecution } from "../src/core/execution/DonateTroopExecution"; import { SpawnExecution } from "../src/core/execution/SpawnExecution"; import { PlayerInfo, PlayerType } from "../src/core/game/Game"; +import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; describe("Donate troops to an ally", () => { it("Troops should be successfully donated", async () => { + const gameID: GameID = "game_id"; const game = await setup("ocean_and_land", { infiniteTroops: false, donateTroops: true, @@ -35,8 +37,8 @@ describe("Donate troops to an ally", () => { const spawnB = game.ref(0, 15); game.addExecution( - new SpawnExecution(donorInfo, spawnA), - new SpawnExecution(recipientInfo, spawnB), + new SpawnExecution(gameID, donorInfo, spawnA), + new SpawnExecution(gameID, recipientInfo, spawnB), ); while (game.inSpawnPhase()) { @@ -73,6 +75,7 @@ describe("Donate gold to an ally", () => { infiniteGold: false, donateGold: true, }); + const gameID: GameID = "game_id"; const donorInfo = new PlayerInfo( "donor", @@ -98,8 +101,8 @@ describe("Donate gold to an ally", () => { const spawnB = game.ref(0, 15); game.addExecution( - new SpawnExecution(donorInfo, spawnA), - new SpawnExecution(recipientInfo, spawnB), + new SpawnExecution(gameID, donorInfo, spawnA), + new SpawnExecution(gameID, recipientInfo, spawnB), ); while (game.inSpawnPhase()) { @@ -137,6 +140,7 @@ describe("Donate troops to a non ally", () => { infiniteTroops: false, donateTroops: true, }); + const gameID: GameID = "game_id"; const donorInfo = new PlayerInfo( "donor", @@ -162,8 +166,8 @@ describe("Donate troops to a non ally", () => { const spawnB = game.ref(0, 15); game.addExecution( - new SpawnExecution(donorInfo, spawnA), - new SpawnExecution(recipientInfo, spawnB), + new SpawnExecution(gameID, donorInfo, spawnA), + new SpawnExecution(gameID, recipientInfo, spawnB), ); while (game.inSpawnPhase()) { @@ -197,6 +201,7 @@ describe("Donate Gold to a non ally", () => { infiniteGold: false, donateGold: true, }); + const gameID: GameID = "game_id"; const donorInfo = new PlayerInfo( "donor", @@ -222,8 +227,8 @@ describe("Donate Gold to a non ally", () => { const spawnB = game.ref(0, 15); game.addExecution( - new SpawnExecution(donorInfo, spawnA), - new SpawnExecution(recipientInfo, spawnB), + new SpawnExecution(gameID, donorInfo, spawnA), + new SpawnExecution(gameID, recipientInfo, spawnB), ); while (game.inSpawnPhase()) { diff --git a/tests/MissileSilo.test.ts b/tests/MissileSilo.test.ts index 5045d4622..57346a6e3 100644 --- a/tests/MissileSilo.test.ts +++ b/tests/MissileSilo.test.ts @@ -9,9 +9,11 @@ import { UnitType, } from "../src/core/game/Game"; import { TileRef } from "../src/core/game/GameMap"; +import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; import { constructionExecution, executeTicks } from "./util/utils"; +const gameID: GameID = "game_id"; let game: Game; let attacker: Player; @@ -41,7 +43,11 @@ describe("MissileSilo", () => { game.addPlayer(attacker_info); game.addExecution( - new SpawnExecution(game.player(attacker_info.id).info(), game.ref(1, 1)), + new SpawnExecution( + gameID, + game.player(attacker_info.id).info(), + game.ref(1, 1), + ), ); while (game.inSpawnPhase()) { diff --git a/tests/TerritoryCapture.test.ts b/tests/TerritoryCapture.test.ts index e46678c08..84a8e23f9 100644 --- a/tests/TerritoryCapture.test.ts +++ b/tests/TerritoryCapture.test.ts @@ -1,16 +1,18 @@ import { SpawnExecution } from "../src/core/execution/SpawnExecution"; import { Player, PlayerInfo, PlayerType } from "../src/core/game/Game"; +import { GameID } from "../src/core/Schemas"; import { setup } from "./util/Setup"; describe("Territory management", () => { test("player owns the tile it spawns on", async () => { const game = await setup("plains"); + const gameID: GameID = "game_id"; game.addPlayer( new PlayerInfo("test_player", PlayerType.Human, null, "test_id"), ); const spawnTile = game.map().ref(50, 50); game.addExecution( - new SpawnExecution(game.player("test_id").info(), spawnTile), + new SpawnExecution(gameID, game.player("test_id").info(), spawnTile), ); // Init the execution game.executeNextTick(); diff --git a/tests/core/execution/SpawnExecution.test.ts b/tests/core/execution/SpawnExecution.test.ts new file mode 100644 index 000000000..d9108acea --- /dev/null +++ b/tests/core/execution/SpawnExecution.test.ts @@ -0,0 +1,112 @@ +import { SpawnExecution } from "../../../src/core/execution/SpawnExecution"; +import { PlayerInfo, PlayerType } from "../../../src/core/game/Game"; +import { setup } from "../../util/Setup"; + +describe("Spawn execution", () => { + // Manually calculated based on number of tiles in manifest of each map + // and minimum distance between players in PlayerSpawner + test.each([ + ["big_plains", 49], + ["half_land_half_ocean", 1], + ["ocean_and_land", 1], + ["plains", 9], + ])( + "Spawn location is found for all players in %s map with %i players", + async (mapName, maxPlayers) => { + const players: PlayerInfo[] = []; + const spawnExecutions: SpawnExecution[] = []; + for (let i = 0; i < maxPlayers; i++) { + const playerInfo = new PlayerInfo( + `player${i}`, + PlayerType.Human, + `client_id${i}`, + `player_id${i}`, + ); + players.push(playerInfo); + + spawnExecutions.push(new SpawnExecution("game_id", playerInfo)); + } + + const game = await setup(mapName, undefined, players); + + game.addExecution(...spawnExecutions); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + game.allPlayers().forEach((player) => { + const spawnTile = player.spawnTile()!; + expect(spawnTile).toEqual(expect.any(Number)); + expect(game.isLand(spawnTile)).toBe(true); + expect(game.isBorder(spawnTile)).toBe(false); + }); + + for (let i = 0; i < game.allPlayers().length; i++) { + for (let j = i + 1; j < game.allPlayers().length; j++) { + const distance = game.manhattanDist( + game.allPlayers()[i].spawnTile()!, + game.allPlayers()[j].spawnTile()!, + ); + expect(distance).toBeGreaterThanOrEqual( + game.config().minDistanceBetweenPlayers(), + ); + } + } + }, + ); + + test("Handles spawn failure when map is too crowded", async () => { + const players: PlayerInfo[] = []; + const spawnExecutions: SpawnExecution[] = []; + + // Try to spawn more players than possible on a small map + for (let i = 0; i < 5; i++) { + const playerInfo = new PlayerInfo( + `player${i}`, + PlayerType.Human, + `client_id${i}`, + `player_id${i}`, + ); + players.push(playerInfo); + + spawnExecutions.push(new SpawnExecution("game_id", playerInfo)); + } + + const game = await setup("half_land_half_ocean", undefined, players); + + game.addExecution(...spawnExecutions); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + // Should spawn fewer than requested when map is too small + expect( + game.allPlayers().filter((player) => player.spawnTile() !== undefined) + .length, + ).toBe(1); + }); + + test("Spawn on specific tile", async () => { + const playerInfo = new PlayerInfo( + `player`, + PlayerType.Human, + `client_id`, + `player_id`, + ); + + 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)); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + expect(game.playerByClientID("client_id")?.spawnTile()).toBe(60); + // Previous territory from first spawn should be relinquished + expect(game.owner(50).isPlayer()).toBe(false); + }); +}); diff --git a/tests/core/execution/utils/PlayerSpawner.test.ts b/tests/core/execution/utils/PlayerSpawner.test.ts deleted file mode 100644 index 6e719c2db..000000000 --- a/tests/core/execution/utils/PlayerSpawner.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PlayerSpawner } from "../../../../src/core/execution/utils/PlayerSpawner"; -import { PlayerInfo, PlayerType } from "../../../../src/core/game/Game"; -import { setup } from "../../../util/Setup"; - -describe("PlayerSpawner", () => { - // Manually calculated based on number of tiles in manifest of each map - // and minimum distance between players in PlayerSpawner - test.each([ - ["big_plains", 49], - ["half_land_half_ocean", 1], - ["ocean_and_land", 1], - ["plains", 9], - ])( - "Spawn location is found for all players in %s map with %i players", - async (mapName, maxPlayers) => { - const players: PlayerInfo[] = []; - - for (let i = 0; i < maxPlayers; i++) { - players.push( - new PlayerInfo( - `player${i}`, - PlayerType.Human, - `client_id${i}`, - `player_id${i}`, - ), - ); - } - - const game = await setup(mapName, undefined, players); - - const executors = new PlayerSpawner(game, "game_id").spawnPlayers(); - expect(executors.length).toBe(maxPlayers); - - for (const executor of executors) { - expect(game.isLand(executor.tile)).toBe(true); - expect(game.isBorder(executor.tile)).toBe(false); - } - - for (let i = 0; i < executors.length; i++) { - for (let j = i + 1; j < executors.length; j++) { - const distance = game.manhattanDist( - executors[i].tile, - executors[j].tile, - ); - expect(distance).toBeGreaterThanOrEqual(30); - } - } - }, - ); - - test("Handles spawn failure when map is too crowded", async () => { - const players: PlayerInfo[] = []; - - // Try to spawn more players than possible on a small map - for (let i = 0; i < 5; i++) { - players.push( - new PlayerInfo( - `player${i}`, - PlayerType.Human, - `client_id${i}`, - `player_id${i}`, - ), - ); - } - - const game = await setup("half_land_half_ocean", undefined, players); - const executors = new PlayerSpawner(game, "game_id").spawnPlayers(); - - // Should spawn fewer than requested when map is too small - expect(executors.length).toBe(1); - }); -}); diff --git a/tests/core/executions/SAMLauncherExecution.test.ts b/tests/core/executions/SAMLauncherExecution.test.ts index ed7ba534b..6916761d1 100644 --- a/tests/core/executions/SAMLauncherExecution.test.ts +++ b/tests/core/executions/SAMLauncherExecution.test.ts @@ -9,10 +9,12 @@ import { PlayerType, UnitType, } from "../../../src/core/game/Game"; +import { GameID } from "../../../src/core/Schemas"; import { setup } from "../../util/Setup"; import { constructionExecution, executeTicks } from "../../util/utils"; let game: Game; +const gameID: GameID = "game_id"; let attacker: Player; let defender: Player; let far_defender: Player; @@ -54,16 +56,26 @@ describe("SAM", () => { game.addPlayer(attacker_info); game.addExecution( - new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)), new SpawnExecution( + gameID, + game.player(defender_info.id).info(), + game.ref(1, 1), + ), + new SpawnExecution( + gameID, game.player(middle_defender_info.id).info(), game.ref(50, 1), ), new SpawnExecution( + gameID, game.player(far_defender_info.id).info(), game.ref(199, 1), ), - new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)), + new SpawnExecution( + gameID, + game.player(attacker_info.id).info(), + game.ref(7, 7), + ), ); while (game.inSpawnPhase()) { diff --git a/tests/core/game/GameImpl.test.ts b/tests/core/game/GameImpl.test.ts index 831036c2f..6ea678be7 100644 --- a/tests/core/game/GameImpl.test.ts +++ b/tests/core/game/GameImpl.test.ts @@ -1,3 +1,4 @@ +import { GameID } from "../../../src/core/Schemas"; import { AttackExecution } from "../../../src/core/execution/AttackExecution"; import { SpawnExecution } from "../../../src/core/execution/SpawnExecution"; //import { TransportShipExecution } from "../../../src/core/execution/TransportShipExecution"; @@ -12,6 +13,7 @@ import { import { TileRef } from "../../../src/core/game/GameMap"; import { setup } from "../../util/Setup"; +const gameID: GameID = "game_id"; let game: Game; let attacker: Player; let defender: Player; @@ -44,8 +46,16 @@ describe("GameImpl", () => { attackerSpawn = game.ref(0, 14); game.addExecution( - new SpawnExecution(game.player(attackerInfo.id).info(), attackerSpawn), - new SpawnExecution(game.player(defenderInfo.id).info(), defenderSpawn), + new SpawnExecution( + gameID, + game.player(attackerInfo.id).info(), + attackerSpawn, + ), + new SpawnExecution( + gameID, + game.player(defenderInfo.id).info(), + defenderSpawn, + ), ); while (game.inSpawnPhase()) { diff --git a/tests/economy/ConstructionGold.test.ts b/tests/economy/ConstructionGold.test.ts index d763aeecf..e4f26b8e1 100644 --- a/tests/economy/ConstructionGold.test.ts +++ b/tests/economy/ConstructionGold.test.ts @@ -8,10 +8,12 @@ import { PlayerType, UnitType, } from "../../src/core/game/Game"; +import { GameID } from "../../src/core/Schemas"; import { setup } from "../util/Setup"; describe("Construction economy", () => { let game: Game; + const gameID: GameID = "game_id"; let player: Player; let other: Player; const builderInfo = new PlayerInfo( @@ -33,8 +35,8 @@ describe("Construction economy", () => { [builderInfo, otherInfo], ); const spawn = game.ref(0, 10); - game.addExecution(new SpawnExecution(builderInfo, spawn)); - game.addExecution(new SpawnExecution(otherInfo, spawn)); + game.addExecution(new SpawnExecution(gameID, builderInfo, spawn)); + game.addExecution(new SpawnExecution(gameID, otherInfo, spawn)); while (game.inSpawnPhase()) { game.executeNextTick(); } diff --git a/tests/nukes/HydrogenAndMirv.test.ts b/tests/nukes/HydrogenAndMirv.test.ts index edfca7f67..e73059f45 100644 --- a/tests/nukes/HydrogenAndMirv.test.ts +++ b/tests/nukes/HydrogenAndMirv.test.ts @@ -7,17 +7,19 @@ import { PlayerType, UnitType, } from "../../src/core/game/Game"; +import { GameID } from "../../src/core/Schemas"; import { setup } from "../util/Setup"; describe("Hydrogen Bomb and MIRV flows", () => { let game: Game; let player: Player; + const gameID: GameID = "game_id"; beforeEach(async () => { game = await setup("plains", { infiniteGold: true, instantBuild: true }); const info = new PlayerInfo("p", PlayerType.Human, null, "p"); game.addPlayer(info); - game.addExecution(new SpawnExecution(info, game.ref(1, 1))); + game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1))); while (game.inSpawnPhase()) game.executeNextTick(); player = game.player(info.id); @@ -57,7 +59,7 @@ describe("Hydrogen Bomb and MIRV flows", () => { const info = new PlayerInfo("p", PlayerType.Human, null, "p"); gameWithConstruction.addPlayer(info); gameWithConstruction.addExecution( - new SpawnExecution(info, gameWithConstruction.ref(1, 1)), + new SpawnExecution(gameID, info, gameWithConstruction.ref(1, 1)), ); while (gameWithConstruction.inSpawnPhase()) gameWithConstruction.executeNextTick();