import { Config } from "../configuration/Config"; import { Execution, Game, Player, UnitType } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { calculateBoundingBox, simpleHash } from "../Util"; interface ClusterTraversalState { visited: Uint32Array; gen: number; } // Per-game traversal state used by calculateClusters() to avoid per-player buffers. const traversalStates = new WeakMap(); export class PlayerExecution implements Execution { private readonly ticksPerClusterCalc = 20; private config: Config; private lastCalc = 0; private mg: Game; private active = true; constructor(private player: Player) {} activeDuringSpawnPhase(): boolean { return false; } init(mg: Game, ticks: number) { this.mg = mg; this.config = mg.config(); this.lastCalc = ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc); } tick(ticks: number) { this.player.decayRelations(); for (const u of this.player.units()) { if (!u.info().territoryBound) { continue; } const owner = this.mg!.owner(u.tile()); if (!owner?.isPlayer()) { u.delete(); continue; } if (owner === this.player) { continue; } const captor = this.mg!.player(owner.id()); if (u.type() === UnitType.DefensePost) { u.decreaseLevel(captor); if (u.isActive()) { captor.captureUnit(u); } } else { captor.captureUnit(u); } } if (!this.player.isAlive()) { // Player has no tiles, delete any remaining units and gold const gold = this.player.gold(); this.player.removeGold(gold); this.player.units().forEach((u) => { if ( u.type() !== UnitType.AtomBomb && u.type() !== UnitType.HydrogenBomb && u.type() !== UnitType.MIRVWarhead && u.type() !== UnitType.MIRV ) { u.delete(); } }); this.active = false; this.mg.stats().playerKilled(this.player, ticks); return; } const troopInc = this.config.troopIncreaseRate(this.player); this.player.addTroops(troopInc); const goldFromWorkers = this.config.goldAdditionRate(this.player); this.player.addGold(goldFromWorkers); // Record stats this.mg.stats().goldWork(this.player, goldFromWorkers); const alliances = Array.from(this.player.alliances()); for (const alliance of alliances) { if (alliance.expiresAt() <= this.mg.ticks()) { alliance.expire(); } } const embargoes = this.player.getEmbargoes(); for (const embargo of embargoes) { if ( embargo.isTemporary && this.mg.ticks() - embargo.createdAt > this.mg.config().temporaryEmbargoDuration() ) { this.player.stopEmbargo(embargo.target); } } if (ticks - this.lastCalc > this.ticksPerClusterCalc) { if (this.player.lastTileChange() > this.lastCalc) { this.lastCalc = ticks; const start = performance.now(); this.removeClusters(); const end = performance.now(); if (end - start > 1000) { console.log(`player ${this.player.name()}, took ${end - start}ms`); } } } } private removeClusters() { const clusters = this.calculateClusters(); if (clusters.length === 0) { this.player.largestClusterBoundingBox = null; return; } // Find the largest cluster with a single linear scan (O(n)). let largestIndex = 0; let largestSize = clusters[0].size; for (let i = 1; i < clusters.length; i++) { const size = clusters[i].size; if (size > largestSize) { largestSize = size; largestIndex = i; } } const largestCluster = clusters[largestIndex]; this.player.largestClusterBoundingBox = calculateBoundingBox( this.mg, largestCluster, ); } private calculateClusters(): Set[] { 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[] = []; 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; } owner(): Player { if (this.player === null) { throw new Error("Not initialized"); } return this.player; } 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; } }