diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 8f81c6242..16549c607 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -1,6 +1,6 @@ import { Config } from "../configuration/Config"; import { Execution, Game, Player, UnitType } from "../game/Game"; -import { GameMap, TileRef } from "../game/GameMap"; +import { TileRef } from "../game/GameMap"; import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; interface ClusterTraversalState { @@ -139,17 +139,23 @@ export class PlayerExecution implements Execution { private surroundedBySamePlayer(cluster: Set): false | Player { const enemies = new Set(); for (const tile of cluster) { - if ( - this.mg.isOceanShore(tile) || - this.mg.isOnEdgeOfMap(tile) || - this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n)) - ) { + let hasUnownedNeighbor = false; + if (this.mg.isOceanShore(tile) || this.mg.isOnEdgeOfMap(tile)) { + return false; + } + this.mg.forEachNeighbor(tile, (n) => { + if (!this.mg.hasOwner(n)) { + hasUnownedNeighbor = true; + return; + } + const ownerId = this.mg.ownerID(n); + if (ownerId !== this.player.smallID()) { + enemies.add(ownerId); + } + }); + if (hasUnownedNeighbor) { return false; } - this.mg - .neighbors(tile) - .filter((n) => this.mg?.ownerID(n) !== this.player?.smallID()) - .forEach((p) => this.mg && enemies.add(this.mg.ownerID(p))); if (enemies.size !== 1) { return false; } @@ -172,14 +178,12 @@ export class PlayerExecution implements Execution { if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) { return false; } - this.mg - .neighbors(tr) - .filter( - (n) => - this.mg?.owner(n).isPlayer() && - this.mg?.ownerID(n) !== this.player?.smallID(), - ) - .forEach((n) => enemyTiles.add(n)); + this.mg.forEachNeighbor(tr, (n) => { + const owner = this.mg.owner(n); + if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) { + enemyTiles.add(n); + } + }); } if (enemyTiles.size === 0) { return false; @@ -210,9 +214,13 @@ export class PlayerExecution implements Execution { return; } - const filter = (_: GameMap, t: TileRef): boolean => - this.mg?.ownerID(t) === this.player?.smallID(); - const tiles = this.mg.bfs(firstTile, filter); + const tiles = this.floodFillWithGen( + this.bumpGeneration(), + this.traversalState().visited, + [firstTile], + (tile, cb) => this.mg.forEachNeighbor(tile, cb), + (tile) => this.mg.ownerID(tile) === this.player.smallID(), + ); if (this.player.numTilesOwned() === tiles.size) { this.mg.conquerPlayer(capturing, this.player); @@ -226,7 +234,7 @@ export class PlayerExecution implements Execution { private getCapturingPlayer(cluster: Set): Player | null { const neighbors = new Map(); for (const t of cluster) { - for (const neighbor of this.mg.neighbors(t)) { + this.mg.forEachNeighbor(t, (neighbor) => { const owner = this.mg.owner(neighbor); if ( owner.isPlayer() && @@ -235,7 +243,7 @@ export class PlayerExecution implements Execution { ) { neighbors.set(owner, (neighbors.get(owner) ?? 0) + 1); } - } + }); } // If there are no enemies, return null @@ -269,51 +277,23 @@ export class PlayerExecution implements Execution { const borderTiles = this.player.borderTiles(); if (borderTiles.size === 0) return []; - const totalTiles = this.mg.width() * this.mg.height(); - - // Retrieve or initialize traversal state for this specific Game instance. - let state = traversalStates.get(this.mg); - if (!state || state.visited.length < totalTiles) { - state = { - visited: new Uint32Array(totalTiles), - gen: 0, - }; - traversalStates.set(this.mg, state); - } - - // Generational clear: bump generation instead of filling the array. - state.gen++; - if (state.gen === 0xffffffff) { - // Extremely rare wrap-around; reset the buffer. - state.visited.fill(0); - state.gen = 1; - } - - const currentGen = state.gen; + const state = this.traversalState(); + const currentGen = this.bumpGeneration(); const visited = state.visited; const clusters: Set[] = []; - const stack: TileRef[] = []; for (const startTile of borderTiles) { if (visited[startTile] === currentGen) continue; - const currentCluster = new Set(); - stack.push(startTile); - visited[startTile] = currentGen; - - while (stack.length > 0) { - const tile = stack.pop()!; - currentCluster.add(tile); - - this.mg.forEachNeighborWithDiag(tile, (neighbor) => { - if (borderTiles.has(neighbor) && visited[neighbor] !== currentGen) { - stack.push(neighbor); - visited[neighbor] = currentGen; - } - }); - } - clusters.push(currentCluster); + const cluster = this.floodFillWithGen( + currentGen, + visited, + [startTile], + (tile, cb) => this.mg.forEachNeighborWithDiag(tile, cb), + (tile) => borderTiles.has(tile), + ); + clusters.push(cluster); } return clusters; } @@ -328,4 +308,63 @@ export class PlayerExecution implements Execution { isActive(): boolean { return this.active; } + + private traversalState(): ClusterTraversalState { + const totalTiles = this.mg.width() * this.mg.height(); + let state = traversalStates.get(this.mg); + if (!state || state.visited.length < totalTiles) { + state = { + visited: new Uint32Array(totalTiles), + gen: 0, + }; + traversalStates.set(this.mg, state); + } + return state; + } + + private bumpGeneration(): number { + const state = this.traversalState(); + state.gen++; + if (state.gen === 0xffffffff) { + state.visited.fill(0); + state.gen = 1; + } + return state.gen; + } + + private floodFillWithGen( + currentGen: number, + visited: Uint32Array, + startTiles: TileRef[], + neighborFn: (tile: TileRef, callback: (neighbor: TileRef) => void) => void, + includeFn: (tile: TileRef) => boolean, + ): Set { + const result = new Set(); + const stack: TileRef[] = []; + + for (const start of startTiles) { + if (visited[start] === currentGen) continue; + if (!includeFn(start)) continue; + visited[start] = currentGen; + result.add(start); + stack.push(start); + } + + while (stack.length > 0) { + const tile = stack.pop()!; + neighborFn(tile, (neighbor) => { + if (visited[neighbor] === currentGen) { + return; + } + if (!includeFn(neighbor)) { + return; + } + visited[neighbor] = currentGen; + result.add(neighbor); + stack.push(neighbor); + }); + } + + return result; + } } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 0cf13f917..93b8af56d 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -668,6 +668,8 @@ export interface Game extends GameMap { map(): GameMap; miniMap(): GameMap; forEachTile(fn: (tile: TileRef) => void): void; + // Zero-allocation neighbor iteration (cardinal only) to avoid creating arrays + forEachNeighbor(tile: TileRef, callback: (neighbor: 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 diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 57ad29055..73a5faf87 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -898,6 +898,15 @@ export class GameImpl implements Game { neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref); } + // Zero-allocation neighbor iteration (cardinal only) + forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void { + const x = this.x(tile); + const y = this.y(tile); + if (x > 0) callback(this._map.ref(x - 1, y)); + if (x + 1 < this._width) callback(this._map.ref(x + 1, y)); + if (y > 0) callback(this._map.ref(x, y - 1)); + if (y + 1 < this._height) callback(this._map.ref(x, y + 1)); + } isWater(ref: TileRef): boolean { return this._map.isWater(ref); }