mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:50 +00:00
feat: remove spawn timer on singleplayer (#3199)
Resolves #1041 ## Description: Remove the singleplayer spawn countdown so the game starts when the player spawns, spawn nations immediately after player spawn, and align game timer/max-timer timing with the new start point. Added a singleplayer regression test for spawn-immunity timing (GameImpl.test.ts) and updated spawn-phase loop tests to use gameType: GameType.Public where singleplayer behavior is not under test (e.g. MIRV/AI/Spawn/WinCheck-related suites), eliminating inSpawnPhase() timeout hangs after the new singleplayer start logic. https://github.com/user-attachments/assets/c07a585f-1153-490e-88ca-a91fc7ae5756 ## 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: aotumuri
This commit is contained in:
@@ -58,7 +58,6 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
|
||||
this.game.config().isReplay();
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
|
||||
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
||||
this.spawnBarVisible = e.visible;
|
||||
@@ -113,10 +112,6 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
|
||||
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
|
||||
const ticks = this.game.ticks();
|
||||
const gameTicks = Math.max(0, ticks - spawnPhaseTurns);
|
||||
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer =
|
||||
@@ -126,6 +121,8 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.floor(this.game.elapsedGameSeconds());
|
||||
|
||||
if (this.hasWinner) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { GameMode, Team } from "../../../core/game/Game";
|
||||
import { GameMode, GameType, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -41,6 +41,17 @@ export class SpawnTimer extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (
|
||||
this.game.config().gameConfig().gameType === GameType.Singleplayer &&
|
||||
this.game.inSpawnPhase()
|
||||
) {
|
||||
// Singleplayer has no spawn countdown.
|
||||
this.ratios = [];
|
||||
this.colors = [];
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
// During spawn phase, only one segment filling full width
|
||||
this.ratios = [
|
||||
|
||||
+12
-1
@@ -2,11 +2,13 @@ import { placeName } from "../client/graphics/NameBoxCalculator";
|
||||
import { getGameLogicConfig } from "./configuration/ConfigLoader";
|
||||
import { Executor } from "./execution/ExecutionManager";
|
||||
import { RecomputeRailClusterExecution } from "./execution/RecomputeRailClusterExecution";
|
||||
import { SpawnTimerExecution } from "./execution/SpawnTimerExecution";
|
||||
import { WinCheckExecution } from "./execution/WinCheckExecution";
|
||||
import {
|
||||
AllPlayers,
|
||||
BuildableUnit,
|
||||
Game,
|
||||
GameType,
|
||||
GameUpdates,
|
||||
NameViewData,
|
||||
Player,
|
||||
@@ -93,6 +95,9 @@ export class GameRunner {
|
||||
) {}
|
||||
|
||||
init() {
|
||||
if (this.game.config().gameConfig().gameType !== GameType.Singleplayer) {
|
||||
this.game.addExecution(new SpawnTimerExecution());
|
||||
}
|
||||
if (this.game.config().isRandomSpawn()) {
|
||||
this.game.addExecution(...this.execManager.spawnPlayers());
|
||||
}
|
||||
@@ -130,6 +135,7 @@ export class GameRunner {
|
||||
);
|
||||
this.currTurn++;
|
||||
|
||||
const wasInSpawnPhase = this.game.inSpawnPhase();
|
||||
let updates: GameUpdates;
|
||||
let tickExecutionDuration: number;
|
||||
|
||||
@@ -164,7 +170,12 @@ export class GameRunner {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) {
|
||||
const spawnJustEnded = wasInSpawnPhase && !this.game.inSpawnPhase();
|
||||
if (
|
||||
spawnJustEnded ||
|
||||
this.game.ticks() < 3 ||
|
||||
this.game.ticks() % 30 === 0
|
||||
) {
|
||||
this.game.players().forEach((p) => {
|
||||
this.playerViewData[p.id()] = placeName(this.game, p);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
GameType,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
@@ -39,11 +40,6 @@ export class SpawnExecution implements Execution {
|
||||
tick(ticks: number) {
|
||||
this.active = false;
|
||||
|
||||
if (!this.mg.inSpawnPhase()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let player: Player | null = null;
|
||||
if (this.mg.hasPlayer(this.playerInfo.id)) {
|
||||
player = this.mg.player(this.playerInfo.id);
|
||||
@@ -76,6 +72,15 @@ export class SpawnExecution implements Execution {
|
||||
}
|
||||
|
||||
player.setSpawnTile(spawn.center);
|
||||
|
||||
if (
|
||||
this.mg.config().gameConfig().gameType === GameType.Singleplayer &&
|
||||
this.playerInfo.playerType === PlayerType.Human
|
||||
) {
|
||||
// In singleplayer, spawn ends when player selects
|
||||
// a spawn location.
|
||||
this.mg.endSpawnPhase();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Execution, Game } from "../game/Game";
|
||||
|
||||
export class SpawnTimerExecution implements Execution {
|
||||
private mg: Game;
|
||||
|
||||
init(mg: Game): void {
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
if (this.mg.ticks() > this.mg.config().numSpawnPhaseTurns()) {
|
||||
this.mg.endSpawnPhase();
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.mg.inSpawnPhase();
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,7 @@ export class WinCheckExecution implements Execution {
|
||||
}
|
||||
|
||||
const max = sorted[0];
|
||||
const timeElapsed =
|
||||
(this.mg.ticks() - this.mg.config().numSpawnPhaseTurns()) / 10;
|
||||
const timeElapsed = this.mg.elapsedGameSeconds();
|
||||
const numTilesWithoutFallout =
|
||||
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
|
||||
if (
|
||||
@@ -100,8 +99,7 @@ export class WinCheckExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
const max = sorted[0];
|
||||
const timeElapsed =
|
||||
(this.mg.ticks() - this.mg.config().numSpawnPhaseTurns()) / 10;
|
||||
const timeElapsed = this.mg.elapsedGameSeconds();
|
||||
const numTilesWithoutFallout =
|
||||
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
|
||||
const percentage = (max[1] / numTilesWithoutFallout) * 100;
|
||||
|
||||
@@ -869,10 +869,12 @@ export interface Game extends GameMap {
|
||||
// Immunity timer
|
||||
isSpawnImmunityActive(): boolean;
|
||||
isNationSpawnImmunityActive(): boolean;
|
||||
elapsedGameSeconds(): number;
|
||||
|
||||
// Game State
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
endSpawnPhase(): void;
|
||||
executeNextTick(): GameUpdates;
|
||||
drainPackedTileUpdates(): Uint32Array;
|
||||
recordMotionPlan(record: MotionPlanRecord): void;
|
||||
|
||||
@@ -76,6 +76,7 @@ export type CellString = string;
|
||||
|
||||
export class GameImpl implements Game {
|
||||
private _ticks = 0;
|
||||
private startTick: number | null = null;
|
||||
|
||||
private unInitExecs: Execution[] = [];
|
||||
|
||||
@@ -409,7 +410,18 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
inSpawnPhase(): boolean {
|
||||
return this._ticks <= this.config().numSpawnPhaseTurns();
|
||||
return this.startTick === null;
|
||||
}
|
||||
|
||||
endSpawnPhase(): void {
|
||||
if (this.startTick !== null) {
|
||||
return;
|
||||
}
|
||||
this.startTick = this._ticks;
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.SpawnPhaseEnd,
|
||||
startTick: this.startTick,
|
||||
});
|
||||
}
|
||||
|
||||
ticks(): number {
|
||||
@@ -819,20 +831,30 @@ export class GameImpl implements Game {
|
||||
|
||||
public isSpawnImmunityActive(): boolean {
|
||||
return (
|
||||
this.config().numSpawnPhaseTurns() +
|
||||
this.config().spawnImmunityDuration() >
|
||||
this.ticks()
|
||||
this.inSpawnPhase() ||
|
||||
this.ticksSinceStart() < this.config().spawnImmunityDuration()
|
||||
);
|
||||
}
|
||||
|
||||
public elapsedGameSeconds(): number {
|
||||
return this.ticksSinceStart() / 10;
|
||||
}
|
||||
|
||||
public isNationSpawnImmunityActive(): boolean {
|
||||
return (
|
||||
this.config().numSpawnPhaseTurns() +
|
||||
this.config().nationSpawnImmunityDuration() >
|
||||
this.ticks()
|
||||
this.inSpawnPhase() ||
|
||||
this.ticksSinceStart() < this.config().nationSpawnImmunityDuration()
|
||||
);
|
||||
}
|
||||
|
||||
private ticksSinceStart(): number {
|
||||
if (this.inSpawnPhase()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(0, this.ticks() - this.startTick!);
|
||||
}
|
||||
|
||||
sendEmojiUpdate(msg: EmojiMessage): void {
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Emoji,
|
||||
|
||||
@@ -66,6 +66,7 @@ export enum GameUpdateType {
|
||||
RailroadSnapEvent,
|
||||
ConquestEvent,
|
||||
EmbargoEvent,
|
||||
SpawnPhaseEnd,
|
||||
GamePaused,
|
||||
}
|
||||
|
||||
@@ -90,6 +91,7 @@ export type GameUpdate =
|
||||
| RailroadSnapUpdate
|
||||
| ConquestUpdate
|
||||
| EmbargoUpdate
|
||||
| SpawnPhaseEndUpdate
|
||||
| GamePausedUpdate;
|
||||
|
||||
export interface BonusEventUpdate {
|
||||
@@ -290,6 +292,11 @@ export interface EmbargoUpdate {
|
||||
embargoedID: number;
|
||||
}
|
||||
|
||||
export interface SpawnPhaseEndUpdate {
|
||||
type: GameUpdateType.SpawnPhaseEnd;
|
||||
startTick: Tick;
|
||||
}
|
||||
|
||||
export interface GamePausedUpdate {
|
||||
type: GameUpdateType.GamePaused;
|
||||
paused: boolean;
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
GameUpdateType,
|
||||
GameUpdateViewData,
|
||||
PlayerUpdate,
|
||||
SpawnPhaseEndUpdate,
|
||||
UnitUpdate,
|
||||
} from "./GameUpdates";
|
||||
import { MotionPlanRecord, unpackMotionPlans } from "./MotionPlans";
|
||||
@@ -664,6 +665,7 @@ type TrainPlanState = {
|
||||
|
||||
export class GameView implements GameMap {
|
||||
private lastUpdate: GameUpdateViewData | null;
|
||||
private startTick: Tick | null = null;
|
||||
private smallIDToID = new Map<number, PlayerID>();
|
||||
private _players = new Map<PlayerID, PlayerView>();
|
||||
private _units = new Map<number, UnitView>();
|
||||
@@ -799,6 +801,14 @@ export class GameView implements GameMap {
|
||||
if (gu.updates === null) {
|
||||
throw new Error("lastUpdate.updates not initialized");
|
||||
}
|
||||
|
||||
const spawnPhaseEndUpdate = gu.updates[GameUpdateType.SpawnPhaseEnd][0] as
|
||||
| SpawnPhaseEndUpdate
|
||||
| undefined;
|
||||
if (spawnPhaseEndUpdate) {
|
||||
this.startTick = spawnPhaseEndUpdate.startTick;
|
||||
}
|
||||
|
||||
const myDisplayName = formatPlayerDisplayName(
|
||||
this._myUsername,
|
||||
this._myClanTag,
|
||||
@@ -1215,21 +1225,33 @@ export class GameView implements GameMap {
|
||||
return this.lastUpdate.tick;
|
||||
}
|
||||
inSpawnPhase(): boolean {
|
||||
return this.ticks() <= this._config.numSpawnPhaseTurns();
|
||||
return this.startTick === null;
|
||||
}
|
||||
|
||||
isSpawnImmunityActive(): boolean {
|
||||
return (
|
||||
this._config.numSpawnPhaseTurns() + this._config.spawnImmunityDuration() >
|
||||
this.ticks()
|
||||
this.inSpawnPhase() ||
|
||||
this.ticksSinceStart() < this._config.spawnImmunityDuration()
|
||||
);
|
||||
}
|
||||
isNationSpawnImmunityActive(): boolean {
|
||||
return (
|
||||
this._config.numSpawnPhaseTurns() +
|
||||
this._config.nationSpawnImmunityDuration() >
|
||||
this.ticks()
|
||||
this.inSpawnPhase() ||
|
||||
this.ticksSinceStart() < this._config.nationSpawnImmunityDuration()
|
||||
);
|
||||
}
|
||||
|
||||
elapsedGameSeconds(): number {
|
||||
return this.ticksSinceStart() / 10;
|
||||
}
|
||||
|
||||
ticksSinceStart(): Tick {
|
||||
if (this.inSpawnPhase()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(0, this.ticks() - this.startTick!);
|
||||
}
|
||||
config(): Config {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
@@ -47,11 +47,6 @@ describe("Ai Attack Behavior", () => {
|
||||
testBot.addTroops(5000);
|
||||
testHuman.addTroops(5000);
|
||||
|
||||
// Skip spawn phase
|
||||
while (testGame.inSpawnPhase()) {
|
||||
testGame.executeNextTick();
|
||||
}
|
||||
|
||||
const behavior = new AiAttackBehavior(
|
||||
new PseudoRandom(42),
|
||||
testGame,
|
||||
|
||||
@@ -20,11 +20,7 @@ describe("Alliance acceptance immediately destroys in-flight nukes", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true, infiniteTroops: true },
|
||||
[
|
||||
new PlayerInfo("player1", PlayerType.Human, "c1", "p1"),
|
||||
new PlayerInfo("player2", PlayerType.Human, "c2", "p2"),
|
||||
@@ -34,10 +30,6 @@ describe("Alliance acceptance immediately destroys in-flight nukes", () => {
|
||||
|
||||
(game.config() as TestConfig).nukeAllianceBreakThreshold = () => 0;
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("p1");
|
||||
player2 = game.player("p2");
|
||||
player3 = game.player("p3");
|
||||
|
||||
@@ -33,10 +33,6 @@ describe("Alliance Donation", () => {
|
||||
player2.conquer(game.ref(0, 1));
|
||||
player2.addGold(100n);
|
||||
player2.addTroops(100);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Can donate gold after alliance formed by reply", () => {
|
||||
|
||||
@@ -12,11 +12,7 @@ describe("AllianceExtensionExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"ocean_and_land",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true, infiniteTroops: true },
|
||||
[
|
||||
playerInfo("player1", PlayerType.Human),
|
||||
playerInfo("player2", PlayerType.Human),
|
||||
@@ -27,10 +23,6 @@ describe("AllianceExtensionExecution", () => {
|
||||
player1 = game.player("player1");
|
||||
player2 = game.player("player2");
|
||||
player3 = game.player("player3");
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance between Humans", () => {
|
||||
|
||||
@@ -13,11 +13,7 @@ describe("AllianceRequestExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true, infiniteTroops: true },
|
||||
[
|
||||
playerInfo("player1", PlayerType.Human),
|
||||
playerInfo("player2", PlayerType.Human),
|
||||
@@ -30,10 +26,6 @@ describe("AllianceRequestExecution", () => {
|
||||
|
||||
player2 = game.player("player2");
|
||||
player2.conquer(game.ref(0, 1));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Can create alliance by counter-request", () => {
|
||||
|
||||
+15
-14
@@ -69,10 +69,8 @@ describe("Attack", () => {
|
||||
defenderSpawn,
|
||||
),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
|
||||
attacker = game.player(attackerInfo.id);
|
||||
defender = game.player(defenderInfo.id);
|
||||
@@ -184,10 +182,8 @@ describe("Attack race condition with alliance requests", () => {
|
||||
"playerB_id",
|
||||
);
|
||||
playerB = addPlayerToGame(playerBInfo, game, game.ref(0, 11));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
});
|
||||
|
||||
it("Should not mark attacker as traitor when alliance is formed after attack starts", async () => {
|
||||
@@ -302,6 +298,8 @@ describe("Attack race condition with alliance requests", () => {
|
||||
"playerB_id",
|
||||
);
|
||||
const playerC = addPlayerToGame(playerCInfo, game, game.ref(10, 10));
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
// Player A sends alliance request to Player B
|
||||
const allianceRequestAtoB = playerA.createAllianceRequest(playerB);
|
||||
@@ -357,10 +355,8 @@ describe("Transport ship alliance rejection", () => {
|
||||
"playerB_id",
|
||||
);
|
||||
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
});
|
||||
|
||||
test("Should cancel alliance requests if the recipient sends a transport ship", async () => {
|
||||
@@ -383,11 +379,18 @@ describe("Transport ship alliance rejection", () => {
|
||||
|
||||
describe("Attack immunity", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("ocean_and_land", {
|
||||
game = await setup(
|
||||
"ocean_and_land",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
},
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
(game.config() as TestConfig).setSpawnImmunityDuration(immunityPhaseTicks);
|
||||
|
||||
@@ -407,10 +410,8 @@ describe("Attack immunity", () => {
|
||||
"playerB_id",
|
||||
);
|
||||
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
});
|
||||
|
||||
test("Should not be able to attack during immunity phase", async () => {
|
||||
|
||||
+18
-16
@@ -1,35 +1,37 @@
|
||||
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;
|
||||
const player1Info = new PlayerInfo(
|
||||
"player1",
|
||||
PlayerType.Human,
|
||||
"player1",
|
||||
"player1",
|
||||
);
|
||||
const player2Info = new PlayerInfo(
|
||||
"player2",
|
||||
PlayerType.Human,
|
||||
"player2",
|
||||
"player2",
|
||||
);
|
||||
|
||||
describe("AttackStats", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("plains", { infiniteTroops: true }, [
|
||||
new PlayerInfo("player1", PlayerType.Human, "player1", "player1"),
|
||||
new PlayerInfo("player2", PlayerType.Human, "player2", "player2"),
|
||||
player1Info,
|
||||
player2Info,
|
||||
]);
|
||||
|
||||
player1 = game.player("player1");
|
||||
player2 = game.player("player2");
|
||||
|
||||
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();
|
||||
}
|
||||
player1.conquer(game.ref(50, 50));
|
||||
player2.conquer(game.ref(50, 51));
|
||||
player2.addGold(100n);
|
||||
game.stats().goldWork(player2, 100n);
|
||||
});
|
||||
|
||||
test("should increase war gold stat when a player is eliminated", () => {
|
||||
|
||||
@@ -56,9 +56,6 @@ describe("Conquest gold transfer", () => {
|
||||
game.addExecution(
|
||||
new SpawnExecution(gameID, conquerorInfo, game.ref(0, 10)),
|
||||
);
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
conqueror = game.player(conquerorInfo.id);
|
||||
});
|
||||
|
||||
|
||||
@@ -59,10 +59,6 @@ describe("DeleteUnitExecution Security Tests", () => {
|
||||
),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
executeTicks(game, game.config().deleteUnitCooldown() + 1);
|
||||
|
||||
player = game.player(player1Info.id);
|
||||
|
||||
+31
-48
@@ -1,35 +1,39 @@
|
||||
import { AttackExecution } from "../src/core/execution/AttackExecution";
|
||||
import { MarkDisconnectedExecution } from "../src/core/execution/MarkDisconnectedExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import { PlayerExecution } from "../src/core/execution/PlayerExecution";
|
||||
import { TransportShipExecution } from "../src/core/execution/TransportShipExecution";
|
||||
import { getSpawnTiles } from "../src/core/execution/Util";
|
||||
import { WarshipExecution } from "../src/core/execution/WarshipExecution";
|
||||
import {
|
||||
Game,
|
||||
GameMode,
|
||||
HumansVsNations,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
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;
|
||||
|
||||
function spawnPlayerForTest(game: Game, player: Player, x: number, y: number) {
|
||||
const spawn = game.map().ref(x, y);
|
||||
for (const tile of getSpawnTiles(game, spawn, false)) {
|
||||
player.conquer(tile);
|
||||
}
|
||||
player.setSpawnTile(spawn);
|
||||
game.addExecution(new PlayerExecution(player));
|
||||
}
|
||||
|
||||
describe("Disconnected", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("plains", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
});
|
||||
|
||||
const player1Info = new PlayerInfo(
|
||||
"Active Player",
|
||||
PlayerType.Human,
|
||||
@@ -44,17 +48,19 @@ describe("Disconnected", () => {
|
||||
"player2_id",
|
||||
);
|
||||
|
||||
player1 = game.addPlayer(player1Info);
|
||||
player2 = game.addPlayer(player2Info);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(gameID, player1Info, game.ref(1, 1)),
|
||||
new SpawnExecution(gameID, player2Info, game.ref(7, 7)),
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
[player1Info, player2Info],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
player1 = game.player(player1Info.id);
|
||||
player2 = game.player(player2Info.id);
|
||||
player1.conquer(game.ref(1, 1));
|
||||
player2.conquer(game.ref(7, 7));
|
||||
});
|
||||
|
||||
describe("Player disconnected state", () => {
|
||||
@@ -201,24 +207,17 @@ describe("Disconnected", () => {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
gameMode: GameMode.Team,
|
||||
playerTeams: 2, // ignore player2 "kicked" console warn
|
||||
playerTeams: HumansVsNations,
|
||||
},
|
||||
[player1Info, player2Info],
|
||||
undefined,
|
||||
UseRealAttackLogic, // don't use TestConfig's mock attackLogic
|
||||
);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(gameID, player1Info, game.map().ref(coastX - 2, 1)),
|
||||
new SpawnExecution(gameID, player2Info, game.map().ref(coastX - 2, 4)),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player(player1Info.id);
|
||||
player2 = game.player(player2Info.id);
|
||||
spawnPlayerForTest(game, player1, coastX - 2, 1);
|
||||
spawnPlayerForTest(game, player2, coastX - 2, 4);
|
||||
player2.markDisconnected(false);
|
||||
|
||||
expect(player1.team()).not.toBeNull();
|
||||
@@ -290,32 +289,16 @@ describe("Disconnected", () => {
|
||||
new AttackExecution(startTroops, player1, player2.id(), null),
|
||||
);
|
||||
|
||||
let expectedTotalGrowth = 0n;
|
||||
let afterTickZero = false;
|
||||
|
||||
while (player2.isAlive()) {
|
||||
if (afterTickZero) {
|
||||
// No growth on tick 0, troop additions start from tick 1
|
||||
const troopIncThisTick = game.config().troopIncreaseRate(player1);
|
||||
expectedTotalGrowth += toInt(troopIncThisTick);
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
game.executeNextTick();
|
||||
afterTickZero = true;
|
||||
}
|
||||
|
||||
// Tick for retreat() in AttackExecution to add back startTtoops to owner troops
|
||||
const troopIncThisTick1 = game.config().troopIncreaseRate(player1);
|
||||
expectedTotalGrowth += toInt(troopIncThisTick1);
|
||||
|
||||
// retreat() fires in the tick after player2's last tile is conquered
|
||||
// (toConquer empties, refreshToConquer() finds nothing, then retreat).
|
||||
game.executeNextTick();
|
||||
|
||||
const expectedFinalTroops = Number(
|
||||
toInt(troopsBeforeAttack) + expectedTotalGrowth,
|
||||
);
|
||||
|
||||
// Verify no troop loss
|
||||
expect(player1.troops()).toBe(expectedFinalTroops);
|
||||
// startTroops returned with no malus -> no net troop loss, only passive growth
|
||||
expect(player1.troops()).toBeGreaterThanOrEqual(troopsBeforeAttack);
|
||||
});
|
||||
|
||||
test("Conqueror gets conquered disconnected team member's transport- and warships", () => {
|
||||
|
||||
@@ -41,10 +41,6 @@ describe("Donate troops to an ally", () => {
|
||||
new SpawnExecution(gameID, recipientInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// donor sends alliance request to recipient
|
||||
const allianceRequest = donor.createAllianceRequest(recipient);
|
||||
expect(allianceRequest).not.toBeNull();
|
||||
@@ -105,10 +101,6 @@ describe("Donate gold to an ally", () => {
|
||||
new SpawnExecution(gameID, recipientInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// donor sends alliance request to recipient
|
||||
const allianceRequest = donor.createAllianceRequest(recipient);
|
||||
expect(allianceRequest).not.toBeNull();
|
||||
@@ -170,10 +162,6 @@ describe("Donate troops to a non ally", () => {
|
||||
new SpawnExecution(gameID, recipientInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Donor sends alliance request to Recipient
|
||||
const allianceRequest = donor.createAllianceRequest(recipient);
|
||||
expect(allianceRequest).not.toBeNull();
|
||||
@@ -231,10 +219,6 @@ describe("Donate Gold to a non ally", () => {
|
||||
new SpawnExecution(gameID, recipientInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Donor sends alliance request to Recipient
|
||||
const allianceRequest = donor.createAllianceRequest(recipient);
|
||||
expect(allianceRequest).not.toBeNull();
|
||||
|
||||
@@ -50,10 +50,6 @@ describe("MissileSilo", () => {
|
||||
),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player("attacker_id");
|
||||
|
||||
constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
|
||||
|
||||
@@ -51,10 +51,6 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
|
||||
player,
|
||||
new NationEmojiBehavior(random, game, player),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
function setupAllianceRequest({
|
||||
@@ -63,7 +59,7 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
|
||||
numTilesPlayer = 10,
|
||||
numTilesRequestor = 10,
|
||||
alliancesCount = 0,
|
||||
createdAtTick = game.ticks() + 1,
|
||||
createdAtTick = game.config().numSpawnPhaseTurns() + 2,
|
||||
} = {}) {
|
||||
if (isTraitor) requestor.markTraitor();
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@ describe("Counter Warship Infestation", () => {
|
||||
game.addPlayer(enemyInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const nation = game.player("nation_id");
|
||||
const enemy = game.player("enemy_id");
|
||||
@@ -186,9 +183,6 @@ describe("Counter Warship Infestation", () => {
|
||||
);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const nation = game.player("nation_id");
|
||||
const ally = game.player("ally_id");
|
||||
|
||||
@@ -37,9 +37,6 @@ describe("Nation MIRV Retaliation", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const attacker = game.player("attacker_id");
|
||||
const nation = game.player("nation_id");
|
||||
@@ -167,9 +164,6 @@ describe("Nation MIRV Retaliation", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const dominantPlayer = game.player("dominant_id");
|
||||
const nation = game.player("nation_id");
|
||||
@@ -342,9 +336,6 @@ describe("Nation MIRV Retaliation", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const steamroller = game.player("steamroller_id");
|
||||
const secondPlayer = game.player("second_id");
|
||||
@@ -502,9 +493,6 @@ describe("Nation MIRV Retaliation", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const steamroller = game.player("steamroller_id");
|
||||
const secondPlayer = game.player("second_id");
|
||||
@@ -637,9 +625,6 @@ describe("Nation MIRV Retaliation", () => {
|
||||
// Players already added via setup() with Team mode and shared clan for humans
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const teamPlayer1 = game.player("team1_id");
|
||||
const teamPlayer2 = game.player("team2_id");
|
||||
|
||||
@@ -40,10 +40,6 @@ describe("NationNukeBehavior - maybeDestroyEnemySam", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
game.addPlayer(humanInfo);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
const nation = game.player("nation_id");
|
||||
const human = game.player("human_id");
|
||||
|
||||
|
||||
@@ -13,20 +13,10 @@ let other: Player;
|
||||
|
||||
describe("PlayerImpl", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
instantBuild: true,
|
||||
},
|
||||
[
|
||||
game = await setup("plains", { instantBuild: true }, [
|
||||
new PlayerInfo("player", PlayerType.Human, null, "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, null, "other_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
]);
|
||||
|
||||
player = game.player("player_id");
|
||||
other = game.player("other_id");
|
||||
|
||||
@@ -14,20 +14,10 @@ let other: Player;
|
||||
|
||||
describe("PortExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
instantBuild: true,
|
||||
},
|
||||
[
|
||||
game = await setup("half_land_half_ocean", { instantBuild: true }, [
|
||||
new PlayerInfo("player", PlayerType.Human, null, "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, null, "other_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
]);
|
||||
|
||||
player = game.player("player_id");
|
||||
player.addGold(BigInt(1000000));
|
||||
|
||||
@@ -19,20 +19,13 @@ describe("Shell Random Damage", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("attacker", PlayerType.Human, null, "player_1_id"),
|
||||
new PlayerInfo("defender", PlayerType.Human, null, "player_2_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
player2 = game.player("player_2_id");
|
||||
});
|
||||
|
||||
@@ -23,10 +23,6 @@ describe("Stats", () => {
|
||||
new PlayerInfo("boat dude", PlayerType.Human, "client2", "player_2_id"),
|
||||
]);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
player2 = game.player("player_2_id");
|
||||
});
|
||||
|
||||
@@ -21,22 +21,18 @@ describe("Warship", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("boat dude", PlayerType.Human, null, "player_1_id"),
|
||||
new PlayerInfo("boat dude", PlayerType.Human, null, "player_2_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player1 = game.player("player_1_id");
|
||||
player2 = game.player("player_2_id");
|
||||
|
||||
// Advance past the manualMoveRetreatDisabledDuration window.
|
||||
executeTicks(game, 50);
|
||||
});
|
||||
|
||||
test("Warship heals only if player has port", async () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ describe("Warship multi-selection (MoveWarshipExecution)", () => {
|
||||
new PlayerInfo("p2", PlayerType.Human, null, "p2"),
|
||||
],
|
||||
);
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
player1 = game.player("p1");
|
||||
player2 = game.player("p2");
|
||||
});
|
||||
|
||||
@@ -27,13 +27,11 @@ describe("Spawn execution", () => {
|
||||
spawnExecutions.push(new SpawnExecution("game_id", playerInfo));
|
||||
}
|
||||
|
||||
const game = await setup(mapName, undefined, players);
|
||||
const game = await setup(mapName, {}, players);
|
||||
|
||||
game.addExecution(...spawnExecutions);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
|
||||
game.allPlayers().forEach((player) => {
|
||||
const spawnTile = player.spawnTile()!;
|
||||
@@ -73,13 +71,11 @@ describe("Spawn execution", () => {
|
||||
spawnExecutions.push(new SpawnExecution("game_id", playerInfo));
|
||||
}
|
||||
|
||||
const game = await setup("half_land_half_ocean", undefined, players);
|
||||
const game = await setup("half_land_half_ocean", {}, players);
|
||||
|
||||
game.addExecution(...spawnExecutions);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
|
||||
// Should spawn fewer than requested when map is too small
|
||||
expect(
|
||||
@@ -96,14 +92,12 @@ describe("Spawn execution", () => {
|
||||
`player_id`,
|
||||
);
|
||||
|
||||
const game = await setup("half_land_half_ocean", undefined, [playerInfo]);
|
||||
const game = await setup("half_land_half_ocean", {}, [playerInfo]);
|
||||
|
||||
game.addExecution(new SpawnExecution("game_id", playerInfo, 10));
|
||||
game.addExecution(new SpawnExecution("game_id", playerInfo, 20));
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.executeNextTick();
|
||||
|
||||
expect(game.playerByClientID("client_id")?.spawnTile()).toBe(20);
|
||||
// Previous territory from first spawn should be relinquished
|
||||
|
||||
@@ -18,20 +18,13 @@ describe("MIRVExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"big_plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
otherPlayer = game.player("other_id");
|
||||
|
||||
|
||||
@@ -17,20 +17,13 @@ describe("PlayerExecution Annexation Bug", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"big_plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("large", PlayerType.Human, "client1", "large_id"),
|
||||
new PlayerInfo("small", PlayerType.Human, "client2", "small_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
largePlayer = game.player("large_id");
|
||||
smallPlayer = game.player("small_id");
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ describe("NukeExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"big_plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
|
||||
@@ -34,10 +31,6 @@ describe("NukeExecution", () => {
|
||||
}));
|
||||
(game.config() as TestConfig).nukeAllianceBreakThreshold = vi.fn(() => 5);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
otherPlayer = game.player("other_id");
|
||||
|
||||
|
||||
@@ -17,20 +17,13 @@ describe("PlayerExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"big_plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
},
|
||||
{ infiniteGold: true, instantBuild: true },
|
||||
[
|
||||
new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
otherPlayer = game.player("other_id");
|
||||
|
||||
|
||||
@@ -78,10 +78,6 @@ describe("SAM", () => {
|
||||
),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player("attacker_id");
|
||||
defender = game.player("defender_id");
|
||||
middle_defender = game.player("middle_defender_id");
|
||||
|
||||
@@ -65,12 +65,8 @@ describe("WinCheckExecution", () => {
|
||||
mg.numLandTiles = vi.fn(() => 100);
|
||||
mg.numTilesWithFallout = vi.fn(() => 0);
|
||||
mg.stats = vi.fn(() => ({ stats: () => ({ mocked: true }) }));
|
||||
// Advance ticks until timeElapsed (in seconds) >= maxTimerValue * 60
|
||||
// timeElapsed = (ticks - numSpawnPhaseTurns) / 10 =>
|
||||
// ticks >= numSpawnPhaseTurns + maxTimerValue * 600
|
||||
const threshold =
|
||||
mg.config().numSpawnPhaseTurns() +
|
||||
(mg.config().gameConfig().maxTimerValue ?? 0) * 600;
|
||||
mg.endSpawnPhase();
|
||||
const threshold = (mg.config().gameConfig().maxTimerValue ?? 0) * 600;
|
||||
while (mg.ticks() < threshold) {
|
||||
mg.executeNextTick();
|
||||
}
|
||||
@@ -109,9 +105,6 @@ describe("WinCheckExecution - Nation Winners", () => {
|
||||
const nation = game.player("nation_id");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign 81% of land to Nation
|
||||
const totalLand = game.numLandTiles();
|
||||
@@ -171,10 +164,7 @@ describe("WinCheckExecution - Nation Winners", () => {
|
||||
game.addPlayer(nationInfo);
|
||||
const nation = game.player("nation_id");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
game.endSpawnPhase();
|
||||
|
||||
// Give Nation 60% territory (below 80% threshold)
|
||||
// Give human 30% territory
|
||||
@@ -200,9 +190,7 @@ describe("WinCheckExecution - Nation Winners", () => {
|
||||
expect(nation.numTilesOwned()).toBeGreaterThan(human.numTilesOwned());
|
||||
|
||||
// Fast-forward game ticks past timer expiration
|
||||
const threshold =
|
||||
game.config().numSpawnPhaseTurns() +
|
||||
(game.config().gameConfig().maxTimerValue ?? 0) * 600;
|
||||
const threshold = (game.config().gameConfig().maxTimerValue ?? 0) * 600;
|
||||
while (game.ticks() < threshold) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
@@ -258,9 +246,6 @@ describe("WinCheckExecution - Nation Winners", () => {
|
||||
const nation3 = game.player("nation3_id");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign territories: Nation1 (85%), Nation2 (10%), Nation3 (5%)
|
||||
const totalLand = game.numLandTiles();
|
||||
@@ -327,9 +312,6 @@ describe("WinCheckExecution - Nation Winners", () => {
|
||||
expect(bot2.team()).toBe(ColoredTeams.Bot);
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign 96% of land to bot team (above 95% Team mode threshold)
|
||||
const totalLand = game.numLandTiles();
|
||||
@@ -392,9 +374,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
|
||||
const human2 = game.player("Player2");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign some territory to both players
|
||||
let human1Count = 0;
|
||||
@@ -447,9 +426,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
|
||||
const human2 = game.player("Player2");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign territory to both players
|
||||
let human1Count = 0;
|
||||
@@ -503,9 +479,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
|
||||
const human2 = game.player("Player2");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Both players disconnect
|
||||
human1.markDisconnected(true);
|
||||
@@ -547,9 +520,6 @@ describe("WinCheckExecution - 1v1 Ranked Mode", () => {
|
||||
const nation = game.player("NationPlayer");
|
||||
|
||||
// Skip spawn phase
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Assign territory to all players
|
||||
let humanCount = 0;
|
||||
|
||||
@@ -5,11 +5,13 @@ import { SpawnExecution } from "../../../src/core/execution/SpawnExecution";
|
||||
import { AllianceRequestExecution } from "../../../src/core/execution/alliance/AllianceRequestExecution";
|
||||
import {
|
||||
Game,
|
||||
GameType,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
} from "../../../src/core/game/Game";
|
||||
import { TileRef } from "../../../src/core/game/GameMap";
|
||||
import { GameUpdateType } from "../../../src/core/game/GameUpdates";
|
||||
import { setup } from "../../util/Setup";
|
||||
|
||||
const gameID: GameID = "game_id";
|
||||
@@ -57,10 +59,6 @@ describe("GameImpl", () => {
|
||||
),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player(attackerInfo.id);
|
||||
defender = game.player(defenderInfo.id);
|
||||
});
|
||||
@@ -132,4 +130,43 @@ describe("GameImpl", () => {
|
||||
expect(attacker.isTraitor()).toBe(true);
|
||||
expect(attacker.allianceWith(defender)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Singleplayer late human spawn gets spawn immunity", async () => {
|
||||
const singleplayerGame = await setup(
|
||||
"plains",
|
||||
{
|
||||
gameType: GameType.Singleplayer,
|
||||
},
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
(singleplayerGame.config() as any).setSpawnImmunityDuration(100);
|
||||
|
||||
const pastSpawnCountdown =
|
||||
singleplayerGame.config().numSpawnPhaseTurns() + 20;
|
||||
for (let i = 0; i < pastSpawnCountdown; i++) {
|
||||
singleplayerGame.executeNextTick();
|
||||
}
|
||||
|
||||
const lateHumanInfo = new PlayerInfo(
|
||||
"late human",
|
||||
PlayerType.Human,
|
||||
"late_client_id",
|
||||
"late_player_id",
|
||||
);
|
||||
|
||||
singleplayerGame.addExecution(
|
||||
new SpawnExecution(gameID, lateHumanInfo, singleplayerGame.ref(5, 5)),
|
||||
);
|
||||
|
||||
// First tick initializes the execution, second tick applies the spawn.
|
||||
singleplayerGame.executeNextTick();
|
||||
const spawnUpdates = singleplayerGame.executeNextTick();
|
||||
|
||||
expect(singleplayerGame.player(lateHumanInfo.id).hasSpawned()).toBe(true);
|
||||
expect(spawnUpdates[GameUpdateType.SpawnPhaseEnd]).toHaveLength(1);
|
||||
expect(singleplayerGame.isSpawnImmunityActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ function addPlayer(game: Game, tile: TileRef): Player {
|
||||
const info = new PlayerInfo("test", PlayerType.Human, null, "test_id");
|
||||
game.addPlayer(info);
|
||||
game.addExecution(new SpawnExecution("game_id", info, tile));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
return game.player(info.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { NukeExecution } from "../../src/core/execution/NukeExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -8,12 +7,10 @@ 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(
|
||||
@@ -34,14 +31,10 @@ describe("Construction economy", () => {
|
||||
},
|
||||
[builderInfo, otherInfo],
|
||||
);
|
||||
const spawn = game.ref(0, 10);
|
||||
game.addExecution(new SpawnExecution(gameID, builderInfo, spawn));
|
||||
game.addExecution(new SpawnExecution(gameID, otherInfo, spawn));
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
player = game.player(builderInfo.id);
|
||||
other = game.player(otherInfo.id);
|
||||
player.conquer(game.ref(0, 10));
|
||||
other.conquer(game.ref(10, 10));
|
||||
});
|
||||
|
||||
test("City charges gold once and no refund thereafter (allow passive income)", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -7,22 +6,18 @@ 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";
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
|
||||
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(gameID, info, game.ref(1, 1)));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
game = await setup("plains", { infiniteGold: true, instantBuild: true }, [
|
||||
info,
|
||||
]);
|
||||
player = game.player(info.id);
|
||||
|
||||
player.conquer(game.ref(1, 1));
|
||||
});
|
||||
|
||||
@@ -52,17 +47,14 @@ describe("Hydrogen Bomb and MIRV flows", () => {
|
||||
|
||||
test("Hydrogen bomb launch fails when silo is under construction and succeeds after completion", async () => {
|
||||
// Set up a game without instantBuild to test construction duration
|
||||
const gameWithConstruction = await setup("plains", {
|
||||
const gameWithConstruction = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: false,
|
||||
instantBuild: false,
|
||||
});
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
gameWithConstruction.addPlayer(info);
|
||||
gameWithConstruction.addExecution(
|
||||
new SpawnExecution(gameID, info, gameWithConstruction.ref(1, 1)),
|
||||
},
|
||||
[info],
|
||||
);
|
||||
while (gameWithConstruction.inSpawnPhase())
|
||||
gameWithConstruction.executeNextTick();
|
||||
const playerWithConstruction = gameWithConstruction.player(info.id);
|
||||
|
||||
playerWithConstruction.conquer(gameWithConstruction.ref(1, 1));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { NukeExecution } from "../../src/core/execution/NukeExecution";
|
||||
import { SpawnExecution } from "../../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
@@ -8,12 +7,9 @@ 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 } from "../util/utils";
|
||||
|
||||
const gameID: GameID = "game_id";
|
||||
|
||||
function launchNukeAt(game: Game, player: Player, target: TileRef): void {
|
||||
game.addExecution(new NukeExecution(UnitType.AtomBomb, player, target, null));
|
||||
// init + build
|
||||
@@ -30,19 +26,21 @@ function tickUntilNukeLands(game: Game, maxTicks = 50): void {
|
||||
describe("Water Nukes", () => {
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
|
||||
describe("when waterNukes is enabled", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("plains", {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
waterNukes: true,
|
||||
});
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
game.addPlayer(info);
|
||||
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
},
|
||||
[info],
|
||||
);
|
||||
player = game.player(info.id);
|
||||
player.conquer(game.ref(1, 1));
|
||||
|
||||
// Build a missile silo
|
||||
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
||||
@@ -111,19 +109,18 @@ describe("Water Nukes", () => {
|
||||
|
||||
test("waterGraphVersion increments after water conversion", async () => {
|
||||
// Need a game with nav mesh enabled for graph rebuilds
|
||||
const navGame = await setup("plains", {
|
||||
const navGame = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
waterNukes: true,
|
||||
disableNavMesh: false,
|
||||
});
|
||||
const info2 = new PlayerInfo("p2", PlayerType.Human, null, "p2");
|
||||
navGame.addPlayer(info2);
|
||||
navGame.addExecution(
|
||||
new SpawnExecution(gameID, info2, navGame.ref(1, 1)),
|
||||
},
|
||||
[info],
|
||||
);
|
||||
while (navGame.inSpawnPhase()) navGame.executeNextTick();
|
||||
const player2 = navGame.player(info2.id);
|
||||
const player2 = navGame.player(info.id);
|
||||
player2.conquer(navGame.ref(1, 1));
|
||||
constructionExecution(navGame, player2, 1, 1, UnitType.MissileSilo);
|
||||
|
||||
const versionBefore = navGame.waterGraphVersion();
|
||||
@@ -143,16 +140,17 @@ describe("Water Nukes", () => {
|
||||
|
||||
describe("when waterNukes is disabled (default)", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("plains", {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
waterNukes: false,
|
||||
});
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
game.addPlayer(info);
|
||||
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
},
|
||||
[info],
|
||||
);
|
||||
player = game.player(info.id);
|
||||
player.conquer(game.ref(1, 1));
|
||||
|
||||
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
||||
});
|
||||
@@ -182,16 +180,17 @@ describe("Water Nukes", () => {
|
||||
|
||||
describe("updateTile terrain byte round-trip", () => {
|
||||
test("terrain byte is packed and unpacked correctly", async () => {
|
||||
game = await setup("plains", {
|
||||
game = await setup(
|
||||
"plains",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
waterNukes: true,
|
||||
});
|
||||
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
||||
game.addPlayer(info);
|
||||
game.addExecution(new SpawnExecution(gameID, info, game.ref(1, 1)));
|
||||
while (game.inSpawnPhase()) game.executeNextTick();
|
||||
},
|
||||
[info],
|
||||
);
|
||||
player = game.player(info.id);
|
||||
player.conquer(game.ref(1, 1));
|
||||
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
||||
|
||||
const target = game.ref(10, 10);
|
||||
|
||||
@@ -16,10 +16,6 @@ const sparseTerritoryGame = await setup(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
);
|
||||
|
||||
while (sparseTerritoryGame.inSpawnPhase()) {
|
||||
sparseTerritoryGame.executeNextTick();
|
||||
}
|
||||
|
||||
const sparsePlayer = sparseTerritoryGame.player("player_id");
|
||||
|
||||
function claimRow(y: number, length: number) {
|
||||
@@ -56,10 +52,6 @@ const denseTerritoryGame = await setup(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
);
|
||||
|
||||
while (denseTerritoryGame.inSpawnPhase()) {
|
||||
denseTerritoryGame.executeNextTick();
|
||||
}
|
||||
|
||||
const densePlayer = denseTerritoryGame.player("player_id");
|
||||
|
||||
for (let x = 0; x < 200; x++) {
|
||||
@@ -84,10 +76,6 @@ const giantMapGame = await setup(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
);
|
||||
|
||||
while (giantMapGame.inSpawnPhase()) {
|
||||
giantMapGame.executeNextTick();
|
||||
}
|
||||
|
||||
const giantMapPlayer = giantMapGame.player("player_id");
|
||||
|
||||
// Conquer ALL available land tiles on the giant world map
|
||||
|
||||
+4
-1
@@ -26,6 +26,7 @@ export async function setup(
|
||||
humans: PlayerInfo[] = [],
|
||||
currentDir: string = __dirname,
|
||||
ConfigClass: typeof TestConfig = TestConfig,
|
||||
autoEndSpawnPhase: boolean = true,
|
||||
): Promise<Game> {
|
||||
// Suppress console.debug for tests.
|
||||
console.debug = () => {};
|
||||
@@ -78,7 +79,9 @@ export async function setup(
|
||||
false,
|
||||
);
|
||||
|
||||
return createGame(humans, [], gameMap, miniGameMap, config);
|
||||
const game = createGame(humans, [], gameMap, miniGameMap, config);
|
||||
if (autoEndSpawnPhase) game.endSpawnPhase();
|
||||
return game;
|
||||
}
|
||||
|
||||
export function playerInfo(name: string, type: PlayerType): PlayerInfo {
|
||||
|
||||
Reference in New Issue
Block a user