mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:50 +00:00
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 <evanpelle@gmail.com>
This commit is contained in:
@@ -154,6 +154,7 @@ export interface Config {
|
||||
defensePostRange(): number;
|
||||
SAMCooldown(): number;
|
||||
SiloCooldown(): number;
|
||||
minDistanceBetweenPlayers(): number;
|
||||
defensePostDefenseBonus(): number;
|
||||
defensePostSpeedBonus(): number;
|
||||
falloutDefenseModifier(percentOfFallout: number): number;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
Relation,
|
||||
TerrainType,
|
||||
Tick,
|
||||
Unit,
|
||||
UnitType,
|
||||
@@ -54,7 +53,7 @@ export class NationExecution implements Execution {
|
||||
private trackedTradeShips: Set<Unit> = 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<TileRef>;
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
+13
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
+13
-8
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user