Exclude Transport Ship in many cases

This commit is contained in:
VariableVince
2026-02-14 06:27:47 +01:00
parent 70b5c0f2cc
commit 713ec85ca9
10 changed files with 185 additions and 29 deletions
+27 -21
View File
@@ -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,
),
);
}
+5 -2
View File
@@ -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()!,
+3 -1
View File
@@ -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;
+10 -1
View File
@@ -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<T extends UnitType>(
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.
+6 -1
View File
@@ -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<PlayerActions> {
async actions(
tile?: TileRef,
transportShip?: BuildableUnitsTransportShip,
): Promise<PlayerActions> {
return this.game.worker.playerInteraction(
this.id(),
tile && this.game.x(tile),
tile && this.game.y(tile),
transportShip,
);
}
+14 -2
View File
@@ -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;
+1
View File
@@ -94,6 +94,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.playerID,
message.x,
message.y,
message.transportShip,
);
sendMessage({
type: "player_actions_result",
+4 -1
View File
@@ -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<PlayerActions> {
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 } : {}),
});
});
}
+2
View File
@@ -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 {
+113
View File
@@ -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 });