Skip to content

Commit 53ad82f

Browse files
committed
Improve nuke targeting logic: prioritize strongest team, other little fixes
1 parent 957a763 commit 53ad82f

2 files changed

Lines changed: 97 additions & 26 deletions

File tree

src/core/execution/nation/NationNukeBehavior.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,9 @@ export class NationNukeBehavior {
3535
) {}
3636

3737
maybeSendNuke(other: Player | null) {
38-
if (this.attackBehavior === null) throw new Error("not initialized");
3938
const silos = this.player.units(UnitType.MissileSilo);
4039
if (
4140
silos.length === 0 ||
42-
this.player.gold() < this.getPerceivedNukeCost(UnitType.AtomBomb) ||
4341
other === null ||
4442
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to nations and humans)
4543
this.player.isOnSameTeam(other) ||
@@ -50,10 +48,15 @@ export class NationNukeBehavior {
5048

5149
const hydroCost = this.getPerceivedNukeCost(UnitType.HydrogenBomb);
5250
const atomCost = this.getPerceivedNukeCost(UnitType.AtomBomb);
53-
const minCost = hydroCost < atomCost ? hydroCost : atomCost;
54-
const nukeType =
55-
this.player.gold() > minCost ? UnitType.HydrogenBomb : UnitType.AtomBomb;
56-
const range = nukeType === UnitType.HydrogenBomb ? 80 : 20;
51+
let nukeType: UnitType;
52+
if (this.player.gold() >= hydroCost) {
53+
nukeType = UnitType.HydrogenBomb;
54+
} else if (this.player.gold() >= atomCost) {
55+
nukeType = UnitType.AtomBomb;
56+
} else {
57+
return;
58+
}
59+
const range = this.mg.config().nukeMagnitudes(nukeType).inner;
5760

5861
const structures = other.units(
5962
UnitType.City,
@@ -93,7 +96,7 @@ export class NationNukeBehavior {
9396
continue;
9497
}
9598

96-
const value = this.nukeTileScore(tile, silos, structures);
99+
const value = this.nukeTileScore(tile, silos, structures, nukeType);
97100
if (value > bestValue) {
98101
bestTile = tile;
99102
bestValue = value;
@@ -246,9 +249,14 @@ export class NationNukeBehavior {
246249
}
247250
}
248251

249-
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
250-
// Potential damage in a 25-tile radius
251-
const dist = euclDistFN(tile, 25, false);
252+
private nukeTileScore(
253+
tile: TileRef,
254+
silos: Unit[],
255+
targets: Unit[],
256+
nukeType: UnitType.AtomBomb | UnitType.HydrogenBomb,
257+
): number {
258+
const magnitude = this.mg.config().nukeMagnitudes(nukeType);
259+
const dist = euclDistFN(tile, magnitude.inner, false);
252260
let tileValue = targets
253261
.filter((unit) => dist(this.mg, unit.tile()))
254262
.map((unit): number => {
@@ -261,7 +269,9 @@ export class NationNukeBehavior {
261269
case UnitType.MissileSilo:
262270
return 50_000 * level;
263271
case UnitType.Port:
264-
return 10_000 * level;
272+
return 15_000 * level;
273+
case UnitType.Factory:
274+
return 15_000 * level;
265275
default:
266276
return 0;
267277
}
@@ -274,13 +284,11 @@ export class NationNukeBehavior {
274284
// On Hard & Impossible we rely on trajectory-based interception checks instead. See maybeSendNuke().
275285
if (difficulty === Difficulty.Medium) {
276286
const dist50 = euclDistFN(tile, 50, false);
277-
tileValue -=
278-
50_000 *
279-
targets.filter(
280-
(unit) =>
281-
unit.type() === UnitType.SAMLauncher &&
282-
dist50(this.mg, unit.tile()),
283-
).length;
287+
const hasSam = targets.some(
288+
(unit) =>
289+
unit.type() === UnitType.SAMLauncher && dist50(this.mg, unit.tile()),
290+
);
291+
if (hasSam) return -1;
284292
}
285293

286294
// Prefer tiles that are closer to a silo
@@ -293,8 +301,9 @@ export class NationNukeBehavior {
293301
tileValue -= distanceToClosestSilo * 30;
294302

295303
// Don't target near recent targets
304+
const dist25 = euclDistFN(tile, 25, false);
296305
tileValue -= this.lastNukeSent
297-
.filter(([_tick, tile]) => dist(this.mg, tile))
306+
.filter(([_tick, tile]) => dist25(this.mg, tile))
298307
.map((_) => 1_000_000)
299308
.reduce((prev, cur) => prev + cur, 0);
300309

@@ -306,19 +315,17 @@ export class NationNukeBehavior {
306315
nukeType: UnitType.AtomBomb | UnitType.HydrogenBomb,
307316
targetPlayer: Player,
308317
) {
309-
if (this.attackBehavior === null || this.emojiBehavior === null)
310-
throw new Error("not initialized");
311318
const tick = this.mg.ticks();
312319
this.lastNukeSent.push([tick, tile]);
313320
if (nukeType === UnitType.AtomBomb) {
314321
this.atomBombsLaunched++;
315-
// Increase perceived cost by 20% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
316-
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 120n) / 100n;
322+
// Increase perceived cost by 25% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
323+
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 125n) / 100n;
317324
} else if (nukeType === UnitType.HydrogenBomb) {
318325
this.hydrogenBombsLaunched++;
319-
// Increase perceived cost by 10% each time to simulate saving up for a MIRV
326+
// Increase perceived cost by 15% each time to simulate saving up for a MIRV
320327
this.hydrogenBombPerceivedCost =
321-
(this.hydrogenBombPerceivedCost * 110n) / 100n;
328+
(this.hydrogenBombPerceivedCost * 115n) / 100n;
322329
}
323330
this.mg.addExecution(new NukeExecution(nukeType, this.player, tile));
324331
this.emojiBehavior.maybeSendEmoji(targetPlayer, EMOJI_NUKE);

src/core/execution/utils/AiAttackBehavior.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export class AiAttackBehavior {
200200
}
201201
}
202202

203-
findBestNukeTarget(borderingEnemies: Player[]): Player | null {
203+
findBestNukeTarget(): Player | null {
204204
// Retaliate against incoming attacks (Most important!)
205205
const incomingAttackPlayer = this.findIncomingAttackPlayer();
206206
if (incomingAttackPlayer) {
@@ -240,6 +240,12 @@ export class AiAttackBehavior {
240240
return crownTarget;
241241
}
242242

243+
// In Teams, nuke the strongest team
244+
const teamTarget = this.findStrongestTeamTarget();
245+
if (teamTarget) {
246+
return teamTarget;
247+
}
248+
243249
return null;
244250
}
245251

@@ -299,6 +305,64 @@ export class AiAttackBehavior {
299305
return null;
300306
}
301307

308+
private findStrongestTeamTarget(): Player | null {
309+
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
310+
return null;
311+
}
312+
313+
if (this.game.players().length <= 1) {
314+
return null;
315+
}
316+
317+
const teamTiles = new Map<string, number>();
318+
const teamPlayers = new Map<string, Player[]>();
319+
320+
for (const p of this.game.players()) {
321+
const team = p.team();
322+
if (team === null) continue;
323+
324+
teamTiles.set(team, (teamTiles.get(team) ?? 0) + p.numTilesOwned());
325+
let players = teamPlayers.get(team);
326+
if (!players) {
327+
players = [];
328+
teamPlayers.set(team, players);
329+
}
330+
players.push(p);
331+
}
332+
333+
const sortedTeams = Array.from(teamTiles.entries()).sort(
334+
(a, b) => b[1] - a[1],
335+
);
336+
337+
if (sortedTeams.length === 0) {
338+
return null;
339+
}
340+
341+
let strongestTeam = sortedTeams[0][0];
342+
if (strongestTeam === this.player.team()) {
343+
if (sortedTeams.length > 1) {
344+
strongestTeam = sortedTeams[1][0];
345+
} else {
346+
return null;
347+
}
348+
}
349+
350+
const targetTeamPlayers = teamPlayers.get(strongestTeam)!;
351+
352+
if (this.random.chance(2)) {
353+
// Strongest player
354+
return targetTeamPlayers.reduce((prev, current) =>
355+
this.game.config().maxTroops(prev) >
356+
this.game.config().maxTroops(current)
357+
? prev
358+
: current,
359+
);
360+
} else {
361+
// Random player
362+
return this.random.randElement(targetTeamPlayers);
363+
}
364+
}
365+
302366
private hasReserveRatioTroops(): boolean {
303367
const maxTroops = this.game.config().maxTroops(this.player);
304368
const ratio = this.player.troops() / maxTroops;

0 commit comments

Comments
 (0)