From a579c6ed8c3091909f22f6f148e31688b6be4409 Mon Sep 17 00:00:00 2001 From: bibizu Date: Tue, 2 Dec 2025 23:56:28 -0500 Subject: [PATCH 01/10] fix: Alliance breakage is deterministic now And only breaks alliances once, on launch. --- src/core/execution/NukeExecution.ts | 35 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 923535f1fe..240a8dc72f 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -20,6 +20,7 @@ export class NukeExecution implements Execution { private active = true; private mg: Game; private nuke: Unit | null = null; + private tilesInRangeCache: Map; private tilesToDestroyCache: Set | undefined; private pathFinder: ParabolaPathFinder; @@ -44,6 +45,25 @@ export class NukeExecution implements Execution { return this.mg.owner(this.dst); } + private tilesInRange(): Map { + if (this.tilesInRangeCache !== undefined) { + return this.tilesInRangeCache; + } + if (this.nuke === null) { + throw new Error("Not initialized"); + } + this.tilesInRangeCache = new Map(); + const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); + const inner2 = magnitude.inner * magnitude.inner; + const outer2 = magnitude.outer * magnitude.outer; + this.mg.bfs(this.dst, (_, n: TileRef) => { + const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; + this.tilesInRangeCache.set(n, d2 <= inner2 ? 1 : 0.5); + return d2 <= outer2; + }); + return this.tilesInRangeCache; + } + private tilesToDestroy(): Set { if (this.tilesToDestroyCache !== undefined) { return this.tilesToDestroyCache; @@ -62,16 +82,20 @@ export class NukeExecution implements Execution { return this.tilesToDestroyCache; } - private maybeBreakAlliances(toDestroy: Set) { + /** + * Break alliances based on all tiles in range. + * Tiles are weighted based on their chance (1/odds) 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); } } @@ -82,7 +106,7 @@ export class NukeExecution implements Execution { 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 +144,7 @@ export class NukeExecution implements Execution { targetTile: this.dst, trajectory: this.getTrajectory(this.dst), }); - this.maybeBreakAlliances(this.tilesToDestroy()); + 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) From 16c18f300edb84736924130e65e2b66db7800a29 Mon Sep 17 00:00:00 2001 From: bibizu Date: Wed, 3 Dec 2025 00:01:03 -0500 Subject: [PATCH 02/10] style: fix misleading doc comment --- src/core/execution/NukeExecution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 240a8dc72f..d335ecbb5a 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -84,7 +84,7 @@ export class NukeExecution implements Execution { /** * Break alliances based on all tiles in range. - * Tiles are weighted based on their chance (1/odds) of being destroyed. + * Tiles are weighted roughly based on their chance of being destroyed. */ private maybeBreakAlliances(inRange: Map) { if (this.nuke === null) { From 4129b54f91d12138d1c7b9c24b9ea11118c0bc53 Mon Sep 17 00:00:00 2001 From: bibizu Date: Wed, 3 Dec 2025 00:06:43 -0500 Subject: [PATCH 03/10] style: remove tile caches each check is only called once now --- src/core/execution/NukeExecution.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index d335ecbb5a..e3cf24cc7b 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -20,8 +20,6 @@ export class NukeExecution implements Execution { private active = true; private mg: Game; private nuke: Unit | null = null; - private tilesInRangeCache: Map; - private tilesToDestroyCache: Set | undefined; private pathFinder: ParabolaPathFinder; constructor( @@ -46,28 +44,22 @@ export class NukeExecution implements Execution { } private tilesInRange(): Map { - if (this.tilesInRangeCache !== undefined) { - return this.tilesInRangeCache; - } if (this.nuke === null) { throw new Error("Not initialized"); } - this.tilesInRangeCache = new Map(); + const tilesInRange = new Map(); const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); const inner2 = magnitude.inner * magnitude.inner; const outer2 = magnitude.outer * magnitude.outer; this.mg.bfs(this.dst, (_, n: TileRef) => { const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; - this.tilesInRangeCache.set(n, d2 <= inner2 ? 1 : 0.5); + tilesInRange.set(n, d2 <= inner2 ? 1 : 0.5); return d2 <= outer2; }); - return this.tilesInRangeCache; + return tilesInRange; } private tilesToDestroy(): Set { - if (this.tilesToDestroyCache !== undefined) { - return this.tilesToDestroyCache; - } if (this.nuke === null) { throw new Error("Not initialized"); } @@ -75,11 +67,10 @@ export class NukeExecution implements Execution { const rand = new PseudoRandom(this.mg.ticks()); const inner2 = magnitude.inner * magnitude.inner; const outer2 = magnitude.outer * magnitude.outer; - this.tilesToDestroyCache = this.mg.bfs(this.dst, (_, n: TileRef) => { + return this.mg.bfs(this.dst, (_, n: TileRef) => { const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; return d2 <= outer2 && (d2 <= inner2 || rand.chance(2)); }); - return this.tilesToDestroyCache; } /** From adbf13682313868790c3f43aea52c04f18c57741 Mon Sep 17 00:00:00 2001 From: bibizu Date: Wed, 3 Dec 2025 00:10:02 -0500 Subject: [PATCH 04/10] style: rename to reflect weight change --- src/core/execution/NukeExecution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index e3cf24cc7b..6b362e7562 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -91,9 +91,9 @@ export class NukeExecution implements Execution { } 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 From 8be2e79f0168230b954fd6221b878673bcf28271 Mon Sep 17 00:00:00 2001 From: bibizu Date: Wed, 3 Dec 2025 00:17:55 -0500 Subject: [PATCH 05/10] fix: add tiles into range after check thx rabbit --- src/core/execution/NukeExecution.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 6b362e7562..035464628a 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -53,8 +53,11 @@ export class NukeExecution implements Execution { const outer2 = magnitude.outer * magnitude.outer; this.mg.bfs(this.dst, (_, n: TileRef) => { const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; - tilesInRange.set(n, d2 <= inner2 ? 1 : 0.5); - return d2 <= outer2; + if (d2 <= outer2) { + tilesInRange.set(n, d2 <= inner2 ? 1 : 0.5); + return true; + } + return false; }); return tilesInRange; } From 81b9c54770fb1483513d4f0f71edcec13ad4cdcf Mon Sep 17 00:00:00 2001 From: bibizu Date: Wed, 3 Dec 2025 00:29:23 -0500 Subject: [PATCH 06/10] perf: exclude extra bfs search if mirv --- src/core/execution/NukeExecution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 035464628a..e6e1278c4e 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -138,7 +138,9 @@ export class NukeExecution implements Execution { targetTile: this.dst, trajectory: this.getTrajectory(this.dst), }); - this.maybeBreakAlliances(this.tilesInRange()); + 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()) { From 6d38bf210827d8fa543329c6b986702c26704d53 Mon Sep 17 00:00:00 2001 From: bibizu Date: Thu, 4 Dec 2025 00:09:07 -0500 Subject: [PATCH 07/10] perf: Circle search algorithm to replace bfs --- src/core/execution/NukeExecution.ts | 15 +++++++-------- src/core/game/GameImpl.ts | 7 +++++++ src/core/game/GameMap.ts | 24 ++++++++++++++++++++++++ src/core/game/GameView.ts | 7 +++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index e6e1278c4e..3671d386e2 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -50,15 +50,14 @@ export class NukeExecution implements Execution { const tilesInRange = new Map(); const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type()); const inner2 = magnitude.inner * magnitude.inner; - const outer2 = magnitude.outer * magnitude.outer; - this.mg.bfs(this.dst, (_, n: TileRef) => { - const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; - if (d2 <= outer2) { - tilesInRange.set(n, d2 <= inner2 ? 1 : 0.5); + this.mg.circleSearch( + this.dst, + magnitude.outer, + (t: TileRef, d2: number) => { + tilesInRange.set(t, d2 <= inner2 ? 1 : 0.5); return true; - } - return false; - }); + }, + ); return tilesInRange; } 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..f2cc39ab42 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,25 @@ 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(); + for (let i = center.x - radius; i <= center.x + radius; ++i) { + for (let j = center.y - radius; j <= center.y + radius; j++) { + const t = this.ref(i, j); + 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, From 50a059845c69bcf66f2003dfbdb0a8b86ce0ab71 Mon Sep 17 00:00:00 2001 From: bibizu Date: Thu, 4 Dec 2025 00:17:10 -0500 Subject: [PATCH 08/10] fix: oops edge of map crash --- src/core/game/GameMap.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index f2cc39ab42..e5ddda774e 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -300,8 +300,12 @@ export class GameMapImpl implements GameMap { ): Set { const center = { x: this.x(tile), y: this.y(tile) }; const tiles: Set = new Set(); - for (let i = center.x - radius; i <= center.x + radius; ++i) { - for (let j = center.y - radius; j <= center.y + radius; j++) { + 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.ref(i, j); const d2 = this.euclideanDistSquared(tile, t); if (d2 > radius * radius) continue; From 4f2740a7c2e586deaaf9e8b344abc3609f350d35 Mon Sep 17 00:00:00 2001 From: bibizu Date: Thu, 4 Dec 2025 00:18:19 -0500 Subject: [PATCH 09/10] style: rabbit nitpick --- src/core/game/GameMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index e5ddda774e..8d23b926e3 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -306,7 +306,7 @@ export class GameMapImpl implements GameMap { 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.ref(i, j); + const t = this.yToRef[j] + i; const d2 = this.euclideanDistSquared(tile, t); if (d2 > radius * radius) continue; if (!filter || filter(t, d2)) { From 637562745117e0d8b70eb6e1b6c60e5d62fff9dc Mon Sep 17 00:00:00 2001 From: bibizu Date: Thu, 4 Dec 2025 12:51:25 -0500 Subject: [PATCH 10/10] fix: re-add toDestroy cache --- src/core/execution/NukeExecution.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 3671d386e2..c9114c84cb 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -20,6 +20,7 @@ export class NukeExecution implements Execution { private active = true; private mg: Game; private nuke: Unit | null = null; + private tilesToDestroyCache: Set | undefined; private pathFinder: ParabolaPathFinder; constructor( @@ -62,6 +63,9 @@ export class NukeExecution implements Execution { } private tilesToDestroy(): Set { + if (this.tilesToDestroyCache !== undefined) { + return this.tilesToDestroyCache; + } if (this.nuke === null) { throw new Error("Not initialized"); } @@ -69,10 +73,11 @@ export class NukeExecution implements Execution { const rand = new PseudoRandom(this.mg.ticks()); const inner2 = magnitude.inner * magnitude.inner; const outer2 = magnitude.outer * magnitude.outer; - return this.mg.bfs(this.dst, (_, n: TileRef) => { + this.tilesToDestroyCache = this.mg.bfs(this.dst, (_, n: TileRef) => { const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0; return d2 <= outer2 && (d2 <= inner2 || rand.chance(2)); }); + return this.tilesToDestroyCache; } /**