Skip to content

Commit a10b3aa

Browse files
committed
Use game-scoped generational visited buffer in PlayerExecution
- Introduce ClusterTraversalState and a WeakMap<Game, ClusterTraversalState> in PlayerExecution.ts to store visited: Uint32Array and gen per game instance. - Remove the per-player _visitedBuffer - Update calculateClusters() to allocate/resize a single Uint32Array(totalTiles) per Game, use a generation counter instead of calling fill(0) to clear. - Switch visited checks to visited[tile] === currentGen.
1 parent 14bf09f commit a10b3aa

File tree

1 file changed

+35
-21
lines changed

1 file changed

+35
-21
lines changed

src/core/execution/PlayerExecution.ts

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ import { Execution, Game, Player, UnitType } from "../game/Game";
33
import { GameMap, TileRef } from "../game/GameMap";
44
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
55

6+
interface ClusterTraversalState {
7+
visited: Uint32Array;
8+
gen: number;
9+
}
10+
11+
// Per-game traversal state used by calculateClusters() to avoid per-player buffers.
12+
const traversalStates = new WeakMap<Game, ClusterTraversalState>();
13+
614
export class PlayerExecution implements Execution {
715
private readonly ticksPerClusterCalc = 20;
816

917
private config: Config;
1018
private lastCalc = 0;
1119
private mg: Game;
1220
private active = true;
13-
private _visitedBuffer: Uint8Array;
1421

15-
constructor(private player: Player) {
16-
this._visitedBuffer = new Uint8Array(0); // Initialize empty buffer
17-
}
22+
constructor(private player: Player) {}
1823

1924
activeDuringSpawnPhase(): boolean {
2025
return false;
@@ -264,38 +269,47 @@ export class PlayerExecution implements Execution {
264269
const borderTiles = this.player.borderTiles();
265270
if (borderTiles.size === 0) return [];
266271

267-
// Ensure buffer is large enough
268-
const mapSize = this.mg.width() * this.mg.height();
269-
if (!this._visitedBuffer || this._visitedBuffer.length < mapSize) {
270-
this._visitedBuffer = new Uint8Array(mapSize);
271-
} else {
272-
// Fast clear (much faster than creating a new Set)
273-
this._visitedBuffer.fill(0);
272+
const totalTiles = this.mg.width() * this.mg.height();
273+
274+
// Retrieve or initialize traversal state for this specific Game instance.
275+
let state = traversalStates.get(this.mg);
276+
if (!state || state.visited.length < totalTiles) {
277+
state = {
278+
visited: new Uint32Array(totalTiles),
279+
gen: 0,
280+
};
281+
traversalStates.set(this.mg, state);
282+
}
283+
284+
// Generational clear: bump generation instead of filling the array.
285+
state.gen++;
286+
if (state.gen === 0xffffffff) {
287+
// Extremely rare wrap-around; reset the buffer.
288+
state.visited.fill(0);
289+
state.gen = 1;
274290
}
275291

292+
const currentGen = state.gen;
293+
const visited = state.visited;
294+
276295
const clusters: Set<TileRef>[] = [];
277-
const stack: TileRef[] = []; // Reusable stack
296+
const stack: TileRef[] = [];
278297

279298
for (const startTile of borderTiles) {
280-
// FAST: Array access instead of Set.has()
281-
if (this._visitedBuffer[startTile] === 1) continue;
299+
if (visited[startTile] === currentGen) continue;
282300

283301
const currentCluster = new Set<TileRef>();
284302
stack.push(startTile);
285-
this._visitedBuffer[startTile] = 1;
303+
visited[startTile] = currentGen;
286304

287305
while (stack.length > 0) {
288306
const tile = stack.pop()!;
289307
currentCluster.add(tile);
290308

291-
//Use callback to avoid creating a 'neighbors' Array
292309
this.mg.forEachNeighborWithDiag(tile, (neighbor) => {
293-
if (
294-
borderTiles.has(neighbor) &&
295-
this._visitedBuffer[neighbor] === 0
296-
) {
310+
if (borderTiles.has(neighbor) && visited[neighbor] !== currentGen) {
297311
stack.push(neighbor);
298-
this._visitedBuffer[neighbor] = 1;
312+
visited[neighbor] = currentGen;
299313
}
300314
});
301315
}

0 commit comments

Comments
 (0)