diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index a8cc3fb42..a76622be3 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -1,6 +1,5 @@ import { Config } from "../configuration/Config"; import { Execution, Game, Player, UnitType } from "../game/Game"; -import { GameImpl } from "../game/GameImpl"; import { GameMap, TileRef } from "../game/GameMap"; import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; @@ -11,8 +10,11 @@ export class PlayerExecution implements Execution { private lastCalc = 0; private mg: Game; private active = true; + private _visitedBuffer: Uint8Array; - constructor(private player: Player) {} + constructor(private player: Player) { + this._visitedBuffer = new Uint8Array(0); // Initialize empty buffer + } activeDuringSpawnPhase(): boolean { return false; @@ -259,31 +261,45 @@ export class PlayerExecution implements Execution { } private calculateClusters(): Set[] { - const seen = new Set(); - const border = this.player.borderTiles(); + const borderTiles = this.player.borderTiles(); + if (borderTiles.size === 0) return []; + + // Ensure buffer is large enough + const mapSize = this.mg.width() * this.mg.height(); + if (!this._visitedBuffer || this._visitedBuffer.length < mapSize) { + this._visitedBuffer = new Uint8Array(mapSize); + } else { + // Fast clear (much faster than creating a new Set) + this._visitedBuffer.fill(0); + } + const clusters: Set[] = []; - for (const tile of border) { - if (seen.has(tile)) { - continue; - } + const stack: TileRef[] = []; // Reusable stack - const cluster = new Set(); - const queue: TileRef[] = [tile]; - seen.add(tile); - while (queue.length > 0) { - const curr = queue.shift(); - if (curr === undefined) throw new Error("curr is undefined"); - cluster.add(curr); + for (const startTile of borderTiles) { + // FAST: Array access instead of Set.has() + if (this._visitedBuffer[startTile] === 1) continue; - const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr); - for (const neighbor of neighbors) { - if (border.has(neighbor) && !seen.has(neighbor)) { - queue.push(neighbor); - seen.add(neighbor); + const currentCluster = new Set(); + stack.push(startTile); + this._visitedBuffer[startTile] = 1; + + while (stack.length > 0) { + const tile = stack.pop()!; + currentCluster.add(tile); + + //Use callback to avoid creating a 'neighbors' Array + this.mg.forEachNeighborWithDiag(tile, (neighbor) => { + if ( + borderTiles.has(neighbor) && + this._visitedBuffer[neighbor] === 0 + ) { + stack.push(neighbor); + this._visitedBuffer[neighbor] = 1; } - } + }); } - clusters.push(cluster); + clusters.push(currentCluster); } return clusters; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 9587e32be..0cf13f917 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -668,6 +668,13 @@ export interface Game extends GameMap { map(): GameMap; miniMap(): GameMap; forEachTile(fn: (tile: TileRef) => void): void; + // Zero-allocation neighbor iteration for performance-critical cluster calculation + // Alternative to neighborsWithDiag() that returns arrays + // Avoids creating intermediate arrays and uses a callback for better performance + forEachNeighborWithDiag( + tile: TileRef, + callback: (neighbor: TileRef) => void, + ): void; // Player Management player(id: PlayerID): Player; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 26127f558..57ad29055 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -517,6 +517,30 @@ export class GameImpl implements Game { return ns; } + // Zero-allocation neighbor iteration for performance-critical code + forEachNeighborWithDiag( + tile: TileRef, + callback: (neighbor: TileRef) => void, + ): void { + const x = this.x(tile); + const y = this.y(tile); + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + if (dx === 0 && dy === 0) continue; // Skip the center tile + const newX = x + dx; + const newY = y + dy; + if ( + newX >= 0 && + newX < this._width && + newY >= 0 && + newY < this._height + ) { + callback(this._map.ref(newX, newY)); + } + } + } + } + conquer(owner: PlayerImpl, tile: TileRef): void { if (!this.isLand(tile)) { throw Error(`cannot conquer water`);