Spaces:
Paused
Paused
File size: 8,942 Bytes
7e764b0 be1f1f9 f8f75e7 bc8dc44 be1f1f9 42d791d ec3d9d8 42d791d be1f1f9 f8f75e7 be1f1f9 f8f75e7 be1f1f9 ec3d9d8 42d791d be1f1f9 916c3ad 7e764b0 916c3ad be1f1f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | // @ts-check
/* ============================================================
AI STRATEGY β a player-picked overlay, orthogonal to the archetype
(engine/aiArchetypes.js). The archetype comes from the world/opponent and
sets FLAVOR: faction, unit mix, which doctrine it researches. Strategy sets
AGGRESSION: whether and when the AI voluntarily attacks, how big a standing
army it bothers to keep, and how it reacts to being attacked. Picked once at
setup (setup.js "AI Strategy" row) alongside difficulty, and applies in BOTH
skirmish and Odyssey β see engine/ai.js (aiContext.strategy), engine/
aiMilitary.js (offense timing), engine/aiEconomy.js (standing-army throttle),
engine/aiIndustry.js (Helium Bomb) and engine/diplomacy.js (Odyssey grace).
"default" carries no overrides at all, so a match that never picks a
strategy β every save/test predating this feature, and the legacy call
sites that still don't pass one β plays exactly as before: every field
below is read with a `|| 1` (multipliers) or falsy-default (flags), so an
absent strategy or an absent field is always a no-op.
Multipliers COMPOSE with the archetype's own numbers rather than replacing
them, the same way the archetype's own Odyssey overlay composes with its
skirmish base (aiArchetypes.js) β so "an Aggressive Rusher" is even more
aggressive than either alone, and "an Economic Economist" out-turtles a
default one, instead of every archetype flattening into one strategy's
numbers.
============================================================ */
"use strict";
export const STRATEGIES = {
default: {
name: "Adaptive",
desc: "Plays true to its archetype β no extra aggression or restraint.",
},
aggressive: {
name: "Aggressive",
desc: "Attacks earlier and with less, and in Odyssey forces the issue instead of waiting out diplomacy.",
// OFFENSE (both modes): commits sooner (attackTimeoutMult), with a smaller
// muster (armyAttackSizeMult), and holds less back at home (garrisonMult) β
// see engine/aiMilitary.js aiOffense, which multiplies these onto the
// archetype's own attackTimeout/armyAttackSize/garrison.
attackTimeoutMult: 0.55,
armyAttackSizeMult: 0.6,
garrisonMult: 0.4,
// ODYSSEY DIPLOMACY: composes with the archetype's own odyssey overlay
// (aiArchetypes.js graceMult/grievanceMult) β see engine/diplomacy.js.
// Shrinks the opening grace window hard and sours faster on losses, so an
// Aggressive neighbour turns hostile β and presses first β well before a
// default one would, on ANY world, not just a Rusher's.
graceMult: 0.2,
grievanceMult: 1.6,
// β¦and it lets go slowly: a grudge outlasts the fight that caused it (diplomacy.js forgiveness).
forgiveness: 0.6,
// ECONOMY TO PAY FOR THE AGGRESSION (2026-08-04). Aggressive was the WORST of the four in
// head-to-head self-play β `ailab duel --candidates` over the shipped roster, 6 worlds x 2
// seeds x both owner slots: 25W-47L (35%) at Medium, last place, against Economic's 51W-21L.
// A coordinate scan over its own offense dials could not fix that (armyAttackSizeMult and
// garrisonMult moved 2W-6L to at best 3W-5L) β because attacking sooner with less was never
// the problem. What the tournament winner had that this strategy lacked was an economy to
// FOLLOW UP with: Economic's own workerTargetMult 1.25. Given the same one, Aggressive goes
// 25W-47L -> 38W-34L (53%) at Medium and 37W-35L -> 39W-33L (54%) at Hard.
// The value is 1.25 and not higher for a reason worth keeping: at 1.4 this wins Medium
// harder (54%) but LOSES Hard (51% -> 44%), because engine/aiDifficulty.js's Hard row already
// carries workerTargetMult 1.25 and the layers compose multiplicatively β 1.4 there is really
// 1.75x. A dial tuned on one difficulty is not tuned.
workerTargetMult: 1.25,
// SUPERWEAPON (engine/aiSuperweapon.js): once a Helium Bomb is built, walk
// it to the current attack target and trigger it there, rather than
// leaving it as a purely defensive home trap (every strategy gets that
// much for free β see aiSuperweapon.js).
useBombOffensively: true,
},
economic: {
name: "Economic",
desc: "Keeps a minimal standing defense while it builds its economy β but rearms fast the instant it's attacked.",
// STANDING ARMY (engine/aiEconomy.js aiProduceAndFortify): stop feeding the
// unit-mix cycle once the home-army headcount reaches this β a token guard,
// not a real force β freeing that ore for economy/industry instead.
standingArmyCap: 3,
// WAR FOOTING: the instant the AI can SEE enemy combat units pressing its
// base (the same signal aiMilitary's recall already reacts to), the cap
// above is lifted by this multiplier for warFootingTime seconds β so being
// attacked is what turns "minimal defense" into a real production push,
// exactly the brief. Decays back to the minimal cap once the threat's been
// gone this long.
warFootingMult: 5,
warFootingTime: 75,
// OFFENSE: never volunteers into a fight on its own β neverInitiates
// (engine/aiMilitary.js aiOffense) blocks every voluntary commit, including
// the skirmish desperation timeout: a strategy whose whole point is "doesn't
// attack unprovoked" has to mean that literally, or a genuinely passive
// player eventually eats an unexplained all-in wave purely because enough
// time passed. A mutual-turtle skirmish still resolves β victory.js's
// score-based DEFAULT_MATCH_TIME_LIMIT (40 minutes) settles it without
// requiring combat from either side.
// ODYSSEY reads it as "doesn't START fights" rather than "never fights":
// there's no clock and no score there, so a neighbour dragged all the way to
// Hostile that could never answer just read as decoration. It commits only
// once the PLAYER has provoked it β destroyed its ships, or started charging
// a Gate (engine/diplomacy.js provoked()) β never on elapsed time, which is
// the bug above. Unprovoked, it still only ever fights defensively.
neverInitiates: true,
// A lighter static defense to match "minimal" (still real β cheap and
// passive, unlike the standing army above).
turretCountMult: 0.6,
// β¦and it de-escalates readily: fighting is a cost centre, so it cools off fastest of the four
// once you stop shooting (engine/diplomacy.js forgiveness).
forgiveness: 1.4,
// Leans further into economy than its archetype might on its own.
workerTargetMult: 1.25,
// INDUSTRY (engine/aiIndustry.js): always climbs the deep factory chain β
// Plasma Rig included β once teched for it, even paired with a Rusher/
// Balanced archetype that wouldn't normally bother (archetype.wantsRefinery
// false). Pure economy means using every economic capability in the game.
wantsIndustryAlways: true,
},
matching: {
name: "Force Parity",
desc: "Sizes its army to mirror whatever it's seen of the enemy's, so no attack catches it outmatched.",
// STANDING ARMY: instead of a fixed cap, the target continuously tracks
// the enemy combat units the AI can actually SEE right now (its own fog β
// engine/aiMilitary.js visibleEnemyForceCount), with a buffer above parity
// and a floor so a yet-unscouted enemy still gets a minimal home guard.
matchEnemyForce: true,
matchBuffer: 1.15,
matchFloor: 3,
// Deterrence, not vengeance: it stands down a little faster than a default neighbour once the
// force it was mirroring stops appearing (engine/diplomacy.js forgiveness).
forgiveness: 1.2,
// OFFENSE: purely reactive, like Economic β parity is a deterrent/defense
// plan, not a wind-up to attack, and neverInitiates means that literally,
// with no timeout escape hatch (see the Economic entry above, including how
// Odyssey's provocation path narrows it to "doesn't start fights").
neverInitiates: true,
},
};
/** The active strategy for `owner` (default "ai", so every existing save/test/call site that
* predates self-play β the overwhelming majority β reads state.ai.strategy exactly as before) β
* STRATEGIES.default if unset/unknown. Tier 1 self-play (tools/selfplay.js) passes owner="player"
* to read state.playerAi.strategy instead, its own independently-picked overlay; deliberately NOT
* an import of engine/aiCommon.js's controllerFor, so this file stays the pure, import-free leaf
* its header describes. */
/** @param {State} state @param {string} [owner] @returns {Strategy} */
export function strategyFor(state, owner = "ai") {
const controller = owner === "ai" ? state.ai : state.playerAi;
return (controller && STRATEGIES[controller.strategy]) || STRATEGIES.default;
}
|