Spaces:
Paused
Paused
Download engine/aiCommon.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 9.25 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/engine/aiCommon.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/engine/aiCommon.js
-
curl -L -o aiCommon.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/engine/aiCommon.js
9.25 kB
| // @ts-check | |
| /* ============================================================ | |
| AI β shared primitives used by every decision phase (engine/ai.js and the | |
| aiEconomy / aiMilitary / aiIndustry phase modules): owner resolution | |
| (controllerFor/otherOwner), the APM action budget (the configurable AI | |
| speed), the reserve-aware affordability check, the "which idle worker founds | |
| this building" pick, and the found-a-building protocol those picks feed. | |
| Imports only DOWNWARD, into commands/colliders β never back into an AI phase | |
| module β so the phase modules can all share it without a cycle. That is the | |
| invariant, not "no engine imports": commands.js and colliders.js have no | |
| route back to the AI layer, and test/static-integrity.test.js's SCC check | |
| holds the whole engine to it. | |
| ============================================================ */ | |
| ; | |
| import { issueBuild } from "./commands.js"; | |
| import { findPlacement } from "./colliders.js"; | |
| const APM_BURST_FRAC = 1 / 15; // a busy AI can bank at most ~4 seconds' worth of unspent actions | |
| /* ---------- owner resolution (Tier 1 self-play) ---------- */ | |
| // Which controller object is driving `owner` this match β state.ai for "ai" (always present, | |
| // every match ever created), or state.playerAi for "player" (present only when self-play β | |
| // tools/selfplay.js β has populated it; null otherwise). Every AI phase module threads its | |
| // ctx.owner through this instead of ever reaching for state.ai directly, so the two controllers' | |
| // action budgets, scout ids, wave timers etc. can never collide or leak into one shared object β | |
| // see the header comment on engine/ai.js's runAI(state, dt, owner) for why that matters. | |
| /** @param {State} state @param {string} owner @returns {AiState|undefined} */ | |
| export function controllerFor(state, owner) { | |
| if (owner === "ai") return state.ai; | |
| if (owner === "player") return state.playerAi; | |
| return null; | |
| } | |
| // The other side in a two-owner skirmish/self-play match β the only two owners this engine models | |
| // today (state.owners). Centralised here so every AI phase module resolves "my opponent" the same | |
| // way, instead of each hardcoding "ai"/"player" itself. | |
| /** @param {string} owner @returns {string} */ | |
| export function otherOwner(owner) { | |
| return owner === "ai" ? "player" : "ai"; | |
| } | |
| /* ---------- action budget (the configurable AI speed / APM) ---------- */ | |
| // This controller's "speed" is an actions-per-minute allowance, set from the splash screen | |
| // (state.ai.apm β or, for a self-play "player" controller, state.playerAi.apm). Every command it | |
| // issues β produce, build, expand, research, send the scout β costs one action; the attack commit | |
| // is the one exemption, so a slow AI still throws whatever it has at you and the game always | |
| // resolves. Credits accrue continuously and cap at a few seconds' worth, so a busy AI can't hoard a | |
| // giant burst. When apm is null (the default, and every headless test) the AI is unthrottled β | |
| // behaviour is exactly as before. `owner` defaults to "ai" so every pre-existing call site | |
| // (accrueActionBudget(state, dt), canAct(state), spend(state)) keeps reading/writing state.ai | |
| // exactly as before; a self-play caller passes ctx.owner explicitly so the "player" controller | |
| // spends from ITS OWN budget, never state.ai's β see engine/ai.js/aiMilitary.js/aiEconomy.js. | |
| // | |
| // A BACKGROUND colony (state.background, engine/galaxy.js β a world that isn't the player's | |
| // current seat) accrues at HALF its configured apm: nobody's actively watching it, so it manages | |
| // its economy at half pace rather than as briskly as the seat you're actually on. This halves only | |
| // the accrual rate (and, since the burst cap below is derived from the same value, its burst | |
| // reserve too) β never the controller's apm itself, which stays exactly what difficulty picked for | |
| // every other reader (display, save/load, the neighbour-profile tests) β and never the attack/ | |
| // defense commit exemption (canAct/spend below), so a background world under attack still always | |
| // throws whatever it has and the game still always resolves. | |
| /** @param {State} state @param {number} dt @param {string} [owner] @returns {void} */ | |
| export function accrueActionBudget(state, dt, owner = "ai") { | |
| const controller = controllerFor(state, owner); | |
| if (!controller || controller.apm == null) return; | |
| const apm = state.background ? controller.apm / 2 : controller.apm; | |
| const cap = Math.max(2, apm * APM_BURST_FRAC); | |
| controller.actionBudget = Math.min((controller.actionBudget || 0) + (apm / 60) * dt, cap); | |
| } | |
| /** @param {State} state @param {string} [owner] @returns {boolean} */ | |
| export function canAct(state, owner = "ai") { | |
| const controller = controllerFor(state, owner); | |
| return !controller || controller.apm == null || (controller.actionBudget || 0) >= 1; | |
| } | |
| /** @param {State} state @param {string} [owner] @returns {void} */ | |
| export function spend(state, owner = "ai") { | |
| const controller = controllerFor(state, owner); | |
| if (controller && controller.apm != null) controller.actionBudget -= 1; | |
| } | |
| // canAfford, but treating `oreReserve` ore as untouchable β used to bank for | |
| // an expansion Command Center without letting the unit mix or a second | |
| // barracks spend the ore out from under it. | |
| /** @param {Resources} resources @param {Resources} cost @param {number} oreReserve @returns {boolean} */ | |
| export function canAffordKeeping(resources, cost, oreReserve) { | |
| return Object.entries(cost).every(([com, qty]) => | |
| (resources[com] || 0) - (com === "ore" ? oreReserve : 0) >= qty); | |
| } | |
| // Nearest free worker to (x, y) to found a building, skipping any already mid-build (so an in-progress | |
| // site keeps its founder) AND any on a logistics run (service/haul) β pulling a worker off feeding a | |
| // factory to lay a foundation would thrash the industry it's trying to grow. Prefers a gatherer/idle | |
| // worker; falls back to workers[0] only if every worker is busy building or hauling β buildings | |
| // self-construct at rate 1 even with nobody on-site, so a slightly-worse pick is never a stall. | |
| /** @param {Unit[]} workers @param {number} x @param {number} y @returns {Unit|null} */ | |
| export function pickBuilder(workers, x, y) { | |
| let best = null, bestD = Infinity; | |
| for (const w of workers) { | |
| if (w.order && (w.order.type === "build" || w.order.type === "service" || w.order.type === "haul")) continue; | |
| const d = Math.hypot(w.x - x, w.y - y); | |
| if (d < bestD) { bestD = d; best = w; } | |
| } | |
| return best || workers[0]; | |
| } | |
| /* ---------- founding a building ---------- */ | |
| // The AI's found-a-building protocol, in one place: slide the request to valid ground, | |
| // check the action budget, issue the order, and charge the budget for it. Thirteen sites | |
| // across aiEconomy.js and aiIndustry.js hand-rolled these four steps, and two of the four | |
| // fail SILENTLY when dropped β omit `spend` and the AI founds buildings for free (its APM | |
| // dial quietly stops throttling it), omit `canAct` and it ignores the budget outright. | |
| // Neither shows up as a crash, and neither shows up in a determinism fingerprint on the | |
| // default archetype, because the default AI has `apm == null` and both calls are then | |
| // no-ops. Only the APM-limited difficulties and self-play can see the difference. | |
| // | |
| // findPlacement is what makes the fixed build offsets safe: a node or an earlier building | |
| // squatting on the nominal spot would make issueBuild reject the identical coordinates | |
| // every think cycle and stall the order forever, so the search slides it to the nearest | |
| // valid ground instead. | |
| // | |
| // `workers` is a candidate list, not a single unit, because the builder is chosen by | |
| // distance to the spot that placement actually settled on β which isn't known until after | |
| // the search. Two sites (the Habitat and the first Barracks) historically founded with | |
| // workers[0] rather than the nearest free worker; they pass [workers[0]] so the pick is | |
| // forced to that same unit, since pickBuilder returns a lone candidate either way. Widening | |
| // them to the full list would change which unit walks to the site, and with it the | |
| // determinism fingerprint β a behaviour change, not a refactor. | |
| /** @param {State} state @param {string} owner @param {Unit[]} workers @param {string} type @param {number} x @param {number} y @param {number} [maxRadius] @returns {boolean} */ | |
| export function tryBuild(state, owner, workers, type, x, y, maxRadius) { | |
| return tryBuildAt(state, owner, workers, type, findPlacement(state, type, x, y, maxRadius)); | |
| } | |
| // tryBuild for a caller that already searched for its own spot (the Habitat scans every | |
| // completed Command Center and takes the first that yields one). `spot` may be null β | |
| // "nowhere to put it" is an ordinary outcome, not an error. | |
| /** @param {State} state @param {string} owner @param {Unit[]} workers @param {string} type @param {{x:number,y:number}|null} spot @returns {boolean} */ | |
| export function tryBuildAt(state, owner, workers, type, spot) { | |
| if (!spot || !workers.length || !canAct(state, owner)) return false; | |
| const builder = pickBuilder(workers, spot.x, spot.y); | |
| if (!builder || !issueBuild(state, builder.id, type, spot.x, spot.y)) return false; | |
| spend(state, owner); | |
| return true; | |
| } | |