Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/ai-profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"discountFactor": 0.1,
"weightWarship": 1,
"warshipTradeIncomeWeight": 0,
"warshipCoastalThreatWeight": 8e4,
"warshipCoastalThreatWeight": 10e4,
"firstPortIncomeShare": 0.45
}
},
Expand Down
43 changes: 42 additions & 1 deletion src/client/graphics/layers/ConstructionDebugOverlay.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NukeScoreBreakdown } from "../../../core/ai/AINukeHandler";
import {
ConstructionDebugData,
ConstructionScoreEntry,
Expand Down Expand Up @@ -272,8 +273,48 @@ export class ConstructionDebugOverlay implements Layer {
</tr>
</table>
<div style="margin-top: 2px; color: #aaa;">
Adjusted best nuke score (×multiplier×7): <b style="color: ${n.adjustedBestNukeScore > 0 ? "#ff8888" : "#888"};">${this.formatScore(n.adjustedBestNukeScore)}</b>
Adjusted best nuke score (×multiplier×0.7): <b style="color: ${n.adjustedBestNukeScore > 0 ? "#ff8888" : "#888"};">${this.formatScore(n.adjustedBestNukeScore)}</b>
</div>
${this.renderNukeBreakdown("Atom", n.atomBreakdown)}
${this.renderNukeBreakdown("Hydrogen", n.hydrogenBreakdown)}
</div>
`;
}

private renderNukeBreakdown(
label: string,
b: NukeScoreBreakdown | null,
): string {
if (!b) return "";
const fmt = (v: number) => this.formatScore(v);
const fmtK = (v: number) =>
v >= 1_000_000
? (v / 1_000_000).toFixed(2) + "M"
: v >= 1000
? (v / 1000).toFixed(1) + "k"
: v.toFixed(0);
return `
<div style="margin-top: 4px; padding: 4px; background: rgba(255,255,255,0.03); border-radius: 3px;">
<b style="font-size: 10px; color: #ff8888;">${label} Breakdown</b>
<table style="width: 100%; border-collapse: collapse; font-size: 10px; margin-top: 2px;">
<tr><td style="color:#aaa;padding:1px 4px;">Enemy structs</td><td style="text-align:right;padding:1px 4px;">${b.enemyStructureCount}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Raw enemy value</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.rawEnemyValue)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Strongest enemy?</td><td style="text-align:right;padding:1px 4px;">${b.isStrongestEnemy ? "Yes (+1000/struct)" : "No"}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">War score (raw)</td><td style="text-align:right;padding:1px 4px;">${b.rawWarScore.toFixed(1)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">War score sigmoid</td><td style="text-align:right;padding:1px 4px;">${b.warScoreSigmoid.toFixed(4)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Friendly structs</td><td style="text-align:right;padding:1px 4px;">${b.friendlyStructureCount}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Friendly value</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.friendlyValue)}</td></tr>
<tr style="border-top:1px solid rgba(255,255,255,0.1);"><td style="color:#ccc;padding:1px 4px;"><b>Numerator</b></td><td style="text-align:right;padding:1px 4px;"><b>${fmt(b.numerator)}</b></td></tr>
<tr><td colspan="2" style="padding:2px 4px;color:#666;">───── Cost / Discount ─────</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Bomb cost</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.bombCost)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">SAM levels</td><td style="text-align:right;padding:1px 4px;">${b.samLevels}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Silo cost (amort.)</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.siloCost)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Total cost</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.totalCost)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">Gold/min</td><td style="text-align:right;padding:1px 4px;">${fmtK(b.goldPerMinute)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">T (minutes)</td><td style="text-align:right;padding:1px 4px;">${b.T === Infinity ? "∞" : b.T.toFixed(2)}</td></tr>
<tr><td style="color:#aaa;padding:1px 4px;">(1+r)^T</td><td style="text-align:right;padding:1px 4px;">${b.discountFactor >= 1e6 ? b.discountFactor.toExponential(2) : b.discountFactor.toFixed(4)}</td></tr>
<tr style="border-top:1px solid rgba(255,255,255,0.1);"><td style="color:#ff8888;padding:1px 4px;"><b>Final Score</b></td><td style="text-align:right;padding:1px 4px;color:#ff8888;"><b>${fmt(b.finalScore)}</b></td></tr>
</table>
</div>
`;
}
Expand Down
44 changes: 0 additions & 44 deletions src/core/ai/AIConstructionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
PlayerType,
Unit,
UnitType,
UpgradeType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import {
Expand All @@ -20,7 +19,6 @@
import { PseudoRandom } from "../PseudoRandom";
import { tradeIncomeModifiers } from "../tech/TechEffects";
import { AIBehaviorParams } from "./AIBehaviorParams";
import { AINukeEvaluator } from "./AINukeEvaluator";

/**
* Handles structure construction for AI players.
Expand Down Expand Up @@ -143,15 +141,11 @@
/** Optional callback that returns the current naval unit score (max of warship, submarine). */
private _navalScoreProvider: (() => number) | null = null;

/** Internal multiplier applied to nuke scores in shouldDeferToNukes. */
private static readonly NUKE_SCORE_CONSTRUCTION_INTERNAL_MULTIPLIER = 1;

constructor(
private mg: Game,
private playerId: PlayerID,
private random: PseudoRandom,
private params: AIBehaviorParams,
private nukeEvaluator: AINukeEvaluator | null = null,
) {
// Stagger periodic actions across AIs using random offset
this.phaseSeed = random.nextInt(0, 0x7fffffff);
Expand Down Expand Up @@ -290,11 +284,6 @@
return;
}

// If nuke score threshold is set, skip construction when nuke value is higher
if (this.shouldDeferToNukes(player)) {
return;
}

// Only attempt placement if we can afford the target structure
if (!this.canAffordTarget(player, this.target)) {
return;
Expand Down Expand Up @@ -1447,7 +1436,7 @@
// Feature 5: SAM coverage bonus (flat +0.01 if within range of an existing or under-construction SAM)
{
const samData =
precomputedSAMs !== undefined

Check warning on line 1439 in src/core/ai/AIConstructionHandler.ts

View workflow job for this annotation

GitHub Actions / 🔍 ESLint

Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read. (@typescript-eslint/prefer-nullish-coalescing)
? precomputedSAMs
: this.buildSAMData(player);
for (const sam of samData.units) {
Expand Down Expand Up @@ -1632,7 +1621,7 @@
// Feature 6: SAM coverage bonus (flat +0.01 if within range of an existing or under-construction SAM)
{
const samData =
precomputedSAMs !== undefined

Check warning on line 1624 in src/core/ai/AIConstructionHandler.ts

View workflow job for this annotation

GitHub Actions / 🔍 ESLint

Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read. (@typescript-eslint/prefer-nullish-coalescing)
? precomputedSAMs
: this.buildSAMData(player);
for (const sam of samData.units) {
Expand Down Expand Up @@ -1854,7 +1843,7 @@

// Use precomputed SAM data or build it
const samData =
precomputedSAMs !== undefined

Check warning on line 1846 in src/core/ai/AIConstructionHandler.ts

View workflow job for this annotation

GitHub Actions / 🔍 ESLint

Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read. (@typescript-eslint/prefer-nullish-coalescing)
? precomputedSAMs
: this.buildSAMData(player);
const sams = samData.units;
Expand Down Expand Up @@ -2255,39 +2244,6 @@
}
}

/**
* Returns true if construction should be deferred because nuke value
* exceeds the construction target score (scaled by threshold param).
* Only considers hydrogen bomb score if the player has ThermonuclearStaging.
*/
private shouldDeferToNukes(player: Player): boolean {
const threshold = this.params.nukeScoreConstructionThreshold ?? 0;
if (threshold <= 0 || !this.nukeEvaluator || this.target === null)
return false;

// Get the best nuke scores
const atomTarget = this.nukeEvaluator.bestAtomTarget();
let bestNukeScore = atomTarget?.score ?? 0;

// Only consider hydrogen bomb if player has researched ThermonuclearStaging
if (player.hasUpgrade(UpgradeType.ThermonuclearStaging)) {
const hydrogenTarget = this.nukeEvaluator.bestHydrogenTarget();
if (hydrogenTarget && hydrogenTarget.score > bestNukeScore) {
bestNukeScore = hydrogenTarget.score;
}
}

if (bestNukeScore <= 0) return false;

// Apply internal multiplier
bestNukeScore *=
AIConstructionHandler.NUKE_SCORE_CONSTRUCTION_INTERNAL_MULTIPLIER;

const constructionScore = this.scoreTarget(player, this.target);

return constructionScore < threshold * bestNukeScore;
}

/**
* Returns the best construction score across all candidate structure types.
*/
Expand Down
Loading
Loading