File size: 8,222 Bytes
7e764b0
53ce1fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b28c2f0
 
 
 
 
 
 
 
 
 
 
 
 
 
9bb52d4
4b95948
 
 
 
 
 
b02b34f
 
 
 
 
 
 
8101484
 
 
 
 
 
 
2bf7ef0
 
 
 
 
 
 
 
5e33d82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53ce1fb
 
 
 
 
2bf7ef0
f8f75e7
5d60e36
b02b34f
b28c2f0
b02b34f
5d60e36
53ce1fb
 
5d60e36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916c3ad
 
 
 
 
 
 
7e764b0
916c3ad
 
 
53ce1fb
 
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
// @ts-check
/* ============================================================
   AI DIFFICULTY β€” the Easy/Medium/Hard pick from the splash screen, orthogonal to
   both the archetype (engine/aiArchetypes.js β€” which planet/temperament) and the
   player-picked strategy (engine/aiStrategy.js β€” whether/when it voluntarily
   attacks). Bundles the two original dials β€” how FAST the opponent acts (aiApm)
   and whether it MICROS its army (aiMicro) β€” plus the picker's own label/note.
   This is the ONE list of valid difficulty keys: it drives the Easy/Medium/Hard
   buttons in setup.js AND (via boot.js's difficultyDials, which looks a key up in
   this same array) the AI dials a match actually runs with β€” so a key can never
   exist in one place and not the other, which used to let a mismatched difficulty
   silently downgrade to Medium instead of erroring.

   Economic dials land here as more fields on an entry, read through
   difficultyFor(state) the same defensive way engine/aiStrategy.js's
   strategyFor(state) already reads STRATEGIES β€” a multiplier field with `|| 1`
   at its use site, a flag read falsy β€” so an absent field or a legacy/unknown key
   composes as a no-op. Medium carries none of them at all (byte-identical to
   unset), same as STRATEGIES.default β€” it's the baseline every dial is relative to.

   Tier 1 dials (worker target, war patience, economic edge) compose
   MULTIPLICATIVELY on top of whatever the archetype and player-picked strategy
   already contribute β€” the same relationship Aggressive/Economic/Force Parity
   already have with the archetype's own Odyssey overlay (aiArchetypes.js) β€” so
   this is one more layer, not a replacement:
     β€’ workerTargetMult   β€” aiEconomy.js's workerTarget formula.
     β€’ graceMult/grievanceMult β€” diplomacy.js's updateDiplomacy war-onset composition.
     β€’ economicEdge       β€” seeds the synthetic `hardEdge` UPGRADES entry
       (entities.js) onto the AI's own upgrades at creation (engine/state.js), which
       gather.js/production.js already pick up via the existing generic upgradeMult
       β€” no plumbing changes needed there at all.

   Tier 2 adds researchPaceMult β€” techtree.js's updateResearch applies it to the AI's OWN
   research only, never the player's, even on the same tech-rated world. That now reaches
   BOTH node tables it resolves: the Odyssey Datacenter tech tree AND the Refinery's
   doctrine upgrades (production.js's researchUpgrade queues and times them the same way,
   engine/entities.js UPGRADES) β€” so Hard's AI also develops its doctrine pick faster, and
   Easy's slower, than Medium's.

   Tier 3 adds marketAccess β€” unlocks market.js's aiBarter (called from
   aiEconomy.js's aiMarketBarter, gated through the AI's own APM budget like every
   other decision). Medium and Hard both get it; Easy doesn't, so a new player
   isn't shown an AI that "trades" in a way they can't yet see or counter. No
   separate throttle needed: Hard's higher APM already means more frequent
   barters for free, the same way it already means more frequent everything else.

   Tier 4 adds rusherGraduates (Hard only β€” the one identity-level change, not just
   a number): past aiIndustry.js's RUSHER_GRADUATE_TIME into an Odyssey world, a
   non-developing archetype (today, only a Rusher) picks up the deep factory chain
   a patient developer would, the same third condition strategy.wantsIndustryAlways
   already is on that gate. Skirmish is untouched by construction (aiIndustry.js
   returns on !state.endless before this is ever read).

   Tier 5 adds counterEvery β€” aiMilitary.js's pickNextUnitType reads it directly
   (not a multiplier: a cadence override, `?? COUNTER_EVERY` at the read site, so
   an absent value composes as today's fixed-3 cadence). Easy is 0 β€” it never
   counter-picks, so its army stays exactly its learnable, exploitable archetype
   mix; Hard is 2 β€” it reacts to the player's composition half again as fast as
   Medium's unchanged 3. Same "Medium carries none of it" baseline as every other
   tier.

   Tier 6 adds strategicCeiling (Easy only β€” the second identity-level dial,
   exactly the precedent Tier 4's header sets for rusherGraduates). aiIndustry.js's
   INDUSTRY_CHAIN climb and its RESEARCH_ORDER research both stop short of the
   Strategic tier (antimatterforge / 'antimatter' Containment onward) when this is
   set β€” which transitively withholds the Star Dock, the Leviathan, the Helium
   Bomb, the Plasma Rig, and (Phase 7) a rival Antimatter Gate, since every one of
   those sits downstream of that same chain. This is the identical argument this
   file's own Tier 3 already makes for withholding marketAccess from Easy ("a new
   player isn't shown an AI that trades in a way they can't yet see or counter")
   applied harder to the Strategic tier: a long enough Odyssey session would
   otherwise let even a placid Easy Economist climb to a 900-hp Leviathan and a
   doomsday bomb, exactly the escalation Easy exists to spare a new player from.
   Medium and Hard are untouched β€” a patient or graduated neighbour on either can
   still walk the whole chain, same as today.
   ============================================================ */

"use strict";

export const DIFFICULTY_OPTIONS = [
  { label: "Easy", mult: "easy", note: "slow Β· no micro Β· predictable", aiApm: 20, aiMicro: false,
    workerTargetMult: 0.8, graceMult: 1.15, grievanceMult: 0.85, researchPaceMult: 1.3,
    forgiveness: 1.25, counterEvery: 0, strategicCeiling: true, adaptivity: 0 },
  { label: "Medium", mult: "medium", note: "a fair fight", aiApm: 65, aiMicro: false, marketAccess: true },
  { label: "Hard", mult: "hard", note: "fast Β· focus-fire Β· kite", aiApm: 140, aiMicro: true,
    workerTargetMult: 1.25, graceMult: 0.9, grievanceMult: 1.15, economicEdge: true, researchPaceMult: 0.75,
    marketAccess: true, rusherGraduates: true, forgiveness: 0.8, counterEvery: 2, adaptivity: 1.5 },
];

/* Tier 7 adds `adaptivity` β€” how strongly this difficulty lets the AI act on its READ of the
   opponent (engine/aiIntel.js, docs/ai-adaptive-opponent.md). Easy is 0: it never adapts, so its
   play stays exactly its learnable, exploitable archetype line. That is the identical argument
   Tier 5 makes for `counterEvery: 0` β€” a new player has to be able to form a model of the
   opponent and beat it, and an AI that answers everything they try is unreadable rather than
   hard. Medium carries none of it (the 1 default, i.e. it does adapt, at ordinary strength);
   Hard is 1.5, reacting harder and on thinner evidence.

   Read through adaptivityFor(state, owner) below with a `?? 1` at the source, so β€” as with every
   other dial in this file β€” an absent value composes as the ordinary behaviour and a legacy or
   unknown difficulty key is a no-op rather than an accidental opt-out. */

/** How strongly `owner` acts on its opponent read: 0 disables adaptation entirely, 1 is ordinary,
 * >1 reacts harder and on thinner evidence. @param {State} state @param {string} [owner] @returns {number} */
export function adaptivityFor(state, owner = "ai") {
  const v = difficultyFor(state, owner).adaptivity;
  return v == null ? 1 : v;
}

/** The active difficulty entry for `owner` (default "ai", so every existing save/test/call site
 * that predates self-play reads state.ai.difficulty exactly as before) β€” DIFFICULTY_OPTIONS'
 * medium entry when unset or naming a key that isn't in the list, the same fallback boot.js's
 * difficultyDials already applies before a match even starts. Tier 1 self-play (tools/selfplay.js)
 * passes owner="player" to read state.playerAi.difficulty instead, its own independently-picked
 * dial; 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 {Object} */
export function difficultyFor(state, owner = "ai") {
  const controller = owner === "ai" ? state.ai : state.playerAi;
  return DIFFICULTY_OPTIONS.find(o => o.mult === (controller && controller.difficulty))
      || DIFFICULTY_OPTIONS.find(o => o.mult === "medium");
}