From 713ec85ca93e135ae65fa59202cff81a363c16c9 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Sat, 14 Feb 2026 06:27:47 +0100 Subject: [PATCH] Exclude Transport Ship in many cases --- src/client/ClientGameRunner.ts | 48 ++++---- src/client/graphics/layers/MainRadialMenu.ts | 7 +- src/core/GameRunner.ts | 4 +- src/core/game/Game.ts | 11 +- src/core/game/GameView.ts | 7 +- src/core/game/PlayerImpl.ts | 16 ++- src/core/worker/Worker.worker.ts | 1 + src/core/worker/WorkerClient.ts | 5 +- src/core/worker/WorkerMessages.ts | 2 + tests/perf/buildableunitsperf.ts | 113 +++++++++++++++++++ 10 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 tests/perf/buildableunitsperf.ts diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index ca7dfef7f..a17cf8d54 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -13,7 +13,11 @@ 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 { + BuildableUnitsTransportShip, + PlayerActions, + UnitType, +} from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { GameMapLoader } from "../core/game/GameMapLoader"; import { @@ -557,19 +561,20 @@ export class ClientGameRunner { if (myPlayer === null) return; this.myPlayer = myPlayer; } - this.myPlayer.actions(tile).then((actions) => { - if (this.myPlayer === null) return; - if (actions.canAttack) { - this.eventBus.emit( - new SendAttackIntentEvent( - this.gameView.owner(tile).id(), - this.myPlayer.troops() * this.renderer.uiState.attackRatio, - ), - ); - } else if (this.canAutoBoat(actions, tile)) { - this.sendBoatAttackIntent(tile); - } - }); + this.myPlayer + .actions(tile, BuildableUnitsTransportShip.Include) + .then((actions) => { + if (actions.canAttack) { + this.eventBus.emit( + new SendAttackIntentEvent( + this.gameView.owner(tile).id(), + this.myPlayer!.troops() * this.renderer.uiState.attackRatio, + ), + ); + } else if (this.canAutoBoat(actions, tile)) { + this.sendBoatAttackIntent(tile); + } + }); } private autoUpgradeEvent(event: AutoUpgradeEvent) { @@ -654,11 +659,13 @@ export class ClientGameRunner { this.myPlayer = myPlayer; } - this.myPlayer.actions(tile).then((actions) => { - if (this.canBoatAttack(actions) !== false) { - this.sendBoatAttackIntent(tile); - } - }); + this.myPlayer + .actions(tile, BuildableUnitsTransportShip.Only) + .then((actions) => { + if (this.canBoatAttack(actions) !== false) { + this.sendBoatAttackIntent(tile); + } + }); } private doGroundAttackUnderCursor(): void { @@ -674,12 +681,11 @@ export class ClientGameRunner { } this.myPlayer.actions(tile).then((actions) => { - if (this.myPlayer === null) return; if (actions.canAttack) { this.eventBus.emit( new SendAttackIntentEvent( this.gameView.owner(tile).id(), - this.myPlayer.troops() * this.renderer.uiState.attackRatio, + this.myPlayer!.troops() * this.renderer.uiState.attackRatio, ), ); } diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts index 989b5aa79..42adc6cf0 100644 --- a/src/client/graphics/layers/MainRadialMenu.ts +++ b/src/client/graphics/layers/MainRadialMenu.ts @@ -1,7 +1,10 @@ import { LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; -import { PlayerActions } from "../../../core/game/Game"; +import { + BuildableUnitsTransportShip, + PlayerActions, +} from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; @@ -162,7 +165,7 @@ export class MainRadialMenu extends LitElement implements Layer { if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return; this.game .myPlayer()! - .actions(this.clickedTile) + .actions(this.clickedTile, BuildableUnitsTransportShip.Include) .then((actions) => { this.updatePlayerActions( this.game.myPlayer()!, diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 856e19691..8e277d4c3 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -6,6 +6,7 @@ import { WinCheckExecution } from "./execution/WinCheckExecution"; import { AllPlayers, Attack, + BuildableUnitsTransportShip, Cell, Game, GameUpdates, @@ -195,13 +196,14 @@ export class GameRunner { playerID: PlayerID, x?: number, y?: number, + transportShip: BuildableUnitsTransportShip = BuildableUnitsTransportShip.Exclude, ): PlayerActions { const player = this.game.player(playerID); const tile = x !== undefined && y !== undefined ? this.game.ref(x, y) : null; const actions = { canAttack: tile !== null && player.canAttack(tile), - buildableUnits: player.buildableUnits(tile), + buildableUnits: player.buildableUnits(tile, transportShip), canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers), canEmbargoAll: player.canEmbargoAll(), } as PlayerActions; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 11bee0562..1a3ed75a0 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -641,7 +641,10 @@ export interface Player { unitCount(type: UnitType): number; unitsConstructed(type: UnitType): number; unitsOwned(type: UnitType): number; - buildableUnits(tile: TileRef | null): BuildableUnit[]; + buildableUnits( + tile: TileRef | null, + transportShip?: BuildableUnitsTransportShip, + ): BuildableUnit[]; canBuild(type: UnitType, targetTile: TileRef): TileRef | false; buildUnit( type: T, @@ -858,6 +861,12 @@ export interface PlayerActions { interaction?: PlayerInteraction; } +export enum BuildableUnitsTransportShip { + Exclude = "e", // default when undefined to save data between threads + Include = "i", + Only = "o", +} + export interface BuildableUnit { canBuild: TileRef | false; // unit id of the existing unit that can be upgraded, or false if it cannot be upgraded. diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 69f0608eb..c05c9e047 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -7,6 +7,7 @@ import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas"; import { createRandomName } from "../Util"; import { WorkerClient } from "../worker/WorkerClient"; import { + BuildableUnitsTransportShip, Cell, EmojiMessage, GameUpdates, @@ -403,11 +404,15 @@ export class PlayerView { return { hasEmbargo, hasFriendly }; } - async actions(tile?: TileRef): Promise { + async actions( + tile?: TileRef, + transportShip?: BuildableUnitsTransportShip, + ): Promise { return this.game.worker.playerInteraction( this.id(), tile && this.game.x(tile), tile && this.game.y(tile), + transportShip, ); } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 7dba7439a..1147c539a 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -16,6 +16,7 @@ import { AllPlayers, Attack, BuildableUnit, + BuildableUnitsTransportShip, Cell, ColoredTeams, Embargo, @@ -965,10 +966,21 @@ export class PlayerImpl implements Player { this.recordUnitConstructed(unit.type()); } - public buildableUnits(tile: TileRef | null): BuildableUnit[] { + public buildableUnits( + tile: TileRef | null, + transportShip: BuildableUnitsTransportShip = BuildableUnitsTransportShip.Exclude, + ): BuildableUnit[] { const validTiles = tile !== null ? this.validStructureSpawnTiles(tile) : []; - return PlayerBuildableTypes.map((u) => { + return PlayerBuildableTypes.filter((u) => { + if (transportShip === BuildableUnitsTransportShip.Exclude) { + return u !== UnitType.TransportShip; + } + if (transportShip === BuildableUnitsTransportShip.Only) { + return u === UnitType.TransportShip; + } + return true; // Include TransportShip + }).map((u) => { const cost = this.mg.config().unitInfo(u).cost(this.mg, this); let canUpgrade: number | false = false; let canBuild: TileRef | false = false; diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 31fd3f136..706bd7901 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -94,6 +94,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { message.playerID, message.x, message.y, + message.transportShip, ); sendMessage({ type: "player_actions_result", diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index fe0ac38fc..791a6620c 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -1,4 +1,5 @@ import { + BuildableUnitsTransportShip, Cell, PlayerActions, PlayerBorderTiles, @@ -164,6 +165,7 @@ export class WorkerClient { playerID: PlayerID, x?: number, y?: number, + transportShip?: BuildableUnitsTransportShip, ): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { @@ -185,9 +187,10 @@ export class WorkerClient { this.worker.postMessage({ type: "player_actions", id: messageId, - playerID: playerID, + playerID, x: x, y: y, + ...(transportShip !== undefined ? { transportShip } : {}), }); }); } diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index a8d30e9b1..2d978820f 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -1,4 +1,5 @@ import { + BuildableUnitsTransportShip, PlayerActions, PlayerBorderTiles, PlayerID, @@ -62,6 +63,7 @@ export interface PlayerActionsMessage extends BaseWorkerMessage { playerID: PlayerID; x?: number; y?: number; + transportShip?: BuildableUnitsTransportShip; } export interface PlayerActionsResultMessage extends BaseWorkerMessage { diff --git a/tests/perf/buildableunitsperf.ts b/tests/perf/buildableunitsperf.ts new file mode 100644 index 000000000..6901580de --- /dev/null +++ b/tests/perf/buildableunitsperf.ts @@ -0,0 +1,113 @@ +import Benchmark from "benchmark"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { PlayerInfo, PlayerType } from "../../src/core/game/Game"; +import { setup } from "../util/Setup"; + +// Setup dense territory scenario (large target area) +// We use a dense territory to ensure that checks like port spawning, etc. have many candidates if applicable. +// buildableUnits(null) checks global availability which might verify conditions across the map. + +const game = await setup( + "big_plains", + { + infiniteGold: true, + instantBuild: true, + }, + [new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id")], + dirname(fileURLToPath(import.meta.url)), +); + +while (game.inSpawnPhase()) { + game.executeNextTick(); +} + +const player = game.player("player_id"); + +// Conquer a significant portion of the map to have valid spawn locations for things like ports (if near water) +// and to ensure we have "validStructureSpawnTiles" cached or calculated. +for (let x = 0; x < 50; x++) { + for (let y = 0; y < 50; y++) { + const tile = game.ref(x, y); + if (game.map().isLand(tile)) { + player.conquer(tile); + } + } +} + +let specificLandTile = game.ref(25, 25); +let specificWaterTile = game.ref(0, 0); + +// Search for a water tile if 0,0 is not water +if (!game.map().isWater(specificWaterTile)) { + for (let x = 0; x < game.map().width(); x++) { + for (let y = 0; y < game.map().height(); y++) { + const t = game.ref(x, y); + if (game.map().isWater(t)) { + specificWaterTile = t; + break; + } + } + if (game.map().isWater(specificWaterTile)) break; + } +} + +// Ensure land tile is actually land +if (!game.map().isLand(specificLandTile)) { + for (let x = 0; x < game.map().width(); x++) { + for (let y = 0; y < game.map().height(); y++) { + const t = game.ref(x, y); + if (game.map().isLand(t)) { + specificLandTile = t; + break; + } + } + if (game.map().isLand(specificLandTile)) break; + } +} + +console.log("Benchmarks ready."); +console.log("Land tile:", specificLandTile.toString()); +console.log("Water tile:", specificWaterTile.toString()); + +// Warmup +player.buildableUnits(null); +player.buildableUnits(specificLandTile); + +const results: string[] = []; + +new Benchmark.Suite() + .add("buildableUnits(null)", () => { + player.buildableUnits(null); + }) + .add("buildableUnits(landTile)", () => { + player.buildableUnits(specificLandTile); + }) + .add("buildableUnits(waterTile)", () => { + player.buildableUnits(specificWaterTile); + }) + /* + Future: If buildableUnits accepts a filter argument (e.g. ignore TransportShip), add cases here for performance comparison: + + // Test case: Check only TransportShip (e.g. for right-click on water) + .add("buildableUnits(landTile, ONLY_TRANSPORT)", () => { + // @ts-ignore + player.buildableUnits(specificLandTile, { only: UnitType.TransportShip }); + }) + + // Test case: Check everything EXCEPT TransportShip (e.g. build menu on land) + .add("buildableUnits(landTile, NO_TRANSPORT)", () => { + // @ts-ignore + player.buildableUnits(specificLandTile, { exclude: UnitType.TransportShip }); + }) + */ + .on("cycle", (event: any) => { + results.push(String(event.target)); + }) + .on("complete", () => { + console.log("\n=== buildableUnits Performance Benchmark Results ==="); + for (const result of results) { + console.log(result); + } + }) + .run({ async: true });