Skip to content
47 changes: 33 additions & 14 deletions src/core/execution/NukeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export class NukeExecution implements Execution {
private active = true;
private mg: Game;
private nuke: Unit | null = null;
private tilesToDestroyCache: Set<TileRef> | undefined;
private pathFinder: ParabolaPathFinder;

constructor(
Expand All @@ -44,45 +43,64 @@ export class NukeExecution implements Execution {
return this.mg.owner(this.dst);
}

private tilesToDestroy(): Set<TileRef> {
if (this.tilesToDestroyCache !== undefined) {
return this.tilesToDestroyCache;
private tilesInRange(): Map<TileRef, number> {
if (this.nuke === null) {
throw new Error("Not initialized");
}
const tilesInRange = new Map<TileRef, number>();
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);
return true;
}
return false;
});
return tilesInRange;
}

private tilesToDestroy(): Set<TileRef> {
if (this.nuke === null) {
throw new Error("Not initialized");
}
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
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;
}

private maybeBreakAlliances(toDestroy: Set<TileRef>) {
/**
* Break alliances based on all tiles in range.
* Tiles are weighted roughly based on their chance of being destroyed.
*/
private maybeBreakAlliances(inRange: Map<TileRef, number>) {
if (this.nuke === null) {
throw new Error("Not initialized");
}
const attacked = new Map<Player, number>();
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);
Expand Down Expand Up @@ -120,7 +138,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()) {
Expand Down Expand Up @@ -233,7 +253,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)
Expand Down
Loading