Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 113 additions & 46 deletions src/core/execution/PlayerExecution.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
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 { TileRef } from "../game/GameMap";
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";

interface ClusterTraversalState {
visited: Uint32Array;
gen: number;
}

// Single shared traversal state; there is only ever one Game per runtime.
let traversalState: ClusterTraversalState | null = null;

export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;

Expand Down Expand Up @@ -132,17 +139,23 @@ export class PlayerExecution implements Execution {
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
const enemies = new Set<number>();
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;
}
Expand All @@ -165,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;
Expand Down Expand Up @@ -203,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);
Expand All @@ -219,7 +234,7 @@ export class PlayerExecution implements Execution {
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
const neighbors = new Map<Player, number>();
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() &&
Expand All @@ -228,7 +243,7 @@ export class PlayerExecution implements Execution {
) {
neighbors.set(owner, (neighbors.get(owner) ?? 0) + 1);
}
}
});
}

// If there are no enemies, return null
Expand Down Expand Up @@ -259,30 +274,25 @@ export class PlayerExecution implements Execution {
}

private calculateClusters(): Set<TileRef>[] {
const seen = new Set<TileRef>();
const border = this.player.borderTiles();
const borderTiles = this.player.borderTiles();
if (borderTiles.size === 0) return [];

const state = this.traversalState();
const currentGen = this.bumpGeneration();
const visited = state.visited;

const clusters: Set<TileRef>[] = [];
for (const tile of border) {
if (seen.has(tile)) {
continue;
}

const cluster = new Set<TileRef>();
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);

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);
}
}
}
for (const startTile of borderTiles) {
if (visited[startTile] === currentGen) continue;

const cluster = this.floodFillWithGen(
currentGen,
visited,
[startTile],
(tile, cb) => this.mg.forEachNeighborWithDiag(tile, cb),
(tile) => borderTiles.has(tile),
);
clusters.push(cluster);
}
return clusters;
Expand All @@ -298,4 +308,61 @@ export class PlayerExecution implements Execution {
isActive(): boolean {
return this.active;
}

private traversalState(): ClusterTraversalState {
const totalTiles = this.mg.width() * this.mg.height();
if (!traversalState || traversalState.visited.length < totalTiles) {
traversalState = {
visited: new Uint32Array(totalTiles),
gen: 0,
};
}
return traversalState;
}

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<TileRef> {
const result = new Set<TileRef>();
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;
}
}
9 changes: 9 additions & 0 deletions src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,15 @@ 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
forEachNeighborWithDiag(
tile: TileRef,
callback: (neighbor: TileRef) => void,
): void;

// Player Management
player(id: PlayerID): Player;
Expand Down
33 changes: 33 additions & 0 deletions src/core/game/GameImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down Expand Up @@ -858,6 +882,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);
}
Expand Down
Loading