Skip to content

Commit e77c9f2

Browse files
committed
unify flood-fill and neighbor callbacks in PlayerExecution
- Convert neighbor loops in surroundedBySamePlayer, isSurrounded, getCapturingPlayer to forEachNeighbor callbacks - Implement floodFillWithGen() method for configurable zero-allocation flood fill - Replace BFS in removeCluster() with floodFillWithGen using cardinal neighbors - Refactor calculateClusters() to use floodFillWithGen with diagonal neighbors - Add generational state management and forEachNeighbor interface method
1 parent a10b3aa commit e77c9f2

File tree

3 files changed

+109
-59
lines changed

3 files changed

+109
-59
lines changed

src/core/execution/PlayerExecution.ts

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Config } from "../configuration/Config";
22
import { Execution, Game, Player, UnitType } from "../game/Game";
3-
import { GameMap, TileRef } from "../game/GameMap";
3+
import { TileRef } from "../game/GameMap";
44
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
55

66
interface ClusterTraversalState {
@@ -139,17 +139,23 @@ export class PlayerExecution implements Execution {
139139
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
140140
const enemies = new Set<number>();
141141
for (const tile of cluster) {
142-
if (
143-
this.mg.isOceanShore(tile) ||
144-
this.mg.isOnEdgeOfMap(tile) ||
145-
this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n))
146-
) {
142+
let hasUnownedNeighbor = false;
143+
if (this.mg.isOceanShore(tile) || this.mg.isOnEdgeOfMap(tile)) {
144+
return false;
145+
}
146+
this.mg.forEachNeighbor(tile, (n) => {
147+
if (!this.mg.hasOwner(n)) {
148+
hasUnownedNeighbor = true;
149+
return;
150+
}
151+
const ownerId = this.mg.ownerID(n);
152+
if (ownerId !== this.player.smallID()) {
153+
enemies.add(ownerId);
154+
}
155+
});
156+
if (hasUnownedNeighbor) {
147157
return false;
148158
}
149-
this.mg
150-
.neighbors(tile)
151-
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
152-
.forEach((p) => this.mg && enemies.add(this.mg.ownerID(p)));
153159
if (enemies.size !== 1) {
154160
return false;
155161
}
@@ -172,14 +178,12 @@ export class PlayerExecution implements Execution {
172178
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
173179
return false;
174180
}
175-
this.mg
176-
.neighbors(tr)
177-
.filter(
178-
(n) =>
179-
this.mg?.owner(n).isPlayer() &&
180-
this.mg?.ownerID(n) !== this.player?.smallID(),
181-
)
182-
.forEach((n) => enemyTiles.add(n));
181+
this.mg.forEachNeighbor(tr, (n) => {
182+
const owner = this.mg.owner(n);
183+
if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) {
184+
enemyTiles.add(n);
185+
}
186+
});
183187
}
184188
if (enemyTiles.size === 0) {
185189
return false;
@@ -210,9 +214,13 @@ export class PlayerExecution implements Execution {
210214
return;
211215
}
212216

213-
const filter = (_: GameMap, t: TileRef): boolean =>
214-
this.mg?.ownerID(t) === this.player?.smallID();
215-
const tiles = this.mg.bfs(firstTile, filter);
217+
const tiles = this.floodFillWithGen(
218+
this.bumpGeneration(),
219+
this.traversalState().visited,
220+
[firstTile],
221+
(tile, cb) => this.mg.forEachNeighbor(tile, cb),
222+
(tile) => this.mg.ownerID(tile) === this.player.smallID(),
223+
);
216224

217225
if (this.player.numTilesOwned() === tiles.size) {
218226
this.mg.conquerPlayer(capturing, this.player);
@@ -226,7 +234,7 @@ export class PlayerExecution implements Execution {
226234
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
227235
const neighbors = new Map<Player, number>();
228236
for (const t of cluster) {
229-
for (const neighbor of this.mg.neighbors(t)) {
237+
this.mg.forEachNeighbor(t, (neighbor) => {
230238
const owner = this.mg.owner(neighbor);
231239
if (
232240
owner.isPlayer() &&
@@ -235,7 +243,7 @@ export class PlayerExecution implements Execution {
235243
) {
236244
neighbors.set(owner, (neighbors.get(owner) ?? 0) + 1);
237245
}
238-
}
246+
});
239247
}
240248

241249
// If there are no enemies, return null
@@ -269,9 +277,40 @@ export class PlayerExecution implements Execution {
269277
const borderTiles = this.player.borderTiles();
270278
if (borderTiles.size === 0) return [];
271279

272-
const totalTiles = this.mg.width() * this.mg.height();
280+
const state = this.traversalState();
281+
const currentGen = this.bumpGeneration();
282+
const visited = state.visited;
283+
284+
const clusters: Set<TileRef>[] = [];
285+
286+
for (const startTile of borderTiles) {
287+
if (visited[startTile] === currentGen) continue;
288+
289+
const cluster = this.floodFillWithGen(
290+
currentGen,
291+
visited,
292+
[startTile],
293+
(tile, cb) => this.mg.forEachNeighborWithDiag(tile, cb),
294+
(tile) => borderTiles.has(tile),
295+
);
296+
clusters.push(cluster);
297+
}
298+
return clusters;
299+
}
300+
301+
owner(): Player {
302+
if (this.player === null) {
303+
throw new Error("Not initialized");
304+
}
305+
return this.player;
306+
}
273307

274-
// Retrieve or initialize traversal state for this specific Game instance.
308+
isActive(): boolean {
309+
return this.active;
310+
}
311+
312+
private traversalState(): ClusterTraversalState {
313+
const totalTiles = this.mg.width() * this.mg.height();
275314
let state = traversalStates.get(this.mg);
276315
if (!state || state.visited.length < totalTiles) {
277316
state = {
@@ -280,52 +319,52 @@ export class PlayerExecution implements Execution {
280319
};
281320
traversalStates.set(this.mg, state);
282321
}
322+
return state;
323+
}
283324

284-
// Generational clear: bump generation instead of filling the array.
325+
private bumpGeneration(): number {
326+
const state = this.traversalState();
285327
state.gen++;
286328
if (state.gen === 0xffffffff) {
287-
// Extremely rare wrap-around; reset the buffer.
288329
state.visited.fill(0);
289330
state.gen = 1;
290331
}
332+
return state.gen;
333+
}
291334

292-
const currentGen = state.gen;
293-
const visited = state.visited;
294-
295-
const clusters: Set<TileRef>[] = [];
335+
private floodFillWithGen(
336+
currentGen: number,
337+
visited: Uint32Array,
338+
startTiles: TileRef[],
339+
neighborFn: (tile: TileRef, callback: (neighbor: TileRef) => void) => void,
340+
includeFn: (tile: TileRef) => boolean,
341+
): Set<TileRef> {
342+
const result = new Set<TileRef>();
296343
const stack: TileRef[] = [];
297344

298-
for (const startTile of borderTiles) {
299-
if (visited[startTile] === currentGen) continue;
300-
301-
const currentCluster = new Set<TileRef>();
302-
stack.push(startTile);
303-
visited[startTile] = currentGen;
304-
305-
while (stack.length > 0) {
306-
const tile = stack.pop()!;
307-
currentCluster.add(tile);
308-
309-
this.mg.forEachNeighborWithDiag(tile, (neighbor) => {
310-
if (borderTiles.has(neighbor) && visited[neighbor] !== currentGen) {
311-
stack.push(neighbor);
312-
visited[neighbor] = currentGen;
313-
}
314-
});
315-
}
316-
clusters.push(currentCluster);
345+
for (const start of startTiles) {
346+
if (visited[start] === currentGen) continue;
347+
if (!includeFn(start)) continue;
348+
visited[start] = currentGen;
349+
result.add(start);
350+
stack.push(start);
317351
}
318-
return clusters;
319-
}
320352

321-
owner(): Player {
322-
if (this.player === null) {
323-
throw new Error("Not initialized");
353+
while (stack.length > 0) {
354+
const tile = stack.pop()!;
355+
neighborFn(tile, (neighbor) => {
356+
if (visited[neighbor] === currentGen) {
357+
return;
358+
}
359+
if (!includeFn(neighbor)) {
360+
return;
361+
}
362+
visited[neighbor] = currentGen;
363+
result.add(neighbor);
364+
stack.push(neighbor);
365+
});
324366
}
325-
return this.player;
326-
}
327367

328-
isActive(): boolean {
329-
return this.active;
368+
return result;
330369
}
331370
}

src/core/game/Game.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,8 @@ export interface Game extends GameMap {
664664
map(): GameMap;
665665
miniMap(): GameMap;
666666
forEachTile(fn: (tile: TileRef) => void): void;
667+
// Zero-allocation neighbor iteration (cardinal only) to avoid creating arrays
668+
forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void;
667669
// Zero-allocation neighbor iteration for performance-critical cluster calculation
668670
// Alternative to neighborsWithDiag() that returns arrays
669671
// Avoids creating intermediate arrays and uses a callback for better performance

src/core/game/GameImpl.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,15 @@ export class GameImpl implements Game {
882882
neighbors(ref: TileRef): TileRef[] {
883883
return this._map.neighbors(ref);
884884
}
885+
// Zero-allocation neighbor iteration (cardinal only)
886+
forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void {
887+
const x = this.x(tile);
888+
const y = this.y(tile);
889+
if (x > 0) callback(this._map.ref(x - 1, y));
890+
if (x + 1 < this._width) callback(this._map.ref(x + 1, y));
891+
if (y > 0) callback(this._map.ref(x, y - 1));
892+
if (y + 1 < this._height) callback(this._map.ref(x, y + 1));
893+
}
885894
isWater(ref: TileRef): boolean {
886895
return this._map.isWater(ref);
887896
}

0 commit comments

Comments
 (0)