This commit is contained in:
evanpelle
2026-05-01 16:20:34 -06:00
parent a93c466334
commit 33e1ad9897
46 changed files with 230 additions and 238 deletions
+1 -1
View File
@@ -524,7 +524,7 @@ export class ClientGameRunner {
if (
!this.gameView.inSpawnPhase() &&
!hasGoneToPlayer &&
this.gameView.myPlayer()
this.gameView.myPlayer()?.nameLocation()
) {
hasGoneToPlayer = true;
this.eventBus.emit(new GoToPlayerEvent(this.gameView.myPlayer()!, 8));
@@ -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;
}
+12 -1
View File
@@ -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
View File
@@ -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,
@@ -104,6 +106,9 @@ export class GameRunner {
if (this.game.config().spawnNations()) {
this.game.addExecution(...this.execManager.nationExecutions());
}
if (this.game.config().gameConfig().gameType !== GameType.Singleplayer) {
this.game.addExecution(new SpawnTimerExecution());
}
this.game.addExecution(new WinCheckExecution());
if (!this.game.config().isUnitDisabled(UnitType.Factory)) {
this.game.addExecution(
@@ -130,6 +135,7 @@ export class GameRunner {
);
this.currTurn++;
const wasInSpawnPhase = this.game.inSpawnPhase();
let updates: GameUpdates;
let tickExecutionDuration: number = 0;
@@ -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);
});
+10 -5
View File
@@ -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 {
+23
View File
@@ -0,0 +1,23 @@
import { Execution, Game } from "../game/Game";
export class SpawnTimerExecution implements Execution {
private mg: Game;
init(mg: Game, ticks: number): void {
this.mg = mg;
}
tick(ticks: number): void {
if (this.mg.ticks() > this.mg.config().numSpawnPhaseTurns()) {
this.mg.endSpawnPhase();
}
}
isActive(): boolean {
return this.mg.inSpawnPhase();
}
activeDuringSpawnPhase(): boolean {
return true;
}
}
+2 -4
View File
@@ -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;
+2
View File
@@ -865,10 +865,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;
+38 -7
View File
@@ -18,6 +18,7 @@ import {
Execution,
Game,
GameMode,
GameType,
GameUpdates,
HumansVsNations,
MessageType,
@@ -76,6 +77,7 @@ export type CellString = string;
export class GameImpl implements Game {
private _ticks = 0;
private startTick: number | null = null;
private unInitExecs: Execution[] = [];
@@ -409,7 +411,15 @@ export class GameImpl implements Game {
}
inSpawnPhase(): boolean {
return this._ticks <= this.config().numSpawnPhaseTurns();
return this.startTick === null;
}
endSpawnPhase(): void {
this.startTick = this._ticks;
this.addUpdate({
type: GameUpdateType.SpawnPhaseEnd,
startTick: this.startTick,
});
}
ticks(): number {
@@ -458,6 +468,17 @@ export class GameImpl implements Game {
for (const tile of waterChangedTiles) {
this.recordTileUpdate(tile);
}
if (
this.config().gameConfig().gameType !== GameType.Singleplayer &&
this._ticks === this.startTick
) {
this.addUpdate({
type: GameUpdateType.SpawnPhaseEnd,
startTick: this.startTick,
});
}
this._ticks++;
return this.updates;
}
@@ -819,20 +840,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,
+7
View File
@@ -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;
+28 -6
View File
@@ -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;
}
-5
View File
@@ -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,
-4
View File
@@ -34,10 +34,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");
-4
View File
@@ -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", () => {
-4
View File
@@ -27,10 +27,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", () => {
-4
View File
@@ -30,10 +30,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", () => {
+8 -16
View File
@@ -69,10 +69,8 @@ describe("Attack", () => {
defenderSpawn,
),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
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(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
});
it("Should not mark attacker as traitor when alliance is formed after attack starts", async () => {
@@ -357,10 +353,8 @@ describe("Transport ship alliance rejection", () => {
"playerB_id",
);
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
});
test("Should cancel alliance requests if the recipient sends a transport ship", async () => {
@@ -407,10 +401,8 @@ describe("Attack immunity", () => {
"playerB_id",
);
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
});
test("Should not be able to attack during immunity phase", async () => {
+2 -4
View File
@@ -26,10 +26,8 @@ describe("AttackStats", () => {
game.addExecution(
new SpawnExecution(gameID, player2.info(), game.ref(50, 55)),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
});
test("should increase war gold stat when a player is eliminated", () => {
-4
View File
@@ -59,10 +59,6 @@ describe("DeleteUnitExecution Security Tests", () => {
),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
executeTicks(game, game.config().deleteUnitCooldown() + 1);
player = game.player(player1Info.id);
+8 -28
View File
@@ -51,10 +51,8 @@ describe("Disconnected", () => {
new SpawnExecution(gameID, player1Info, game.ref(1, 1)),
new SpawnExecution(gameID, player2Info, game.ref(7, 7)),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
});
describe("Player disconnected state", () => {
@@ -212,10 +210,8 @@ describe("Disconnected", () => {
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();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
player1 = game.player(player1Info.id);
player2 = game.player(player2Info.id);
@@ -290,32 +286,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();
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", () => {
-16
View File
@@ -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();
-2
View File
@@ -7,7 +7,6 @@ import {
GameMapSize,
GameMapType,
GameMode,
GameType,
} from "../src/core/game/Game";
import { AnalyticsRecord, GameConfig } from "../src/core/Schemas";
import {
@@ -24,7 +23,6 @@ describe("Ranking class", () => {
difficulty: Difficulty.Medium,
donateGold: false,
donateTroops: false,
gameType: GameType.Public,
gameMode: GameMode.FFA,
gameMapSize: GameMapSize.Normal,
nations: "disabled",
-4
View File
@@ -50,10 +50,6 @@ describe("MissileSilo", () => {
),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
attacker = game.player("attacker_id");
constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
+1 -5
View File
@@ -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");
-15
View File
@@ -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");
-4
View File
@@ -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");
-4
View File
@@ -24,10 +24,6 @@ describe("PlayerImpl", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player("player_id");
other = game.player("other_id");
-4
View File
@@ -25,10 +25,6 @@ describe("PortExecution", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player = game.player("player_id");
player.addGold(BigInt(1000000));
other = game.player("other_id");
-4
View File
@@ -29,10 +29,6 @@ describe("Shell Random Damage", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player1 = game.player("player_1_id");
player2 = game.player("player_2_id");
});
-4
View File
@@ -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");
});
+3 -4
View File
@@ -31,12 +31,11 @@ describe("Warship", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
player1 = game.player("player_1_id");
player2 = game.player("player_2_id");
// Advance past the 50-tick manualMoveRetreatDisabledDuration window.
executeTicks(game, 50);
});
test("Warship heals only if player has port", async () => {
-1
View File
@@ -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");
});
+24 -3
View File
@@ -27,7 +27,14 @@ describe("Spawn execution", () => {
spawnExecutions.push(new SpawnExecution("game_id", playerInfo));
}
const game = await setup(mapName, undefined, players);
const game = await setup(
mapName,
{},
players,
undefined,
undefined,
false,
);
game.addExecution(...spawnExecutions);
@@ -73,7 +80,14 @@ 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,
undefined,
undefined,
false,
);
game.addExecution(...spawnExecutions);
@@ -96,7 +110,14 @@ describe("Spawn execution", () => {
`player_id`,
);
const game = await setup("half_land_half_ocean", undefined, [playerInfo]);
const game = await setup(
"half_land_half_ocean",
{},
[playerInfo],
undefined,
undefined,
false,
);
game.addExecution(new SpawnExecution("game_id", playerInfo, 10));
game.addExecution(new SpawnExecution("game_id", playerInfo, 20));
+1 -3
View File
@@ -28,9 +28,7 @@ describe("MIRVExecution", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
game.endSpawnPhase();
player = game.player("player_id");
otherPlayer = game.player("other_id");
@@ -27,10 +27,6 @@ describe("PlayerExecution Annexation Bug", () => {
],
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
largePlayer = game.player("large_id");
smallPlayer = game.player("small_id");
@@ -34,10 +34,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");
@@ -27,10 +27,6 @@ describe("PlayerExecution", () => {
],
);
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");
@@ -109,9 +109,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();
@@ -172,9 +169,6 @@ describe("WinCheckExecution - Nation Winners", () => {
const nation = game.player("nation_id");
// Skip spawn phase
while (game.inSpawnPhase()) {
game.executeNextTick();
}
// Give Nation 60% territory (below 80% threshold)
// Give human 30% territory
@@ -258,9 +252,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 +318,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 +380,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 +432,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 +485,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 +526,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;
+34 -4
View File
@@ -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,36 @@ 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,
});
(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);
});
});
+2 -1
View File
@@ -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(); // init SpawnExecution
game.executeNextTick(); // tick SpawnExecution → player gets territory
return game.player(info.id);
}
+2 -3
View File
@@ -37,9 +37,8 @@ describe("Construction economy", () => {
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();
}
game.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → player gets territory
player = game.player(builderInfo.id);
other = game.player(otherInfo.id);
});
+4 -3
View File
@@ -20,7 +20,8 @@ describe("Hydrogen Bomb and MIRV flows", () => {
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.executeNextTick(); // init spawns
game.executeNextTick(); // tick spawns → players get territory
player = game.player(info.id);
player.conquer(game.ref(1, 1));
@@ -61,8 +62,8 @@ describe("Hydrogen Bomb and MIRV flows", () => {
gameWithConstruction.addExecution(
new SpawnExecution(gameID, info, gameWithConstruction.ref(1, 1)),
);
while (gameWithConstruction.inSpawnPhase())
gameWithConstruction.executeNextTick();
gameWithConstruction.executeNextTick(); // init spawns
gameWithConstruction.executeNextTick(); // tick spawns → players get territory
const playerWithConstruction = gameWithConstruction.player(info.id);
playerWithConstruction.conquer(gameWithConstruction.ref(1, 1));
-4
View File
@@ -41,7 +41,6 @@ describe("Water Nukes", () => {
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();
player = game.player(info.id);
// Build a missile silo
@@ -122,7 +121,6 @@ describe("Water Nukes", () => {
navGame.addExecution(
new SpawnExecution(gameID, info2, navGame.ref(1, 1)),
);
while (navGame.inSpawnPhase()) navGame.executeNextTick();
const player2 = navGame.player(info2.id);
constructionExecution(navGame, player2, 1, 1, UnitType.MissileSilo);
@@ -151,7 +149,6 @@ describe("Water Nukes", () => {
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();
player = game.player(info.id);
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
@@ -190,7 +187,6 @@ describe("Water Nukes", () => {
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();
player = game.player(info.id);
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
+4 -1
View File
@@ -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 {