From ef5f38deeb29df19b836ed955433359a9c9586ad Mon Sep 17 00:00:00 2001 From: Kipstz Avenger <140314732+Kipstz@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:43:20 +0200 Subject: [PATCH 01/21] Remove spawn timer on singleplayer --- src/client/ClientGameRunner.ts | 2 +- src/core/GameRunner.ts | 22 +++++++-- src/core/configuration/DefaultConfig.ts | 2 +- .../execution/DelayedBotSpawnExecution.ts | 45 +++++++++++++++++++ src/core/execution/SpawnExecution.ts | 21 ++++++++- 5 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/core/execution/DelayedBotSpawnExecution.ts diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index c3c093426d..3a9b436d25 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -12,7 +12,7 @@ import { import { createPartialGameRecord, replacer } from "../core/Util"; import { ServerConfig } from "../core/configuration/Config"; import { getConfig } from "../core/configuration/ConfigLoader"; -import { PlayerActions, UnitType } from "../core/game/Game"; +import { GameType, PlayerActions, UnitType } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { GameMapLoader } from "../core/game/GameMapLoader"; import { diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 6a8a4042f3..e87cb88fe4 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -1,5 +1,6 @@ import { placeName } from "../client/graphics/NameBoxCalculator"; import { getConfig } from "./configuration/ConfigLoader"; +import { DelayedBotSpawnExecution } from "./execution/DelayedBotSpawnExecution"; import { Executor } from "./execution/ExecutionManager"; import { WinCheckExecution } from "./execution/WinCheckExecution"; import { @@ -7,6 +8,7 @@ import { Attack, Cell, Game, + GameType, GameUpdates, NameViewData, Nation, @@ -85,6 +87,7 @@ export async function createGameRunner( game, new Executor(game, gameStart.gameID, clientID), callBack, + gameStart, ); gr.init(); return gr; @@ -101,6 +104,7 @@ export class GameRunner { public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, + private gameStartInfo: GameStartInfo, ) {} init() { @@ -108,9 +112,15 @@ export class GameRunner { this.game.addExecution(...this.execManager.spawnPlayers()); } if (this.game.config().bots() > 0) { - this.game.addExecution( - ...this.execManager.spawnBots(this.game.config().numBots()), - ); + if (this.game.config().gameConfig().gameType === GameType.Singleplayer) { + this.game.addExecution( + new DelayedBotSpawnExecution(this.gameStartInfo.gameID), + ); + } else { + this.game.addExecution( + ...this.execManager.spawnBots(this.game.config().numBots()), + ); + } } if (this.game.config().spawnNPCs()) { this.game.addExecution(...this.execManager.fakeHumanExecutions()); @@ -157,7 +167,11 @@ export class GameRunner { return; } - if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) { + if ( + (this.game.inSpawnPhase() || + this.game.config().gameConfig().gameType === GameType.Singleplayer) && + this.game.ticks() % 2 === 0 + ) { this.game .players() .filter( diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 4a215da49b..0870400135 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -626,7 +626,7 @@ export class DefaultConfig implements Config { return 3; } numSpawnPhaseTurns(): number { - return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300; + return this._gameConfig.gameType === GameType.Singleplayer ? 0 : 300; } numBots(): number { return this.bots(); diff --git a/src/core/execution/DelayedBotSpawnExecution.ts b/src/core/execution/DelayedBotSpawnExecution.ts new file mode 100644 index 0000000000..0c604219b9 --- /dev/null +++ b/src/core/execution/DelayedBotSpawnExecution.ts @@ -0,0 +1,45 @@ +import { Execution, Game, PlayerType } from "../game/Game"; +import { BotSpawner } from "./BotSpawner"; + +export class DelayedBotSpawnExecution implements Execution { + private active = true; + private mg: Game; + private botsSpawned = false; + + constructor(private gameID: string) {} + + init(mg: Game, ticks: number) { + this.mg = mg; + } + + tick(ticks: number) { + if (this.botsSpawned) { + this.active = false; + return; + } + + const hasHumanSpawned = this.mg + .players() + .some( + (player) => player.type() === PlayerType.Human && player.hasSpawned(), + ); + + if (hasHumanSpawned) { + const botSpawner = new BotSpawner(this.mg, this.gameID); + const botSpawns = botSpawner.spawnBots(this.mg.config().numBots()); + + this.mg.addExecution(...botSpawns); + + this.botsSpawned = true; + this.active = false; + } + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return true; + } +} diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 57baff6eeb..4e9b34f7ec 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -1,4 +1,11 @@ -import { Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game"; +import { + Execution, + Game, + GameType, + Player, + PlayerInfo, + PlayerType, +} from "../game/Game"; import { TileRef } from "../game/GameMap"; import { BotExecution } from "./BotExecution"; import { PlayerExecution } from "./PlayerExecution"; @@ -20,12 +27,22 @@ export class SpawnExecution implements Execution { tick(ticks: number) { this.active = false; + console.log( + `SpawnExecution tick ${ticks}: player=${this.playerInfo.name}, tile=${this.tile}, inSpawnPhase=${this.mg.inSpawnPhase()}, gameType=${this.mg.config().gameConfig().gameType}`, + ); + if (!this.mg.isValidRef(this.tile)) { console.warn(`SpawnExecution: tile ${this.tile} not valid`); return; } - if (!this.mg.inSpawnPhase()) { + if ( + !this.mg.inSpawnPhase() && + this.mg.config().gameConfig().gameType !== GameType.Singleplayer + ) { + console.log( + `SpawnExecution: spawn phase ended, not singleplayer, aborting`, + ); this.active = false; return; } From d287d6a406614218089a5041626fc5d7ac69f6a2 Mon Sep 17 00:00:00 2001 From: Kipstz Avenger <140314732+Kipstz@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:01:58 +0200 Subject: [PATCH 02/21] update: reviews --- src/core/execution/DelayedBotSpawnExecution.ts | 14 ++++++++++++++ src/core/execution/SpawnExecution.ts | 7 ------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/core/execution/DelayedBotSpawnExecution.ts b/src/core/execution/DelayedBotSpawnExecution.ts index 0c604219b9..09b726f297 100644 --- a/src/core/execution/DelayedBotSpawnExecution.ts +++ b/src/core/execution/DelayedBotSpawnExecution.ts @@ -5,6 +5,8 @@ export class DelayedBotSpawnExecution implements Execution { private active = true; private mg: Game; private botsSpawned = false; + private tickCount = 0; + private readonly MAX_WAIT_TICKS = 6000; constructor(private gameID: string) {} @@ -13,11 +15,23 @@ export class DelayedBotSpawnExecution implements Execution { } tick(ticks: number) { + this.tickCount++; + if (this.botsSpawned) { this.active = false; return; } + if (this.tickCount >= this.MAX_WAIT_TICKS) { + console.warn("DelayedBotSpawnExecution: No human spawned after timeout, spawning bots anyway"); + const botSpawner = new BotSpawner(this.mg, this.gameID); + const botSpawns = botSpawner.spawnBots(this.mg.config().numBots()); + this.mg.addExecution(...botSpawns); + this.botsSpawned = true; + this.active = false; + return; + } + const hasHumanSpawned = this.mg .players() .some( diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 4e9b34f7ec..26701a25fd 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -27,10 +27,6 @@ export class SpawnExecution implements Execution { tick(ticks: number) { this.active = false; - console.log( - `SpawnExecution tick ${ticks}: player=${this.playerInfo.name}, tile=${this.tile}, inSpawnPhase=${this.mg.inSpawnPhase()}, gameType=${this.mg.config().gameConfig().gameType}`, - ); - if (!this.mg.isValidRef(this.tile)) { console.warn(`SpawnExecution: tile ${this.tile} not valid`); return; @@ -40,9 +36,6 @@ export class SpawnExecution implements Execution { !this.mg.inSpawnPhase() && this.mg.config().gameConfig().gameType !== GameType.Singleplayer ) { - console.log( - `SpawnExecution: spawn phase ended, not singleplayer, aborting`, - ); this.active = false; return; } From 454bdf88a1d8d7f1ed62e03e84a0492a4b5997bb Mon Sep 17 00:00:00 2001 From: Kipstz Avenger <140314732+Kipstz@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:04:27 +0200 Subject: [PATCH 03/21] format / lint --- src/core/execution/DelayedBotSpawnExecution.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/execution/DelayedBotSpawnExecution.ts b/src/core/execution/DelayedBotSpawnExecution.ts index 09b726f297..a524fd87d3 100644 --- a/src/core/execution/DelayedBotSpawnExecution.ts +++ b/src/core/execution/DelayedBotSpawnExecution.ts @@ -16,14 +16,16 @@ export class DelayedBotSpawnExecution implements Execution { tick(ticks: number) { this.tickCount++; - + if (this.botsSpawned) { this.active = false; return; } if (this.tickCount >= this.MAX_WAIT_TICKS) { - console.warn("DelayedBotSpawnExecution: No human spawned after timeout, spawning bots anyway"); + console.warn( + "DelayedBotSpawnExecution: No human spawned after timeout, spawning bots anyway", + ); const botSpawner = new BotSpawner(this.mg, this.gameID); const botSpawns = botSpawner.spawnBots(this.mg.config().numBots()); this.mg.addExecution(...botSpawns); From 5f892415b33d9c330e18d602cf6b3a8b3056a2f0 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:59:39 +0100 Subject: [PATCH 04/21] Merge --- src/client/graphics/layers/HeadsUpMessage.ts | 9 +- src/client/graphics/layers/SpawnAd.ts | 142 +++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/client/graphics/layers/SpawnAd.ts diff --git a/src/client/graphics/layers/HeadsUpMessage.ts b/src/client/graphics/layers/HeadsUpMessage.ts index 9ab20dae35..b9147c854c 100644 --- a/src/client/graphics/layers/HeadsUpMessage.ts +++ b/src/client/graphics/layers/HeadsUpMessage.ts @@ -1,6 +1,7 @@ import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { GameView } from "../../../core/game/GameView"; +import { GameType } from "../../../core/game/Game"; import { translateText } from "../../Utils"; import { Layer } from "./Layer"; @@ -21,7 +22,13 @@ export class HeadsUpMessage extends LitElement implements Layer { } tick() { - if (!this.game.inSpawnPhase()) { + // Check if we should show the spawn message + const shouldShowSpawnMessage = + this.game.inSpawnPhase() || + (this.game.config().gameConfig().gameType === GameType.Singleplayer && + !this.game.myPlayer()?.hasSpawned()); + + if (!shouldShowSpawnMessage) { this.isVisible = false; this.requestUpdate(); } diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts new file mode 100644 index 0000000000..404c48886f --- /dev/null +++ b/src/client/graphics/layers/SpawnAd.ts @@ -0,0 +1,142 @@ +import { LitElement, css, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { translateText } from "../../../client/Utils"; +import { GameView } from "../../../core/game/GameView"; +import { GameType } from "../../../core/game/Game"; +import { getGamesPlayed } from "../../Utils"; +import { Layer } from "./Layer"; + +const AD_TYPE = "bottom_rail"; +const AD_CONTAINER_ID = "bottom-rail-ad-container"; + +@customElement("spawn-ad") +export class SpawnAd extends LitElement implements Layer { + public g: GameView; + + @state() + private isVisible: boolean = false; + + @state() + private adLoaded: boolean = false; + + private gamesPlayed: number = 0; + + // Override createRenderRoot to disable shadow DOM + createRenderRoot() { + return this; + } + + static styles = css``; + + constructor() { + super(); + } + + init() { + this.gamesPlayed = getGamesPlayed(); + } + + public show(): void { + this.isVisible = true; + this.loadAd(); + this.requestUpdate(); + } + + public hide(): void { + // Destroy the ad when hiding + this.destroyAd(); + this.isVisible = false; + this.adLoaded = false; + this.requestUpdate(); + } + + public async tick() { + // Check if we should show the spawn ad + const shouldShowSpawnAd = + this.g.inSpawnPhase() || + (this.g.config().gameConfig().gameType === GameType.Singleplayer && + !this.g.myPlayer()?.hasSpawned()); + + if ( + !this.isVisible && + shouldShowSpawnAd && + this.g.ticks() > 10 && + this.gamesPlayed > 5 + ) { + console.log("not showing spawn ad"); + // this.show(); + } + if (this.isVisible && !shouldShowSpawnAd) { + console.log("hiding bottom left ad"); + this.hide(); + } + } + + private loadAd(): void { + if (!window.ramp) { + console.warn("Playwire RAMP not available"); + return; + } + if (this.adLoaded) { + console.log("Ad already loaded, skipping"); + return; + } + try { + window.ramp.que.push(() => { + window.ramp.spaAddAds([ + { + type: AD_TYPE, + selectorId: AD_CONTAINER_ID, + }, + ]); + this.adLoaded = true; + console.log("Playwire ad loaded:", AD_TYPE); + }); + } catch (error) { + console.error("Failed to load Playwire ad:", error); + } + } + + private destroyAd(): void { + if (!window.ramp || !this.adLoaded) { + return; + } + try { + window.ramp.que.push(() => { + window.ramp.destroyUnits("all"); + console.log("Playwire spawn ad destroyed"); + }); + } catch (error) { + console.error("Failed to destroy Playwire ad:", error); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + // Clean up ad when component is removed + this.destroyAd(); + } + + render() { + if (!this.isVisible) { + return html``; + } + + return html` +
+
+ ${!this.adLoaded + ? html`${translateText("spawn_ad.loading")}` + : ""} +
+
+ `; + } +} From bf84bd3538b058eba87f5c9ac371c17772e92dff Mon Sep 17 00:00:00 2001 From: Kipstz Avenger <140314732+Kipstz@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:59:34 +0200 Subject: [PATCH 05/21] Big refactor after previously reviews --- src/client/graphics/layers/HeadsUpMessage.ts | 9 +-- src/client/graphics/layers/SpawnAd.ts | 20 +++--- src/client/graphics/layers/SpawnTimer.ts | 5 ++ src/core/GameRunner.ts | 53 ++++++++++------ src/core/configuration/DefaultConfig.ts | 9 ++- src/core/execution/BotExecution.ts | 10 ++- .../execution/DelayedBotSpawnExecution.ts | 61 ------------------- src/core/execution/FakeHumanExecution.ts | 1 - src/core/execution/SpawnExecution.ts | 32 +++++----- src/core/game/GameImpl.ts | 9 +++ src/core/game/GameView.ts | 9 +++ src/server/GameManager.ts | 6 +- src/server/GameServer.ts | 9 +-- 13 files changed, 103 insertions(+), 130 deletions(-) delete mode 100644 src/core/execution/DelayedBotSpawnExecution.ts diff --git a/src/client/graphics/layers/HeadsUpMessage.ts b/src/client/graphics/layers/HeadsUpMessage.ts index b9147c854c..9ab20dae35 100644 --- a/src/client/graphics/layers/HeadsUpMessage.ts +++ b/src/client/graphics/layers/HeadsUpMessage.ts @@ -1,7 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { GameView } from "../../../core/game/GameView"; -import { GameType } from "../../../core/game/Game"; import { translateText } from "../../Utils"; import { Layer } from "./Layer"; @@ -22,13 +21,7 @@ export class HeadsUpMessage extends LitElement implements Layer { } tick() { - // Check if we should show the spawn message - const shouldShowSpawnMessage = - this.game.inSpawnPhase() || - (this.game.config().gameConfig().gameType === GameType.Singleplayer && - !this.game.myPlayer()?.hasSpawned()); - - if (!shouldShowSpawnMessage) { + if (!this.game.inSpawnPhase()) { this.isVisible = false; this.requestUpdate(); } diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts index 404c48886f..b2868d470c 100644 --- a/src/client/graphics/layers/SpawnAd.ts +++ b/src/client/graphics/layers/SpawnAd.ts @@ -1,8 +1,8 @@ import { LitElement, css, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "../../../client/Utils"; -import { GameView } from "../../../core/game/GameView"; import { GameType } from "../../../core/game/Game"; +import { GameView } from "../../../core/game/GameView"; import { getGamesPlayed } from "../../Utils"; import { Layer } from "./Layer"; @@ -51,23 +51,19 @@ export class SpawnAd extends LitElement implements Layer { } public async tick() { - // Check if we should show the spawn ad - const shouldShowSpawnAd = - this.g.inSpawnPhase() || - (this.g.config().gameConfig().gameType === GameType.Singleplayer && - !this.g.myPlayer()?.hasSpawned()); - + if (this.g.config().gameConfig().gameType === GameType.Singleplayer) { + return; + } + if ( !this.isVisible && - shouldShowSpawnAd && + this.g.inSpawnPhase() && this.g.ticks() > 10 && this.gamesPlayed > 5 ) { - console.log("not showing spawn ad"); - // this.show(); + this.show(); } - if (this.isVisible && !shouldShowSpawnAd) { - console.log("hiding bottom left ad"); + if (this.isVisible && !this.g.inSpawnPhase()) { this.hide(); } } diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts index 393cf96d4d..13fbcf741b 100644 --- a/src/client/graphics/layers/SpawnTimer.ts +++ b/src/client/graphics/layers/SpawnTimer.ts @@ -32,6 +32,11 @@ export class SpawnTimer extends LitElement implements Layer { tick() { if (this.game.inSpawnPhase()) { + if (this.game.config().gameConfig().gameType === GameType.Singleplayer) { + this.ratios = []; + this.colors = []; + return; + } // During spawn phase, only one segment filling full width this.ratios = [ this.game.ticks() / this.game.config().numSpawnPhaseTurns(), diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index e87cb88fe4..3b6c8dca40 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -1,14 +1,13 @@ import { placeName } from "../client/graphics/NameBoxCalculator"; import { getConfig } from "./configuration/ConfigLoader"; -import { DelayedBotSpawnExecution } from "./execution/DelayedBotSpawnExecution"; import { Executor } from "./execution/ExecutionManager"; +import { SpawnExecution } from "./execution/SpawnExecution"; import { WinCheckExecution } from "./execution/WinCheckExecution"; import { AllPlayers, Attack, Cell, Game, - GameType, GameUpdates, NameViewData, Nation, @@ -87,7 +86,6 @@ export async function createGameRunner( game, new Executor(game, gameStart.gameID, clientID), callBack, - gameStart, ); gr.init(); return gr; @@ -97,6 +95,7 @@ export class GameRunner { private turns: Turn[] = []; private currTurn = 0; private isExecuting = false; + private nationsSpawned = false; private playerViewData: Record = {}; @@ -104,7 +103,6 @@ export class GameRunner { public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, - private gameStartInfo: GameStartInfo, ) {} init() { @@ -112,20 +110,25 @@ export class GameRunner { this.game.addExecution(...this.execManager.spawnPlayers()); } if (this.game.config().bots() > 0) { - if (this.game.config().gameConfig().gameType === GameType.Singleplayer) { - this.game.addExecution( - new DelayedBotSpawnExecution(this.gameStartInfo.gameID), - ); - } else { - this.game.addExecution( - ...this.execManager.spawnBots(this.game.config().numBots()), - ); - } + this.game.addExecution( + ...this.execManager.spawnBots(this.game.config().numBots()), + ); } + + this.game.addExecution(new WinCheckExecution()); + } + + private spawnNationsWithDelay() { if (this.game.config().spawnNPCs()) { - this.game.addExecution(...this.execManager.fakeHumanExecutions()); + for (const nation of this.game.nations()) { + const tile = this.game.ref(nation.spawnCell.x, nation.spawnCell.y); + const spawnExecution = new SpawnExecution(nation.playerInfo, tile); + this.game.addExecution(spawnExecution); + } + + const nationExecutions = this.execManager.fakeHumanExecutions(); + this.game.addExecution(...nationExecutions); } - this.game.addExecution(new WinCheckExecution()); } public addTurn(turn: Turn): void { @@ -167,11 +170,12 @@ export class GameRunner { return; } - if ( - (this.game.inSpawnPhase() || - this.game.config().gameConfig().gameType === GameType.Singleplayer) && - this.game.ticks() % 2 === 0 - ) { + if (!this.nationsSpawned && !this.game.inSpawnPhase()) { + this.spawnNationsWithDelay(); + this.nationsSpawned = true; + } + + if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) { this.game .players() .filter( @@ -183,6 +187,15 @@ export class GameRunner { ); } + if (this.nationsSpawned && this.game.ticks() % 2 === 0) { + this.game + .players() + .filter((p) => p.type() === PlayerType.FakeHuman) + .forEach( + (p) => (this.playerViewData[p.id()] = placeName(this.game, p)), + ); + } + if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) { this.game.players().forEach((p) => { this.playerViewData[p.id()] = placeName(this.game, p); diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 0870400135..91d12daa08 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -273,6 +273,13 @@ export class DefaultConfig implements Config { return this._serverConfig; } + turnIntervalMs(): number { + if (this._gameConfig.gameType === GameType.Singleplayer) { + return 10; + } + return this._serverConfig.turnIntervalMs(); + } + userSettings(): UserSettings { if (this._userSettings === null) { throw new Error("userSettings is null"); @@ -626,7 +633,7 @@ export class DefaultConfig implements Config { return 3; } numSpawnPhaseTurns(): number { - return this._gameConfig.gameType === GameType.Singleplayer ? 0 : 300; + return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300; } numBots(): number { return this.bots(); diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 69f1f4f296..b106a5426f 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -8,6 +8,7 @@ export class BotExecution implements Execution { private random: PseudoRandom; private mg: Game; private neighborsTerraNullius = true; + private initializationDelay = 50; private behavior: BotBehavior | null = null; private attackRate: number; @@ -34,6 +35,8 @@ export class BotExecution implements Execution { } tick(ticks: number) { + if (ticks < this.initializationDelay) return; + if (ticks % this.attackRate !== this.attackTick) return; if (!this.bot.isAlive()) { @@ -51,8 +54,6 @@ export class BotExecution implements Execution { this.expandRatio, ); - // Send an attack on the first tick - this.behavior.sendAttack(this.mg.terraNullius()); return; } @@ -81,7 +82,10 @@ export class BotExecution implements Execution { } } - if (this.neighborsTerraNullius) { + if ( + this.neighborsTerraNullius && + this.mg.ticks() > this.initializationDelay + 100 + ) { if (this.bot.sharesBorderWith(this.mg.terraNullius())) { this.behavior.sendAttack(this.mg.terraNullius()); return; diff --git a/src/core/execution/DelayedBotSpawnExecution.ts b/src/core/execution/DelayedBotSpawnExecution.ts deleted file mode 100644 index a524fd87d3..0000000000 --- a/src/core/execution/DelayedBotSpawnExecution.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Execution, Game, PlayerType } from "../game/Game"; -import { BotSpawner } from "./BotSpawner"; - -export class DelayedBotSpawnExecution implements Execution { - private active = true; - private mg: Game; - private botsSpawned = false; - private tickCount = 0; - private readonly MAX_WAIT_TICKS = 6000; - - constructor(private gameID: string) {} - - init(mg: Game, ticks: number) { - this.mg = mg; - } - - tick(ticks: number) { - this.tickCount++; - - if (this.botsSpawned) { - this.active = false; - return; - } - - if (this.tickCount >= this.MAX_WAIT_TICKS) { - console.warn( - "DelayedBotSpawnExecution: No human spawned after timeout, spawning bots anyway", - ); - const botSpawner = new BotSpawner(this.mg, this.gameID); - const botSpawns = botSpawner.spawnBots(this.mg.config().numBots()); - this.mg.addExecution(...botSpawns); - this.botsSpawned = true; - this.active = false; - return; - } - - const hasHumanSpawned = this.mg - .players() - .some( - (player) => player.type() === PlayerType.Human && player.hasSpawned(), - ); - - if (hasHumanSpawned) { - const botSpawner = new BotSpawner(this.mg, this.gameID); - const botSpawns = botSpawner.spawnBots(this.mg.config().numBots()); - - this.mg.addExecution(...botSpawns); - - this.botsSpawned = true; - this.active = false; - } - } - - isActive(): boolean { - return this.active; - } - - activeDuringSpawnPhase(): boolean { - return true; - } -} diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 10805d4b17..03bdf20db4 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -24,7 +24,6 @@ import { EmojiExecution } from "./EmojiExecution"; import { MirvExecution } from "./MIRVExecution"; import { structureSpawnTileValue } from "./nation/structureSpawnTileValue"; import { NukeExecution } from "./NukeExecution"; -import { SpawnExecution } from "./SpawnExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { calculateTerritoryCenter, closestTwoTiles } from "./Util"; import { BotBehavior, EMOJI_HECKLE } from "./utils/BotBehavior"; diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 26701a25fd..0282415f36 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -1,11 +1,4 @@ -import { - Execution, - Game, - GameType, - Player, - PlayerInfo, - PlayerType, -} from "../game/Game"; +import { Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { BotExecution } from "./BotExecution"; import { PlayerExecution } from "./PlayerExecution"; @@ -34,7 +27,7 @@ export class SpawnExecution implements Execution { if ( !this.mg.inSpawnPhase() && - this.mg.config().gameConfig().gameType !== GameType.Singleplayer + this.playerInfo.playerType !== PlayerType.FakeHuman ) { this.active = false; return; @@ -47,18 +40,21 @@ export class SpawnExecution implements Execution { player = this.mg.addPlayer(this.playerInfo); } - player.tiles().forEach((t) => player.relinquish(t)); - getSpawnTiles(this.mg, this.tile).forEach((t) => { - player.conquer(t); - }); + if (player) { + player.tiles().forEach((t) => player.relinquish(t)); + getSpawnTiles(this.mg, this.tile).forEach((t) => { + player.conquer(t); + }); - if (!player.hasSpawned()) { - this.mg.addExecution(new PlayerExecution(player)); - if (player.type() === PlayerType.Bot) { - this.mg.addExecution(new BotExecution(player)); + if (player && !player.hasSpawned()) { + this.mg.addExecution(new PlayerExecution(player)); + if (player.type() === PlayerType.Bot) { + this.mg.addExecution(new BotExecution(player)); + } + + player.setHasSpawned(true); } } - player.setHasSpawned(true); } isActive(): boolean { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 2c23ad8641..414292a517 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -14,6 +14,7 @@ import { Execution, Game, GameMode, + GameType, GameUpdates, HumansVsNations, MessageType, @@ -338,6 +339,14 @@ export class GameImpl implements Game { } inSpawnPhase(): boolean { + if (this.config().gameConfig().gameType === GameType.Singleplayer) { + const hasHumanSpawned = Array.from(this._players.values()).some( + (player) => player.type() === PlayerType.Human && player.hasSpawned(), + ); + if (hasHumanSpawned) { + return false; + } + } return this._ticks <= this.config().numSpawnPhaseTurns(); } diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index c525129d10..bb877c5459 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -9,6 +9,7 @@ import { WorkerClient } from "../worker/WorkerClient"; import { Cell, EmojiMessage, + GameType, GameUpdates, Gold, NameViewData, @@ -655,6 +656,14 @@ export class GameView implements GameMap { return this.lastUpdate.tick; } inSpawnPhase(): boolean { + if (this._config.gameConfig().gameType === GameType.Singleplayer) { + const hasHumanSpawned = Array.from(this._players.values()).some( + (player) => player.type() === PlayerType.Human && player.hasSpawned(), + ); + if (hasHumanSpawned) { + return false; + } + } return this.ticks() <= this._config.numSpawnPhaseTurns(); } config(): Config { diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 0cd4420da7..5794246df0 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -88,14 +88,16 @@ export class GameManager { if (!game.hasStarted()) { // Prestart tells clients to start loading the game. game.prestart(); - // Start game on delay to allow time for clients to connect. + // Start game immediately for single player games, with delay for multiplayer games + const startDelay = + game.gameConfig.gameType === GameType.Singleplayer ? 0 : 2000; setTimeout(() => { try { game.start(); } catch (error) { this.log.error(`error starting game ${id}: ${error}`); } - }, 2000); + }, startDelay); } } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 04c87d5d75..8f3c3abe1d 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -430,10 +430,11 @@ export class GameServer { } this.gameStartInfo = result.data satisfies GameStartInfo; - this.endTurnIntervalID = setInterval( - () => this.endTurn(), - this.config.turnIntervalMs(), - ); + const turnInterval = + this.gameConfig.gameType === GameType.Singleplayer + ? 10 + : this.config.turnIntervalMs(); + this.endTurnIntervalID = setInterval(() => this.endTurn(), turnInterval); this.activeClients.forEach((c) => { this.log.info("sending start message", { clientID: c.clientID, From 8aca6f3c3a96426a7aa3ade814281eaed2d4b4bd Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:38:09 +0100 Subject: [PATCH 06/21] Merge branch 'main' of https://github.com/openfrontio/OpenFrontIO into feat-sp-timer fix merge errors --- src/client/Main.ts | 1 + src/client/graphics/layers/SpawnAd.ts | 138 ----------------------- src/core/execution/FakeHumanExecution.ts | 1 + src/server/GameServer.ts | 5 +- 4 files changed, 3 insertions(+), 142 deletions(-) delete mode 100644 src/client/graphics/layers/SpawnAd.ts diff --git a/src/client/Main.ts b/src/client/Main.ts index ade4758385..83b5d844e7 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -12,6 +12,7 @@ import { DarkModeButton } from "./DarkModeButton"; import "./FlagInput"; import { FlagInput } from "./FlagInput"; import { FlagInputModal } from "./FlagInputModal"; +import { GameType } from "../core/game/Game"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; import { GutterAds } from "./GutterAds"; diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts deleted file mode 100644 index b2868d470c..0000000000 --- a/src/client/graphics/layers/SpawnAd.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { LitElement, css, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { translateText } from "../../../client/Utils"; -import { GameType } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; -import { getGamesPlayed } from "../../Utils"; -import { Layer } from "./Layer"; - -const AD_TYPE = "bottom_rail"; -const AD_CONTAINER_ID = "bottom-rail-ad-container"; - -@customElement("spawn-ad") -export class SpawnAd extends LitElement implements Layer { - public g: GameView; - - @state() - private isVisible: boolean = false; - - @state() - private adLoaded: boolean = false; - - private gamesPlayed: number = 0; - - // Override createRenderRoot to disable shadow DOM - createRenderRoot() { - return this; - } - - static styles = css``; - - constructor() { - super(); - } - - init() { - this.gamesPlayed = getGamesPlayed(); - } - - public show(): void { - this.isVisible = true; - this.loadAd(); - this.requestUpdate(); - } - - public hide(): void { - // Destroy the ad when hiding - this.destroyAd(); - this.isVisible = false; - this.adLoaded = false; - this.requestUpdate(); - } - - public async tick() { - if (this.g.config().gameConfig().gameType === GameType.Singleplayer) { - return; - } - - if ( - !this.isVisible && - this.g.inSpawnPhase() && - this.g.ticks() > 10 && - this.gamesPlayed > 5 - ) { - this.show(); - } - if (this.isVisible && !this.g.inSpawnPhase()) { - this.hide(); - } - } - - private loadAd(): void { - if (!window.ramp) { - console.warn("Playwire RAMP not available"); - return; - } - if (this.adLoaded) { - console.log("Ad already loaded, skipping"); - return; - } - try { - window.ramp.que.push(() => { - window.ramp.spaAddAds([ - { - type: AD_TYPE, - selectorId: AD_CONTAINER_ID, - }, - ]); - this.adLoaded = true; - console.log("Playwire ad loaded:", AD_TYPE); - }); - } catch (error) { - console.error("Failed to load Playwire ad:", error); - } - } - - private destroyAd(): void { - if (!window.ramp || !this.adLoaded) { - return; - } - try { - window.ramp.que.push(() => { - window.ramp.destroyUnits("all"); - console.log("Playwire spawn ad destroyed"); - }); - } catch (error) { - console.error("Failed to destroy Playwire ad:", error); - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - // Clean up ad when component is removed - this.destroyAd(); - } - - render() { - if (!this.isVisible) { - return html``; - } - - return html` -
-
- ${!this.adLoaded - ? html`${translateText("spawn_ad.loading")}` - : ""} -
-
- `; - } -} diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 03bdf20db4..10805d4b17 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -24,6 +24,7 @@ import { EmojiExecution } from "./EmojiExecution"; import { MirvExecution } from "./MIRVExecution"; import { structureSpawnTileValue } from "./nation/structureSpawnTileValue"; import { NukeExecution } from "./NukeExecution"; +import { SpawnExecution } from "./SpawnExecution"; import { TransportShipExecution } from "./TransportShipExecution"; import { calculateTerritoryCenter, closestTwoTiles } from "./Util"; import { BotBehavior, EMOJI_HECKLE } from "./utils/BotBehavior"; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 8f3c3abe1d..64e6b0f2bb 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -430,10 +430,7 @@ export class GameServer { } this.gameStartInfo = result.data satisfies GameStartInfo; - const turnInterval = - this.gameConfig.gameType === GameType.Singleplayer - ? 10 - : this.config.turnIntervalMs(); + const turnInterval = this.config.turnIntervalMs(); this.endTurnIntervalID = setInterval(() => this.endTurn(), turnInterval); this.activeClients.forEach((c) => { this.log.info("sending start message", { From a5920f1b2dbfff708fcd6fb4852bc586e87f53e9 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:44:24 +0100 Subject: [PATCH 07/21] turnInterval unification feedback --- src/core/configuration/Config.ts | 4 +++- src/core/configuration/DefaultConfig.ts | 12 ++++++------ src/server/GameManager.ts | 3 +-- src/server/GameServer.ts | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index abe0f0018c..01c53a0fa8 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -5,6 +5,7 @@ import { Game, GameMapType, GameMode, + GameType, Gold, Player, PlayerInfo, @@ -27,7 +28,7 @@ export enum GameEnv { } export interface ServerConfig { - turnIntervalMs(): number; + turnIntervalMs(gameType?: GameType): number; gameCreationRate(): number; lobbyMaxPlayers( map: GameMapType, @@ -72,6 +73,7 @@ export interface NukeMagnitude { } export interface Config { + turnIntervalMs(gameType?: GameType): number; samHittingChance(): number; samWarheadHittingChance(): number; spawnImmunityDuration(): Tick; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 91d12daa08..4ecdf47ac5 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -172,7 +172,10 @@ export abstract class DefaultServerConfig implements ServerConfig { } abstract numWorkers(): number; abstract env(): GameEnv; - turnIntervalMs(): number { + turnIntervalMs(gameType?: GameType): number { + if (gameType && gameType === GameType.Singleplayer) { + return 10; + } return 100; } gameCreationRate(): number { @@ -273,11 +276,8 @@ export class DefaultConfig implements Config { return this._serverConfig; } - turnIntervalMs(): number { - if (this._gameConfig.gameType === GameType.Singleplayer) { - return 10; - } - return this._serverConfig.turnIntervalMs(); + turnIntervalMs(gameType?: GameType): number { + return this._serverConfig.turnIntervalMs(gameType); } userSettings(): UserSettings { diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 5794246df0..d9b8dd6c1d 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -89,8 +89,7 @@ export class GameManager { // Prestart tells clients to start loading the game. game.prestart(); // Start game immediately for single player games, with delay for multiplayer games - const startDelay = - game.gameConfig.gameType === GameType.Singleplayer ? 0 : 2000; + const startDelay = this.config.turnIntervalMs(game.gameConfig.gameType); setTimeout(() => { try { game.start(); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 64e6b0f2bb..b5d22c79df 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -430,7 +430,7 @@ export class GameServer { } this.gameStartInfo = result.data satisfies GameStartInfo; - const turnInterval = this.config.turnIntervalMs(); + const turnInterval = this.config.turnIntervalMs(this.gameConfig.gameType); this.endTurnIntervalID = setInterval(() => this.endTurn(), turnInterval); this.activeClients.forEach((c) => { this.log.info("sending start message", { From 1ae7884cc7bb8d15a7dba969756fa8dfa97087eb Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:51:15 +0100 Subject: [PATCH 08/21] startDelay encapsulation feedback --- src/client/Main.ts | 2 +- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 6 +++++- src/server/GameManager.ts | 2 +- tests/util/TestServerConfig.ts | 5 ++++- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index 83b5d844e7..3700a34fe0 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -3,6 +3,7 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { EventBus } from "../core/EventBus"; import { GameRecord, GameStartInfo, ID } from "../core/Schemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; +import { GameType } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import "./AccountModal"; import { joinLobby } from "./ClientGameRunner"; @@ -12,7 +13,6 @@ import { DarkModeButton } from "./DarkModeButton"; import "./FlagInput"; import { FlagInput } from "./FlagInput"; import { FlagInputModal } from "./FlagInputModal"; -import { GameType } from "../core/game/Game"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; import { GutterAds } from "./GutterAds"; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 01c53a0fa8..255ff631ba 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -29,6 +29,7 @@ export enum GameEnv { export interface ServerConfig { turnIntervalMs(gameType?: GameType): number; + startDelay(gameType: GameType): number; gameCreationRate(): number; lobbyMaxPlayers( map: GameMapType, diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 4ecdf47ac5..02aa1dca8a 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -178,6 +178,10 @@ export abstract class DefaultServerConfig implements ServerConfig { } return 100; } + // Start game immediately for single player games, with delay for multiplayer games + startDelay(gameType: GameType): number { + return gameType === GameType.Singleplayer ? 0 : 2000; + } gameCreationRate(): number { return 60 * 1000; } @@ -277,7 +281,7 @@ export class DefaultConfig implements Config { } turnIntervalMs(gameType?: GameType): number { - return this._serverConfig.turnIntervalMs(gameType); + return this._serverConfig.turnIntervalMs(gameType); } userSettings(): UserSettings { diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index d9b8dd6c1d..2096fa3fa5 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -89,7 +89,7 @@ export class GameManager { // Prestart tells clients to start loading the game. game.prestart(); // Start game immediately for single player games, with delay for multiplayer games - const startDelay = this.config.turnIntervalMs(game.gameConfig.gameType); + const startDelay = this.config.startDelay(game.gameConfig.gameType); setTimeout(() => { try { game.start(); diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index 6f20fa1cd6..d1a5f58748 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -1,6 +1,6 @@ import { JWK } from "jose"; import { GameEnv, ServerConfig } from "../../src/core/configuration/Config"; -import { GameMapType } from "../../src/core/game/Game"; +import { GameMapType, GameType } from "../../src/core/game/Game"; import { GameID } from "../../src/core/Schemas"; export class TestServerConfig implements ServerConfig { @@ -64,6 +64,9 @@ export class TestServerConfig implements ServerConfig { numWorkers(): number { throw new Error("Method not implemented."); } + startDelay(gameType: GameType): number { + throw new Error("Method not implemented."); + } workerIndex(gameID: GameID): number { throw new Error("Method not implemented."); } From 0b12d89a45b9bc82f7b816b1d60db751564a645a Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:51:58 +0100 Subject: [PATCH 09/21] Remove out of scope changes --- src/core/GameRunner.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 3b6c8dca40..9302b0c9a5 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -187,15 +187,6 @@ export class GameRunner { ); } - if (this.nationsSpawned && this.game.ticks() % 2 === 0) { - this.game - .players() - .filter((p) => p.type() === PlayerType.FakeHuman) - .forEach( - (p) => (this.playerViewData[p.id()] = placeName(this.game, p)), - ); - } - if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) { this.game.players().forEach((p) => { this.playerViewData[p.id()] = placeName(this.game, p); From 75a4f17b4f85438322fbbca7a6784f29a48b6210 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:04:04 +0100 Subject: [PATCH 10/21] Spawn nations on the first tick for all games --- src/core/GameRunner.ts | 23 +++-------------------- src/core/execution/FakeHumanExecution.ts | 3 ++- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 9302b0c9a5..9c49222b12 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -1,7 +1,6 @@ import { placeName } from "../client/graphics/NameBoxCalculator"; import { getConfig } from "./configuration/ConfigLoader"; import { Executor } from "./execution/ExecutionManager"; -import { SpawnExecution } from "./execution/SpawnExecution"; import { WinCheckExecution } from "./execution/WinCheckExecution"; import { AllPlayers, @@ -95,7 +94,6 @@ export class GameRunner { private turns: Turn[] = []; private currTurn = 0; private isExecuting = false; - private nationsSpawned = false; private playerViewData: Record = {}; @@ -114,21 +112,11 @@ export class GameRunner { ...this.execManager.spawnBots(this.game.config().numBots()), ); } - - this.game.addExecution(new WinCheckExecution()); - } - - private spawnNationsWithDelay() { if (this.game.config().spawnNPCs()) { - for (const nation of this.game.nations()) { - const tile = this.game.ref(nation.spawnCell.x, nation.spawnCell.y); - const spawnExecution = new SpawnExecution(nation.playerInfo, tile); - this.game.addExecution(spawnExecution); - } - - const nationExecutions = this.execManager.fakeHumanExecutions(); - this.game.addExecution(...nationExecutions); + this.game.addExecution(...this.execManager.fakeHumanExecutions()); } + + this.game.addExecution(new WinCheckExecution()); } public addTurn(turn: Turn): void { @@ -170,11 +158,6 @@ export class GameRunner { return; } - if (!this.nationsSpawned && !this.game.inSpawnPhase()) { - this.spawnNationsWithDelay(); - this.nationsSpawned = true; - } - if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) { this.game .players() diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 10805d4b17..67787f7b7b 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -149,7 +149,8 @@ export class FakeHumanExecution implements Execution { this.trackTradeShipsAndRetaliate(); } - if (ticks % this.attackRate !== this.attackTick) { + // Tries to spawn nations on the first tick of the match + if (ticks > 1 && ticks % this.attackRate !== this.attackTick) { return; } From 16460955ff32ec629e3dfa543ee4d119952986fe Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:16:41 +0100 Subject: [PATCH 11/21] Add back spawntimer to singleplayer --- src/client/graphics/layers/SpawnTimer.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts index 13fbcf741b..393cf96d4d 100644 --- a/src/client/graphics/layers/SpawnTimer.ts +++ b/src/client/graphics/layers/SpawnTimer.ts @@ -32,11 +32,6 @@ export class SpawnTimer extends LitElement implements Layer { tick() { if (this.game.inSpawnPhase()) { - if (this.game.config().gameConfig().gameType === GameType.Singleplayer) { - this.ratios = []; - this.colors = []; - return; - } // During spawn phase, only one segment filling full width this.ratios = [ this.game.ticks() / this.game.config().numSpawnPhaseTurns(), From 1c264f855dca0dbe99d2570a3576ff03869c43c1 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:49:08 +0100 Subject: [PATCH 12/21] rever out of scope changed to bot execution --- src/core/execution/BotExecution.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index b106a5426f..f559535d3a 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -8,7 +8,7 @@ export class BotExecution implements Execution { private random: PseudoRandom; private mg: Game; private neighborsTerraNullius = true; - private initializationDelay = 50; + private initializationDelay = 0; private behavior: BotBehavior | null = null; private attackRate: number; @@ -54,6 +54,9 @@ export class BotExecution implements Execution { this.expandRatio, ); + // Send an attack on the first tick + this.behavior.sendAttack(this.mg.terraNullius()); + return; } @@ -84,7 +87,7 @@ export class BotExecution implements Execution { if ( this.neighborsTerraNullius && - this.mg.ticks() > this.initializationDelay + 100 + this.mg.ticks() > this.initializationDelay ) { if (this.bot.sharesBorderWith(this.mg.terraNullius())) { this.behavior.sendAttack(this.mg.terraNullius()); From 6a592c473162ecd03020a63d2d1bacb5b18194fa Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:57:37 +0100 Subject: [PATCH 13/21] Other out of scope changes: bot behaviour, preloading games --- src/client/Main.ts | 1 - src/core/execution/SpawnExecution.ts | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index 3700a34fe0..ade4758385 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -3,7 +3,6 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { EventBus } from "../core/EventBus"; import { GameRecord, GameStartInfo, ID } from "../core/Schemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; -import { GameType } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import "./AccountModal"; import { joinLobby } from "./ClientGameRunner"; diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 0282415f36..03c699bc38 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -25,10 +25,7 @@ export class SpawnExecution implements Execution { return; } - if ( - !this.mg.inSpawnPhase() && - this.playerInfo.playerType !== PlayerType.FakeHuman - ) { + if (!this.mg.inSpawnPhase()) { this.active = false; return; } From dc73692cd9e72b1ad2d79e2937f311ada94944af Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:17:21 +0100 Subject: [PATCH 14/21] add grace period to config (1.5s = 15 turns) for player to change placement --- src/client/ClientGameRunner.ts | 2 +- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 4 ++++ src/core/game/GameImpl.ts | 24 ++++++++++++++++++------ src/core/game/GameView.ts | 24 ++++++++++++++++++------ 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 3a9b436d25..c3c093426d 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -12,7 +12,7 @@ import { import { createPartialGameRecord, replacer } from "../core/Util"; import { ServerConfig } from "../core/configuration/Config"; import { getConfig } from "../core/configuration/ConfigLoader"; -import { GameType, PlayerActions, UnitType } from "../core/game/Game"; +import { PlayerActions, UnitType } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { GameMapLoader } from "../core/game/GameMapLoader"; import { diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 255ff631ba..64905e2f76 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -93,6 +93,7 @@ export interface Config { instantBuild(): boolean; isRandomSpawn(): boolean; numSpawnPhaseTurns(): number; + numSingleplayerGracePeriodTurns(): number; userSettings(): UserSettings; playerTeams(): TeamCountConfig; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 02aa1dca8a..6fbf6f9ed0 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -639,6 +639,10 @@ export class DefaultConfig implements Config { numSpawnPhaseTurns(): number { return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300; } + // Amount of ticks for player to change spawn placement in singleplayer + numSingleplayerGracePeriodTurns(): number { + return 15; + } numBots(): number { return this.bots(); } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 414292a517..3a4765bc88 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -70,6 +70,8 @@ export class GameImpl implements Game { private _height: number; _terraNullius: TerraNulliusImpl; + private firstHumanSpawnTurn: number; + allianceRequests: AllianceRequestImpl[] = []; alliances_: AllianceImpl[] = []; @@ -339,15 +341,25 @@ export class GameImpl implements Game { } inSpawnPhase(): boolean { + if (this._ticks > this.config().numSpawnPhaseTurns()) { + return false; + } if (this.config().gameConfig().gameType === GameType.Singleplayer) { - const hasHumanSpawned = Array.from(this._players.values()).some( - (player) => player.type() === PlayerType.Human && player.hasSpawned(), - ); - if (hasHumanSpawned) { - return false; + if (!this.firstHumanSpawnTurn) { + this.firstHumanSpawnTurn = Array.from(this._players.values()).some( + (player) => player.type() === PlayerType.Human && player.hasSpawned(), + ) + ? this._ticks + : 0; + } else { + return ( + this._ticks <= + this.firstHumanSpawnTurn + + this.config().numSingleplayerGracePeriodTurns() + ); } } - return this._ticks <= this.config().numSpawnPhaseTurns(); + return true; } ticks(): number { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index bb877c5459..08c94293f2 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -476,6 +476,8 @@ export class GameView implements GameMap { private _map: GameMap; + private firstHumanSpawnTurn: number; + constructor( public worker: WorkerClient, private _config: Config, @@ -656,15 +658,25 @@ export class GameView implements GameMap { return this.lastUpdate.tick; } inSpawnPhase(): boolean { + if (this.ticks() > this._config.numSpawnPhaseTurns()) { + return false; + } if (this._config.gameConfig().gameType === GameType.Singleplayer) { - const hasHumanSpawned = Array.from(this._players.values()).some( - (player) => player.type() === PlayerType.Human && player.hasSpawned(), - ); - if (hasHumanSpawned) { - return false; + if (!this.firstHumanSpawnTurn) { + this.firstHumanSpawnTurn = Array.from(this._players.values()).some( + (player) => player.type() === PlayerType.Human && player.hasSpawned(), + ) + ? this.ticks() + : 0; + } else { + return ( + this.ticks() <= + this.firstHumanSpawnTurn + + this._config.numSingleplayerGracePeriodTurns() + ); } } - return this.ticks() <= this._config.numSpawnPhaseTurns(); + return true; } config(): Config { return this._config; From f4aaca22c86edfcab3ee8094f7effd637dc9e0ea Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:21:20 +0100 Subject: [PATCH 15/21] Remove redundant delay --- src/core/execution/BotExecution.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index f559535d3a..953e09ff97 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -8,7 +8,6 @@ export class BotExecution implements Execution { private random: PseudoRandom; private mg: Game; private neighborsTerraNullius = true; - private initializationDelay = 0; private behavior: BotBehavior | null = null; private attackRate: number; @@ -35,8 +34,6 @@ export class BotExecution implements Execution { } tick(ticks: number) { - if (ticks < this.initializationDelay) return; - if (ticks % this.attackRate !== this.attackTick) return; if (!this.bot.isAlive()) { @@ -85,10 +82,7 @@ export class BotExecution implements Execution { } } - if ( - this.neighborsTerraNullius && - this.mg.ticks() > this.initializationDelay - ) { + if (this.neighborsTerraNullius) { if (this.bot.sharesBorderWith(this.mg.terraNullius())) { this.behavior.sendAttack(this.mg.terraNullius()); return; From 023eac925556a5b46b420629db4228cdbd9520a0 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:22:01 +0100 Subject: [PATCH 16/21] Style --- src/core/GameRunner.ts | 1 - src/core/execution/BotExecution.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 9c49222b12..6a8a4042f3 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -115,7 +115,6 @@ export class GameRunner { if (this.game.config().spawnNPCs()) { this.game.addExecution(...this.execManager.fakeHumanExecutions()); } - this.game.addExecution(new WinCheckExecution()); } diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 953e09ff97..69f1f4f296 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -53,7 +53,6 @@ export class BotExecution implements Execution { // Send an attack on the first tick this.behavior.sendAttack(this.mg.terraNullius()); - return; } From 4d3359d5504c1bb66a90db4c1d1c817cb87a9fba Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:47:34 +0100 Subject: [PATCH 17/21] Remove variable tickrate --- src/core/configuration/Config.ts | 4 ++-- src/core/configuration/DefaultConfig.ts | 9 +++------ src/server/GameServer.ts | 6 ++++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 64905e2f76..9ed86f77ab 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -28,7 +28,7 @@ export enum GameEnv { } export interface ServerConfig { - turnIntervalMs(gameType?: GameType): number; + turnIntervalMs(): number; startDelay(gameType: GameType): number; gameCreationRate(): number; lobbyMaxPlayers( @@ -74,7 +74,7 @@ export interface NukeMagnitude { } export interface Config { - turnIntervalMs(gameType?: GameType): number; + turnIntervalMs(): number; samHittingChance(): number; samWarheadHittingChance(): number; spawnImmunityDuration(): Tick; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 6fbf6f9ed0..414de6d0a1 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -172,10 +172,7 @@ export abstract class DefaultServerConfig implements ServerConfig { } abstract numWorkers(): number; abstract env(): GameEnv; - turnIntervalMs(gameType?: GameType): number { - if (gameType && gameType === GameType.Singleplayer) { - return 10; - } + turnIntervalMs(): number { return 100; } // Start game immediately for single player games, with delay for multiplayer games @@ -280,8 +277,8 @@ export class DefaultConfig implements Config { return this._serverConfig; } - turnIntervalMs(gameType?: GameType): number { - return this._serverConfig.turnIntervalMs(gameType); + turnIntervalMs(): number { + return this._serverConfig.turnIntervalMs(); } userSettings(): UserSettings { diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index b5d22c79df..04c87d5d75 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -430,8 +430,10 @@ export class GameServer { } this.gameStartInfo = result.data satisfies GameStartInfo; - const turnInterval = this.config.turnIntervalMs(this.gameConfig.gameType); - this.endTurnIntervalID = setInterval(() => this.endTurn(), turnInterval); + this.endTurnIntervalID = setInterval( + () => this.endTurn(), + this.config.turnIntervalMs(), + ); this.activeClients.forEach((c) => { this.log.info("sending start message", { clientID: c.clientID, From 6b9dd7feedff53b0510c819be47ca6864b3aa4be Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:51:47 +0100 Subject: [PATCH 18/21] Better variable name --- src/core/game/GameImpl.ts | 8 ++++---- src/core/game/GameView.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 3a4765bc88..06999c88a7 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -70,7 +70,7 @@ export class GameImpl implements Game { private _height: number; _terraNullius: TerraNulliusImpl; - private firstHumanSpawnTurn: number; + private firstHumanSpawnTick: number; allianceRequests: AllianceRequestImpl[] = []; alliances_: AllianceImpl[] = []; @@ -345,8 +345,8 @@ export class GameImpl implements Game { return false; } if (this.config().gameConfig().gameType === GameType.Singleplayer) { - if (!this.firstHumanSpawnTurn) { - this.firstHumanSpawnTurn = Array.from(this._players.values()).some( + if (!this.firstHumanSpawnTick) { + this.firstHumanSpawnTick = Array.from(this._players.values()).some( (player) => player.type() === PlayerType.Human && player.hasSpawned(), ) ? this._ticks @@ -354,7 +354,7 @@ export class GameImpl implements Game { } else { return ( this._ticks <= - this.firstHumanSpawnTurn + + this.firstHumanSpawnTick + this.config().numSingleplayerGracePeriodTurns() ); } diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 08c94293f2..27186948e3 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -476,7 +476,7 @@ export class GameView implements GameMap { private _map: GameMap; - private firstHumanSpawnTurn: number; + private firstHumanSpawnTick: number; constructor( public worker: WorkerClient, @@ -662,8 +662,8 @@ export class GameView implements GameMap { return false; } if (this._config.gameConfig().gameType === GameType.Singleplayer) { - if (!this.firstHumanSpawnTurn) { - this.firstHumanSpawnTurn = Array.from(this._players.values()).some( + if (!this.firstHumanSpawnTick) { + this.firstHumanSpawnTick = Array.from(this._players.values()).some( (player) => player.type() === PlayerType.Human && player.hasSpawned(), ) ? this.ticks() @@ -671,7 +671,7 @@ export class GameView implements GameMap { } else { return ( this.ticks() <= - this.firstHumanSpawnTurn + + this.firstHumanSpawnTick + this._config.numSingleplayerGracePeriodTurns() ); } From a0a7fa74f4a82edb64c633f66a195757e5d1b6e9 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:42:23 +0100 Subject: [PATCH 19/21] encapsulate isSpawnPhase calculation into default config for reusability --- src/core/configuration/Config.ts | 7 +++++- src/core/configuration/DefaultConfig.ts | 20 ++++++++++++++++- src/core/game/GameImpl.ts | 28 +++++++++--------------- src/core/game/GameView.ts | 29 +++++++++---------------- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 9ed86f77ab..fd329e6587 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -93,7 +93,12 @@ export interface Config { instantBuild(): boolean; isRandomSpawn(): boolean; numSpawnPhaseTurns(): number; - numSingleplayerGracePeriodTurns(): number; + numGracePeriodTurns(): number; + isSpawnPhase( + ticks: number, + gameType: GameType, + firstHumanSpawnTick?: number, + ): boolean; userSettings(): UserSettings; playerTeams(): TeamCountConfig; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 414de6d0a1..c6c830f30f 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -633,13 +633,31 @@ export class DefaultConfig implements Config { boatMaxNumber(): number { return 3; } + numSpawnPhaseTurns(): number { return this._gameConfig.gameType === GameType.Singleplayer ? 100 : 300; } // Amount of ticks for player to change spawn placement in singleplayer - numSingleplayerGracePeriodTurns(): number { + numGracePeriodTurns(): number { return 15; } + isSpawnPhase( + ticks: number, + gameType: GameType, + firstHumanSpawnTick?: number, + ): boolean { + if (ticks > this.numSpawnPhaseTurns()) { + return false; + } + if (gameType !== GameType.Singleplayer) { + return true; + } + if (!firstHumanSpawnTick) { + return true; + } + return ticks <= firstHumanSpawnTick + this.numGracePeriodTurns(); + } + numBots(): number { return this.bots(); } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 06999c88a7..9605da8058 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -14,7 +14,6 @@ import { Execution, Game, GameMode, - GameType, GameUpdates, HumansVsNations, MessageType, @@ -341,24 +340,17 @@ export class GameImpl implements Game { } inSpawnPhase(): boolean { - if (this._ticks > this.config().numSpawnPhaseTurns()) { - return false; - } - if (this.config().gameConfig().gameType === GameType.Singleplayer) { - if (!this.firstHumanSpawnTick) { - this.firstHumanSpawnTick = Array.from(this._players.values()).some( - (player) => player.type() === PlayerType.Human && player.hasSpawned(), - ) - ? this._ticks - : 0; - } else { - return ( - this._ticks <= - this.firstHumanSpawnTick + - this.config().numSingleplayerGracePeriodTurns() - ); - } + if (!this.firstHumanSpawnTick) { + const humanSpawned = Array.from(this._players.values()).some( + (p) => p.type() === PlayerType.Human && p.hasSpawned(), + ); + if (humanSpawned) this.firstHumanSpawnTick = this._ticks; } + return this._config.isSpawnPhase( + this.ticks(), + this.config().gameConfig().gameType, + this.firstHumanSpawnTick, + ); return true; } diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 27186948e3..d3f77d8674 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -9,7 +9,6 @@ import { WorkerClient } from "../worker/WorkerClient"; import { Cell, EmojiMessage, - GameType, GameUpdates, Gold, NameViewData, @@ -658,25 +657,17 @@ export class GameView implements GameMap { return this.lastUpdate.tick; } inSpawnPhase(): boolean { - if (this.ticks() > this._config.numSpawnPhaseTurns()) { - return false; - } - if (this._config.gameConfig().gameType === GameType.Singleplayer) { - if (!this.firstHumanSpawnTick) { - this.firstHumanSpawnTick = Array.from(this._players.values()).some( - (player) => player.type() === PlayerType.Human && player.hasSpawned(), - ) - ? this.ticks() - : 0; - } else { - return ( - this.ticks() <= - this.firstHumanSpawnTick + - this._config.numSingleplayerGracePeriodTurns() - ); - } + if (!this.firstHumanSpawnTick) { + const humanSpawned = this.playerViews().some( + (p) => p.type() === PlayerType.Human && p.hasSpawned(), + ); + if (humanSpawned) this.firstHumanSpawnTick = this.ticks(); } - return true; + return this.config().isSpawnPhase( + this.ticks(), + this.config().gameConfig().gameType, + this.firstHumanSpawnTick, + ); } config(): Config { return this._config; From d6d35b9826379af0f50177fe3ad8102063db7902 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:04:08 +0100 Subject: [PATCH 20/21] redundant return statement --- src/core/game/GameImpl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 9605da8058..f3dc53e142 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -351,7 +351,6 @@ export class GameImpl implements Game { this.config().gameConfig().gameType, this.firstHumanSpawnTick, ); - return true; } ticks(): number { From 2246a13479379269c76665b895c4a24d99837e55 Mon Sep 17 00:00:00 2001 From: Lavodan <21205085+Lavodan@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:24:40 +0100 Subject: [PATCH 21/21] Change default gameType from singleplayer to fix failing tests --- tests/util/Setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 963b134c13..1622df5412 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -59,7 +59,7 @@ export async function setup( gameMap: GameMapType.Asia, gameMapSize: GameMapSize.Normal, gameMode: GameMode.FFA, - gameType: GameType.Singleplayer, + gameType: GameType.Public, difficulty: Difficulty.Medium, disableNPCs: false, donateGold: false,