Spaces:
Paused
Paused
Download engine/aiDifficulty.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 8.22 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/aiDifficulty.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities/engine/aiDifficulty.js
-
curl -L -o aiDifficulty.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/aiDifficulty.js
8.22 kB
| // @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. | |
| ============================================================ */ | |
| ; | |
| 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"); | |
| } | |