diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 923535f1fe..c9114c84cb 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -44,6 +44,24 @@ export class NukeExecution implements Execution { return this.mg.owner(this.dst); } + private tilesInRange(): Map { + if (this.nuke === null) { + throw new Error("Not initialized"); + } + const tilesInRange = new Map(); + const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); + const inner2 = magnitude.inner * magnitude.inner; + this.mg.circleSearch( + this.dst, + magnitude.outer, + (t: TileRef, d2: number) => { + tilesInRange.set(t, d2 <= inner2 ? 1 : 0.5); + return true; + }, + ); + return tilesInRange; + } + private tilesToDestroy(): Set { if (this.tilesToDestroyCache !== undefined) { return this.tilesToDestroyCache; @@ -62,27 +80,31 @@ export class NukeExecution implements Execution { return this.tilesToDestroyCache; } - private maybeBreakAlliances(toDestroy: Set) { + /** + * Break alliances based on all tiles in range. + * Tiles are weighted roughly based on their chance of being destroyed. + */ + private maybeBreakAlliances(inRange: Map) { if (this.nuke === null) { throw new Error("Not initialized"); } const attacked = new Map(); - for (const tile of toDestroy) { + for (const [tile, weight] of inRange.entries()) { const owner = this.mg.owner(tile); if (owner.isPlayer()) { const prev = attacked.get(owner) ?? 0; - attacked.set(owner, prev + 1); + attacked.set(owner, prev + weight); } } const threshold = this.mg.config().nukeAllianceBreakThreshold(); - for (const [attackedPlayer, tilesDestroyed] of attacked) { + for (const [attackedPlayer, totalWeight] of attacked) { if ( - tilesDestroyed > threshold && + totalWeight > threshold && this.nuke.type() !== UnitType.MIRVWarhead ) { // Resolves exploit of alliance breaking in which a pending alliance request - // was accepeted in the middle of an missle attack. + // was accepted in the middle of a missile attack. const allianceRequest = attackedPlayer .incomingAllianceRequests() .find((ar) => ar.requestor() === this.player); @@ -120,7 +142,9 @@ export class NukeExecution implements Execution { targetTile: this.dst, trajectory: this.getTrajectory(this.dst), }); - this.maybeBreakAlliances(this.tilesToDestroy()); + if (this.nuke.type() !== UnitType.MIRVWarhead) { + this.maybeBreakAlliances(this.tilesInRange()); + } if (this.mg.hasOwner(this.dst)) { const target = this.mg.owner(this.dst); if (!target.isPlayer()) { @@ -233,7 +257,6 @@ export class NukeExecution implements Execution { const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); const toDestroy = this.tilesToDestroy(); - this.maybeBreakAlliances(toDestroy); const maxTroops = this.target().isPlayer() ? this.mg.config().maxTroops(this.target() as Player) diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 2c23ad8641..750bf52265 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -882,6 +882,13 @@ export class GameImpl implements Game { euclideanDistSquared(c1: TileRef, c2: TileRef): number { return this._map.euclideanDistSquared(c1, c2); } + circleSearch( + tile: TileRef, + radius: number, + filter?: (tile: TileRef, d2: number) => boolean, + ): Set { + return this._map.circleSearch(tile, radius, filter); + } bfs( tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean, diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index 7a3bd8e6d5..8d23b926e3 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -39,6 +39,11 @@ export interface GameMap { manhattanDist(c1: TileRef, c2: TileRef): number; euclideanDistSquared(c1: TileRef, c2: TileRef): number; + circleSearch( + tile: TileRef, + radius: number, + filter?: (tile: TileRef, d2: number) => boolean, + ): Set; bfs( tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean, @@ -288,6 +293,29 @@ export class GameMapImpl implements GameMap { const y = this.y(c1) - this.y(c2); return x * x + y * y; } + circleSearch( + tile: TileRef, + radius: number, + filter?: (tile: TileRef, d2: number) => boolean, + ): Set { + const center = { x: this.x(tile), y: this.y(tile) }; + const tiles: Set = new Set(); + const minX = Math.max(0, center.x - radius); + const maxX = Math.min(this.width_ - 1, center.x + radius); + const minY = Math.max(0, center.y - radius); + const maxY = Math.min(this.height_ - 1, center.y + radius); + for (let i = minX; i <= maxX; ++i) { + for (let j = minY; j <= maxY; j++) { + const t = this.yToRef[j] + i; + const d2 = this.euclideanDistSquared(tile, t); + if (d2 > radius * radius) continue; + if (!filter || filter(t, d2)) { + tiles.add(t); + } + } + } + return tiles; + } bfs( tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean, diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index c525129d10..0b26b4b121 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -762,6 +762,13 @@ export class GameView implements GameMap { euclideanDistSquared(c1: TileRef, c2: TileRef): number { return this._map.euclideanDistSquared(c1, c2); } + circleSearch( + tile: TileRef, + radius: number, + filter?: (tile: TileRef, d2: number) => boolean, + ): Set { + return this._map.circleSearch(tile, radius, filter); + } bfs( tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean,