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;
}