TA AI Updates
This commit is contained in:
parent
73de46e0f1
commit
625ced5649
|
|
@ -26,6 +26,12 @@ const COMPOSITION = {
|
||||||
// enemy owning one (applyCounters pushes it hard the moment enemy air is actually seen),
|
// enemy owning one (applyCounters pushes it hard the moment enemy air is actually seen),
|
||||||
// and the hover constructor is the same insurance policy as the tracked one.
|
// and the hover constructor is the same insurance policy as the tracked one.
|
||||||
bomber: 0.44, fighter: 0.30, hoverconstructor: 0.10,
|
bomber: 0.44, fighter: 0.30, hoverconstructor: 0.10,
|
||||||
|
// Advanced Vehicle Plant roster — these only compete with each other once one is standing.
|
||||||
|
// Megatank is the AVP's answer to the Tank, so it gets a real share; Rocket Artillery is a
|
||||||
|
// specialist siege piece that commits hard to a single target and is fragile if caught
|
||||||
|
// without escort, so some but not a mainstay; Advanced Construction Vehicle is a
|
||||||
|
// replacement/backup builder like `constructor`, same low share.
|
||||||
|
megatank: 0.22, rocketartillery: 0.08, advancedconstructor: 0.04,
|
||||||
};
|
};
|
||||||
|
|
||||||
function memFor(state, armyIdx) {
|
function memFor(state, armyIdx) {
|
||||||
|
|
@ -181,24 +187,38 @@ function gatherOwn(ctx) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function manageEconomyAndBase(ctx, mine) {
|
function manageEconomyAndBase(ctx, mine) {
|
||||||
const { state, army, rules, mem, expansion } = ctx;
|
const { rules, expansion } = ctx;
|
||||||
const builder = mine.builders[0];
|
if (!mine.builders.length) return;
|
||||||
if (!builder) return;
|
|
||||||
|
|
||||||
// Build orders QUEUE on the builder rather than replacing its current job, so the
|
// Build orders QUEUE on a builder rather than replacing its current job, so it rolls
|
||||||
// Commander rolls straight from one structure into the next with no idle gap. Issuing
|
// straight from one structure into the next with no idle gap. Issuing these unqueued caps
|
||||||
// these unqueued caps every skill at one building in flight, which flattens the whole
|
// every skill at one building in flight, which flattens the whole skill ladder — a skill-5
|
||||||
// skill ladder — a skill-5 economy then looks identical to a skill-1 one.
|
// economy then looks identical to a skill-1 one.
|
||||||
const maxQueued = Math.max(1, Math.round(1 + expansion * 3));
|
const maxQueued = Math.max(1, Math.round(1 + expansion * 3));
|
||||||
const queued = builder.orders.reduce((n, o) => n + (o.type === 'build' ? 1 : 0), 0);
|
const queuedOn = (b) => b.orders.reduce((n, o) => n + (o.type === 'build' ? 1 : 0), 0);
|
||||||
if (queued >= maxQueued) return;
|
const available = mine.builders.filter((b) => queuedOn(b) < maxQueued);
|
||||||
|
if (!available.length) return;
|
||||||
|
|
||||||
const want = chooseBuilding(ctx, mine);
|
// The Commander cannot build an Advanced Vehicle Plant, and NEITHER the Commander nor a
|
||||||
|
// plain Constructor can build an Advanced Metal Generator or Nuclear Power Plant — only a
|
||||||
|
// Constructor/Hover Constructor and an Advanced Construction Vehicle respectively can. What
|
||||||
|
// the whole army wants next (`chooseBuilding`) has to be something SOME available builder can
|
||||||
|
// actually construct, or the AI just spins forever wanting a building nothing can start —
|
||||||
|
// this was silently happening (Advanced Vehicle Plant, Nuclear Power Plant) until caught by
|
||||||
|
// a build-repertoire soak showing zero of either across six full games.
|
||||||
|
const buildable = new Set();
|
||||||
|
for (const b of available) for (const id of rules.defById[b.defId].builds ?? []) buildable.add(id);
|
||||||
|
|
||||||
|
const want = chooseBuilding(ctx, mine, buildable);
|
||||||
if (!want) return;
|
if (!want) return;
|
||||||
|
|
||||||
|
const builder = available.find((b) => (rules.defById[b.defId].builds ?? []).includes(want.id));
|
||||||
|
if (!builder) return;
|
||||||
|
|
||||||
const spot = findPlacement(ctx, mine, want, builder, placementOptionsFor(ctx, want));
|
const spot = findPlacement(ctx, mine, want, builder, placementOptionsFor(ctx, want));
|
||||||
if (!spot) return;
|
if (!spot) return;
|
||||||
|
|
||||||
|
const queued = queuedOn(builder);
|
||||||
order(ctx, {
|
order(ctx, {
|
||||||
army: ctx.armyIdx, unitIds: [builder.id],
|
army: ctx.armyIdx, unitIds: [builder.id],
|
||||||
order: { type: 'build', defId: want.id, tx: spot.tx, ty: spot.ty },
|
order: { type: 'build', defId: want.id, tx: spot.tx, ty: spot.ty },
|
||||||
|
|
@ -206,7 +226,7 @@ function manageEconomyAndBase(ctx, mine) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseBuilding(ctx, mine) {
|
function chooseBuilding(ctx, mine, buildable) {
|
||||||
const { rules, army, mem, expansion, skill } = ctx;
|
const { rules, army, mem, expansion, skill } = ctx;
|
||||||
const n = (id) => (mine.byDef[id] ?? 0);
|
const n = (id) => (mine.byDef[id] ?? 0);
|
||||||
const eGen = rules.buildingById.energygen;
|
const eGen = rules.buildingById.energygen;
|
||||||
|
|
@ -214,6 +234,9 @@ function chooseBuilding(ctx, mine) {
|
||||||
const barracks = rules.buildingById.barracks;
|
const barracks = rules.buildingById.barracks;
|
||||||
const plant = rules.buildingById.vehicleplant;
|
const plant = rules.buildingById.vehicleplant;
|
||||||
const airfield = rules.buildingById.airfield;
|
const airfield = rules.buildingById.airfield;
|
||||||
|
const avp = rules.buildingById.advancedvehicleplant;
|
||||||
|
const advMGen = rules.buildingById.advancedmassgen;
|
||||||
|
const nuclear = rules.buildingById.nuclearplant;
|
||||||
|
|
||||||
// Opening: two power, one mass, then a barracks so there's something to fight with.
|
// Opening: two power, one mass, then a barracks so there's something to fight with.
|
||||||
if (n('energygen') < 2) return eGen;
|
if (n('energygen') < 2) return eGen;
|
||||||
|
|
@ -262,6 +285,23 @@ function chooseBuilding(ctx, mine) {
|
||||||
return airfield;
|
return airfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exactly one Advanced Vehicle Plant, same tier and reasoning as the Airfield above — it is
|
||||||
|
// what unlocks the Megatank, the Advanced Construction Vehicle and the Rocket Artillery,
|
||||||
|
// none of which this AI can ever field without one. Deliberately the SAME numeric threshold
|
||||||
|
// as the Airfield's own rather than a stricter one — an earlier attempt required
|
||||||
|
// energygen>=5 and buildEff>0.55 and it never fired in six full games, because buildEff sags
|
||||||
|
// exactly as generator counts climb (the economy is feeding a bigger base, not sitting
|
||||||
|
// idle), so a stricter combined bar than the Airfield's own already-hard-won one is rarely
|
||||||
|
// met at all. Even matching it exactly wasn't enough on its own, though: since the Airfield
|
||||||
|
// check runs first and shares the identical bar, it claims the first qualifying tick and
|
||||||
|
// buildEff had usually sagged back under 0.5 by the time this check got its own turn — so an
|
||||||
|
// Airfield already standing (proof the economy cleared this bar once already) unlocks the
|
||||||
|
// AVP immediately, instead of waiting on a second independent crossing of the same window.
|
||||||
|
if (avp && buildable.has(avp.id) && n('advancedvehicleplant') < 1 && n('vehicleplant') >= 1
|
||||||
|
&& (n('airfield') >= 1 || (n('energygen') >= 4 && n('massgen') >= 3 && army.buildEff > 0.5))) {
|
||||||
|
return avp;
|
||||||
|
}
|
||||||
|
|
||||||
// Steady state: chase the energy:mass income RATIO that the unit roster actually costs.
|
// Steady state: chase the energy:mass income RATIO that the unit roster actually costs.
|
||||||
// A Tank is 700 energy to 180 mass, so roughly 4:1 — an AI that treats the two resources
|
// A Tank is 700 energy to 180 mass, so roughly 4:1 — an AI that treats the two resources
|
||||||
// symmetrically ends up capping out on energy while its factories starve for mass.
|
// symmetrically ends up capping out on energy while its factories starve for mass.
|
||||||
|
|
@ -269,16 +309,30 @@ function chooseBuilding(ctx, mine) {
|
||||||
const ratio = netE / Math.max(0.5, army.mIncome);
|
const ratio = netE / Math.max(0.5, army.mIncome);
|
||||||
const TARGET_RATIO = 4.0;
|
const TARGET_RATIO = 4.0;
|
||||||
|
|
||||||
|
// Counts BOTH tiers against the same cap — an upgraded generator still occupies one of the
|
||||||
|
// "how many of these do we want" slots, so the cap governs total generating structures
|
||||||
|
// rather than letting the AI stack a full cap's worth of each tier back to back.
|
||||||
const cap = Math.round(3 + expansion * 7);
|
const cap = Math.round(3 + expansion * 7);
|
||||||
const roomE = n('energygen') < cap;
|
const roomE = n('energygen') + n('nuclearplant') < cap;
|
||||||
// A Mass Generator is an energy CONSUMER. Building one without the headroom to power it
|
// A Mass Generator is an energy CONSUMER. Building one without the headroom to power it
|
||||||
// just throttles the ones already standing, so require spare energy before adding another.
|
// just throttles the ones already standing, so require spare energy before adding another.
|
||||||
const upkeepPer = mGen.upkeep?.energy ?? 0;
|
const upkeepPer = mGen.upkeep?.energy ?? 0;
|
||||||
const roomM = n('massgen') < cap && netE > upkeepPer * 1.6;
|
const roomM = n('massgen') + n('advancedmassgen') < cap && netE > upkeepPer * 1.6;
|
||||||
|
|
||||||
|
// A genuine stall wants the fast, cheap fix — a plain generator finishes sooner than its
|
||||||
|
// pricier upgraded form, which matters most exactly when the economy can least afford to
|
||||||
|
// wait, so this stays the base-tier building, not `upgradeOr`.
|
||||||
if (army.stallM < 0.98 && roomM) return mGen;
|
if (army.stallM < 0.98 && roomM) return mGen;
|
||||||
if (army.stallE < 0.98 && roomE) return eGen;
|
if (army.stallE < 0.98 && roomE) return eGen;
|
||||||
|
|
||||||
|
// Trade an existing base-tier generator for its advanced form once there's genuine spare
|
||||||
|
// energy, independent of `roomM`/`roomE` above — those only gate ADDING a new generator, but
|
||||||
|
// replacing one is worth it purely on energy headroom, room-cap or not. Without this an
|
||||||
|
// economy that reaches its generator cap simply stops growing mass/energy income at all
|
||||||
|
// instead of upgrading what it already has, which is the more common case for a mature base.
|
||||||
|
if (advMGen && buildable.has(advMGen.id) && wantsUpgrade(ctx, mine, mGen, advMGen)) return advMGen;
|
||||||
|
if (nuclear && buildable.has(nuclear.id) && wantsUpgrade(ctx, mine, eGen, nuclear)) return nuclear;
|
||||||
|
|
||||||
// Production capacity is grown ALONGSIDE the economy, sized to what the income can
|
// Production capacity is grown ALONGSIDE the economy, sized to what the income can
|
||||||
// actually feed. Saturating generators first (the obvious ordering) is why a big economy
|
// actually feed. Saturating generators first (the obvious ordering) is why a big economy
|
||||||
// used to win nothing: with only two factories the surplus just floated in storage, and an
|
// used to win nothing: with only two factories the surplus just floated in storage, and an
|
||||||
|
|
@ -294,6 +348,9 @@ function chooseBuilding(ctx, mine) {
|
||||||
const defence = steadyDefence(ctx, mine);
|
const defence = steadyDefence(ctx, mine);
|
||||||
if (defence) return defence;
|
if (defence) return defence;
|
||||||
|
|
||||||
|
// Plain generators here too — by this point the dedicated upgrade check above has already
|
||||||
|
// fired if an upgrade was actually wanted, so reaching here means it wasn't; these are purely
|
||||||
|
// about ADDING capacity, which `roomM`/`roomE` already gate.
|
||||||
if (ratio > TARGET_RATIO && roomM) return mGen;
|
if (ratio > TARGET_RATIO && roomM) return mGen;
|
||||||
if (ratio < TARGET_RATIO && roomE) return eGen;
|
if (ratio < TARGET_RATIO && roomE) return eGen;
|
||||||
if (roomM) return mGen;
|
if (roomM) return mGen;
|
||||||
|
|
@ -302,6 +359,23 @@ function chooseBuilding(ctx, mine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once there's a genuine baseline of the base-tier generator AND spare energy to run the
|
||||||
|
* upkeep, trade it for the upgraded form instead of stacking another base-tier one — an
|
||||||
|
* Advanced Metal Generator produces 4x a Mass Generator's mass from the same footprint, and a
|
||||||
|
* Nuclear Power Plant a similar step up over an Energy Generator, so growth past a small base
|
||||||
|
* count is much better spent there. `findPlacement` prefers landing the upgrade directly on
|
||||||
|
* top of an existing base-tier building (reclaiming half its cost) over claiming fresh ground.
|
||||||
|
*/
|
||||||
|
function wantsUpgrade(ctx, mine, base, advanced) {
|
||||||
|
const { army } = ctx;
|
||||||
|
const n = (id) => (mine.byDef[id] ?? 0);
|
||||||
|
if (!advanced || n(base.id) < 2) return false;
|
||||||
|
const netE = Math.max(0, army.eIncome - army.eUpkeep);
|
||||||
|
const upkeepAdv = advanced.upkeep?.energy ?? 0;
|
||||||
|
return netE > upkeepAdv * 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static defence is a skill-3-and-up behaviour, like the Airfield.
|
* Static defence is a skill-3-and-up behaviour, like the Airfield.
|
||||||
*
|
*
|
||||||
|
|
@ -404,11 +478,21 @@ function factoryMassDrain(rules, def) {
|
||||||
* a metal patch, which is what makes map control matter without a reclaim economy.
|
* a metal patch, which is what makes map control matter without a reclaim economy.
|
||||||
*/
|
*/
|
||||||
function findPlacement(ctx, mine, def, builder, opts = {}) {
|
function findPlacement(ctx, mine, def, builder, opts = {}) {
|
||||||
const { state, rules } = ctx;
|
const { state, rules, armyIdx } = ctx;
|
||||||
const mem = ctx.mem;
|
const mem = ctx.mem;
|
||||||
const bx = worldToTileX(state.nav, mem.baseSet ? mem.baseX : builder.x);
|
const bx = worldToTileX(state.nav, mem.baseSet ? mem.baseX : builder.x);
|
||||||
const by = worldToTileY(state.nav, mem.baseSet ? mem.baseY : builder.y);
|
const by = worldToTileY(state.nav, mem.baseSet ? mem.baseY : builder.y);
|
||||||
|
|
||||||
|
// An upgrade building (Advanced Metal Generator over a Mass Generator, Nuclear Power Plant
|
||||||
|
// over an Energy Generator) prefers replacing an existing one of its own base type in place
|
||||||
|
// — reclaiming half its cost — over claiming fresh ground, exactly like a human player
|
||||||
|
// queuing the upgrade on top of what's already there.
|
||||||
|
if (def.upgradesFrom) {
|
||||||
|
const target = mine.buildings.find((b) => b.defId === def.upgradesFrom
|
||||||
|
&& canPlaceAt(state, rules, b.tx, b.ty, def, armyIdx).ok);
|
||||||
|
if (target) return { tx: target.tx, ty: target.ty };
|
||||||
|
}
|
||||||
|
|
||||||
if (def.terrainMultiplier) {
|
if (def.terrainMultiplier) {
|
||||||
const spot = findMetalSpot(ctx, def, bx, by);
|
const spot = findMetalSpot(ctx, def, bx, by);
|
||||||
if (spot) return spot;
|
if (spot) return spot;
|
||||||
|
|
@ -432,7 +516,11 @@ function findPlacement(ctx, mine, def, builder, opts = {}) {
|
||||||
const tx = bx + Math.round(Math.cos(a) * r);
|
const tx = bx + Math.round(Math.cos(a) * r);
|
||||||
const ty = by + Math.round(Math.sin(a) * r);
|
const ty = by + Math.round(Math.sin(a) * r);
|
||||||
if (!spacedEnough(ctx, mine, tx, ty, def)) continue;
|
if (!spacedEnough(ctx, mine, tx, ty, def)) continue;
|
||||||
if (canPlaceAt(state, rules, tx, ty, def).ok) return { tx, ty };
|
if (!canPlaceAt(state, rules, tx, ty, def).ok) continue;
|
||||||
|
// Reject a legal spot that would seal the base off from the rest of the map — see
|
||||||
|
// `keepsBaseOpen`. The spiral just keeps going and tries the next candidate.
|
||||||
|
if (!keepsBaseOpen(ctx, tx, ty, def)) continue;
|
||||||
|
return { tx, ty };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -466,12 +554,72 @@ function findMetalSpot(ctx, def, bx, by) {
|
||||||
const d = Math.hypot(tx - bx, ty - by);
|
const d = Math.hypot(tx - bx, ty - by);
|
||||||
if (d > 30 || d >= bestD) continue;
|
if (d > 30 || d >= bestD) continue;
|
||||||
if (!canPlaceAt(state, rules, tx, ty, def).ok) continue;
|
if (!canPlaceAt(state, rules, tx, ty, def).ok) continue;
|
||||||
|
// Checked last, behind the cheap filters above, since it walks the nav grid — a metal
|
||||||
|
// patch tucked in a corridor is exactly the kind of spot that would wall the base in.
|
||||||
|
if (!keepsBaseOpen(ctx, tx, ty, def)) continue;
|
||||||
best = { tx, ty }; bestD = d;
|
best = { tx, ty }; bestD = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How far past a placement candidate counts as genuinely "off base" for `keepsBaseOpen` — a
|
||||||
|
// little past `findPlacement`'s own widest spiral radius (22) and matching `findMetalSpot`'s
|
||||||
|
// own 30-tile metal-patch search radius, so a legitimate metal-patch generator at the edge of
|
||||||
|
// that range doesn't read as "trapped" just for being far from the base centre.
|
||||||
|
const ESCAPE_DIST_TILES = 30;
|
||||||
|
const ESCAPE_BFS_BUDGET = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cheap connectivity guard: would placing `def` at (tx,ty) cut the base off from the rest of
|
||||||
|
* the map? Floods outward from the base centre (4-connected, ignoring move-class/terrain cost
|
||||||
|
* — a deliberately conservative "is there ANY way out" check, not real pathfinding) treating
|
||||||
|
* the candidate footprint as an extra obstruction on top of whatever is already blocked, and
|
||||||
|
* succeeds as soon as it reaches open ground well past the base's own build radius.
|
||||||
|
*
|
||||||
|
* This is not a real min-cut — it's a bounded flood fill that gives up after `ESCAPE_BFS_BUDGET`
|
||||||
|
* tiles — but a base walls itself in one placement at a time, so catching the specific
|
||||||
|
* placement that would finish sealing it off is enough to prevent the trap without paying for
|
||||||
|
* anything more expensive on every building decision.
|
||||||
|
*/
|
||||||
|
function keepsBaseOpen(ctx, tx, ty, def) {
|
||||||
|
const { state, mem } = ctx;
|
||||||
|
if (!mem.baseSet) return true; // nothing established yet to protect
|
||||||
|
const nav = state.nav;
|
||||||
|
const fw = def.footprint.w, fh = def.footprint.h;
|
||||||
|
const blockedNow = (x, y) => {
|
||||||
|
if (x >= tx && x < tx + fw && y >= ty && y < ty + fh) return true;
|
||||||
|
return !!nav.blocked[y * nav.w + x];
|
||||||
|
};
|
||||||
|
|
||||||
|
const bx = worldToTileX(nav, mem.baseX), by = worldToTileY(nav, mem.baseY);
|
||||||
|
if (blockedNow(bx, by)) return true; // base tile itself isn't open ground; not this check's job
|
||||||
|
|
||||||
|
const seen = new Uint8Array(nav.w * nav.h);
|
||||||
|
// Each of up to ESCAPE_BFS_BUDGET dequeues can enqueue up to 4 neighbours, so the queue
|
||||||
|
// needs headroom for the whole frontier, not just the processed count.
|
||||||
|
const qcap = Math.min(nav.w * nav.h, ESCAPE_BFS_BUDGET * 4 + 8);
|
||||||
|
const qx = new Int32Array(qcap);
|
||||||
|
const qy = new Int32Array(qcap);
|
||||||
|
let head = 0, tail = 0;
|
||||||
|
qx[tail] = bx; qy[tail] = by; tail++;
|
||||||
|
seen[by * nav.w + bx] = 1;
|
||||||
|
|
||||||
|
while (head < tail && head < ESCAPE_BFS_BUDGET) {
|
||||||
|
const x = qx[head], y = qy[head]; head++;
|
||||||
|
if (Math.hypot(x - bx, y - by) > ESCAPE_DIST_TILES) return true;
|
||||||
|
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||||
|
const nx = x + dx, ny = y + dy;
|
||||||
|
if (nx < 0 || ny < 0 || nx >= nav.w || ny >= nav.h) continue;
|
||||||
|
const ni = ny * nav.w + nx;
|
||||||
|
if (seen[ni] || blockedNow(nx, ny)) continue;
|
||||||
|
seen[ni] = 1;
|
||||||
|
if (tail < qx.length) { qx[tail] = nx; qy[tail] = ny; tail++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // exhausted the budget without escaping — this placement would trap the base
|
||||||
|
}
|
||||||
|
|
||||||
function spacedEnough(ctx, mine, tx, ty, def) {
|
function spacedEnough(ctx, mine, tx, ty, def) {
|
||||||
const gap = 1;
|
const gap = 1;
|
||||||
for (const b of [...mine.buildings, ...mine.sites]) {
|
for (const b of [...mine.buildings, ...mine.sites]) {
|
||||||
|
|
@ -514,11 +662,30 @@ function randomPick(ctx, builds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pick whichever buildable unit is furthest below its share of the desired mix. */
|
/** Pick whichever buildable unit is furthest below its share of the desired mix. */
|
||||||
|
// Nothing else in the roster can build an Advanced Vehicle Plant or upgrade a generator
|
||||||
|
// except these two, so if either is ever going to happen at all, at least one of these has to
|
||||||
|
// actually get produced — not just weighted like an ordinary combat unit. Their COMPOSITION
|
||||||
|
// weight is deliberately kept low (they're a poor use of resources beyond that one job), but
|
||||||
|
// under the plain gap-based pick below a low, STATIC weight can get starved out indefinitely:
|
||||||
|
// combat losses keep refreshing everything else's gap back above it, while a unit that's
|
||||||
|
// simply never been built holds the same small gap forever and rarely wins the comparison.
|
||||||
|
// Measured directly: across 6 full skirmishes, Constructor was built zero times, which meant
|
||||||
|
// the Advanced Vehicle Plant — and everything behind it — was unreachable regardless of how
|
||||||
|
// well the economy was doing.
|
||||||
|
const BUILDER_UNITS = new Set(['constructor', 'advancedconstructor']);
|
||||||
|
|
||||||
function templatePick(ctx, mine, builds) {
|
function templatePick(ctx, mine, builds) {
|
||||||
const { rules, skill, profile } = ctx;
|
const { rules, skill, profile } = ctx;
|
||||||
const weights = { ...COMPOSITION, ...(profile.composition ?? {}) };
|
const weights = { ...COMPOSITION, ...(profile.composition ?? {}) };
|
||||||
if (skill >= 4) applyCounters(ctx, weights);
|
if (skill >= 4) applyCounters(ctx, weights);
|
||||||
|
|
||||||
|
// Also checks every factory's pending queue, not just finished/under-construction entities
|
||||||
|
// — otherwise this could re-fire on the same factory's next queue slot before the first one
|
||||||
|
// it ordered ever shows up as a real entity, stacking up redundant Constructors.
|
||||||
|
const missingBuilder = builds.find((id) => BUILDER_UNITS.has(id) && !(mine.byDef[id] ?? 0)
|
||||||
|
&& !mine.factories.some((f) => f.queue.some((q) => q.defId === id)));
|
||||||
|
if (missingBuilder) return missingBuilder;
|
||||||
|
|
||||||
const total = builds.reduce((s, id) => s + (mine.byDef[id] ?? 0), 0) + 1;
|
const total = builds.reduce((s, id) => s + (mine.byDef[id] ?? 0), 0) + 1;
|
||||||
let best = null, bestGap = -Infinity;
|
let best = null, bestGap = -Infinity;
|
||||||
for (const id of builds) {
|
for (const id of builds) {
|
||||||
|
|
@ -552,6 +719,10 @@ function applyCounters(ctx, weights) {
|
||||||
weights.rockettank *= 1 + (structures > 2 ? 0.5 : 0);
|
weights.rockettank *= 1 + (structures > 2 ? 0.5 : 0);
|
||||||
weights.sniper *= 1 + (1 - armourShare) * 0.5;
|
weights.sniper *= 1 + (1 - armourShare) * 0.5;
|
||||||
weights.rockettrooper *= 1 + armourShare * 0.6;
|
weights.rockettrooper *= 1 + armourShare * 0.6;
|
||||||
|
// Megatank is the heavy-armour answer the same way Tank is; Rocket Artillery is the
|
||||||
|
// anti-fortification specialist the same way Rocket Tank leans into a base of structures.
|
||||||
|
weights.megatank *= 1 + (1 - armourShare) * 0.5 + armourShare * 0.6;
|
||||||
|
weights.rocketartillery *= 1 + (structures > 2 ? 0.6 : 0);
|
||||||
}
|
}
|
||||||
// Enemy aircraft are answered by fighters first and by the guided-missile units second —
|
// Enemy aircraft are answered by fighters first and by the guided-missile units second —
|
||||||
// those are the only ground things that can shoot up at all, so the counter is far sharper
|
// those are the only ground things that can shoot up at all, so the counter is far sharper
|
||||||
|
|
|
||||||
|
|
@ -2829,10 +2829,21 @@ section('12b. AI build repertoire');
|
||||||
const n = (id) => (built[id] ?? 0);
|
const n = (id) => (built[id] ?? 0);
|
||||||
console.log(` ${games} games · towers ${n('lasertower')} · launchers ${n('missilelauncher')}`
|
console.log(` ${games} games · towers ${n('lasertower')} · launchers ${n('missilelauncher')}`
|
||||||
+ ` · airfields ${n('airfield')} · fighters ${n('fighter')} · bombers ${n('bomber')}`);
|
+ ` · airfields ${n('airfield')} · fighters ${n('fighter')} · bombers ${n('bomber')}`);
|
||||||
|
console.log(` advanced vehicle plants ${n('advancedvehicleplant')} · megatanks ${n('megatank')}`
|
||||||
|
+ ` · rocket artillery ${n('rocketartillery')} · advanced metal gens ${n('advancedmassgen')}`
|
||||||
|
+ ` · nuclear plants ${n('nuclearplant')}`);
|
||||||
check('the AI builds Laser Towers', n('lasertower') > 0, 'never built one');
|
check('the AI builds Laser Towers', n('lasertower') > 0, 'never built one');
|
||||||
check('the AI builds Missile Launchers', n('missilelauncher') > 0, 'never built one');
|
check('the AI builds Missile Launchers', n('missilelauncher') > 0, 'never built one');
|
||||||
check('the AI builds Airfields', n('airfield') > 0, 'never built one');
|
check('the AI builds Airfields', n('airfield') > 0, 'never built one');
|
||||||
check('the AI produces aircraft', n('fighter') + n('bomber') > 0, 'never built any');
|
check('the AI produces aircraft', n('fighter') + n('bomber') > 0, 'never built any');
|
||||||
|
// Same "dead data unless the AI actually builds it" bar, for the Advanced Vehicle Plant tier
|
||||||
|
// and the generator upgrades — added alongside the Rocket Artillery, since a unit or
|
||||||
|
// building the AI never reaches is exactly as untested as one that doesn't exist.
|
||||||
|
check('the AI builds Advanced Vehicle Plants', n('advancedvehicleplant') > 0, 'never built one');
|
||||||
|
check('the AI produces Advanced Vehicle Plant units',
|
||||||
|
n('megatank') + n('rocketartillery') + n('advancedconstructor') > 0, 'never built any');
|
||||||
|
check('the AI upgrades generators',
|
||||||
|
n('advancedmassgen') + n('nuclearplant') > 0, 'never upgraded one');
|
||||||
// Defence is a reaction, not a habit. An AI that answers every game with a wall of towers
|
// Defence is a reaction, not a habit. An AI that answers every game with a wall of towers
|
||||||
// has stopped building an army, and the skill ladder above is measuring the wrong thing.
|
// has stopped building an army, and the skill ladder above is measuring the wrong thing.
|
||||||
check('defence stays a minority of construction',
|
check('defence stays a minority of construction',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue