Spaces:
Paused
Paused
Commit History
-
Phases 2-3: the AI answers what it readsβ
Claude +1: Claude Opus 5Unverified signaturePHASE 2 -- the raid stops being a metronome. It fired on `waveCount % RAID_EVERY` and only on Hard: harassment on a schedule, regardless of whether harassment was the right answer. It now ALSO fires when the read says the enemy is visibly investing in economy over defence and enough of them has been scouted to act on it. That is harassment as a response: punish greed, and specifically punish the hoarder docs/ai-evolution-design.md found winning by never committing its army. An opponent that can be SEEN to be undefended should not be safe -- which removes that degenerate strategy structurally instead of penalising turtling inside a fitness function, where evolution would just route around it. The read is deliberately not gated on controller.micro. Reacting to what you scouted is ordinary competence, not the unit-level micro Hard buys, and an Easy/Medium AI that never once punishes a greedy opening is the passivity the bench keeps measuring. PHASE 3 -- a damped stance. posture is far tβ¦5d60e36
-
Tier 1, step 2b: expand // @ts-check coverage from 10 files to 20 (D3)β
Claude +1: Claude Opus 5Unverified signatureAdopted in the order docs/code-improvement-tiers.md sets out, cheapest and highest bug-class value first: supply, aiCommon, colliders, victory, production, scout, the three AI leaves (aiStrategy, aiDifficulty, aiArchetypes), and finally persist.js β the prize, because every save field is a State/Unit/Building field read by name, so annotating it makes save-shape drift a check-time error. Each file's exports are annotated with the shared typedefs, not just given the pragma: per the guard added in the previous commit, a pragma without annotations checks nothing. Doing it surfaced three more genuine drifts in engine/types.js, all found by the checker rather than by reading: - Building.rally was typed {x, y} but carries nodeId β the rally-to-resource feature, where a rally dropped on a live node makes a produced unit spawn already mining (engine/production.js). - Building.queue entries were typed {unitType, progress} but carry `alt`, which records that a job was charged the unit'sβ¦7e764b0
-
Make the AI controller owner-parametric: true two-sided self-playβ
Claude +1: Claude Sonnet 5Unverified signaturerunAI(state, dt, owner = "ai") and every AI phase module now resolve their controller, opponent, and fog grid from `owner` instead of hardcoding "ai"/ state.fogAI β 100% backward compatible (every pre-existing call site keeps the default and behaves byte-for-byte as before). A new, independently configured controller slot (state.playerAi, null by default) lets a second real AI drive the "player" seat in a skirmish, headless-only (tools/ selfplay.js) β nothing in setup.js/boot.js/the shipped game changed. Independent adversarial review (fairness lens + correctness lens) caught a real bug before this was accepted: Hard difficulty's economic edge (hardEdge) was seeded onto state.players.ai only, never a self-play "player" controller configured for Hard. Fixed via seedDifficultyEdge (engine/state.js), called for "player" once state.playerAi exists. engine/persist.js's save/load extension is purely additive (no SAVE_VERSION bump) β old saves round-trip unaffected. Co-Authored-By: Claude β¦916c3ad
-
Add Easy strategic ceiling (Phase 7 prerequisite)β
Claude +1: Claude Sonnet 5Unverified signatureAn Easy neighbour's INDUSTRY_CHAIN climb and RESEARCH_ORDER research now stop before antimatterforge/'antimatter' Containment, withholding the whole Strategic tier (and everything downstream of it: Star Dock, Leviathan, Helium Bomb, Plasma Rig, and β this phase β a rival Antimatter Gate) from Easy neighbours, the same argument aiDifficulty.js already makes for withholding marketAccess. Medium and Hard are unaffected. This lands first because the rival-Gate workstream depends on it: an Easy neighbour must never be able to reach antimatter_gate's prereqs to begin with. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>5e33d82 -
Unify the AI's counter-pick: out-of-triangle counters, soft-answerβ
Claude +1: Claude Sonnet 5Unverified signaturefallback, turret-wall reading, and a difficulty-shaped cadence Three catalog proposals all independently patched engine/aiMilitary.js's ~12-line counterToPlayerArmy; this lands them as one coherent rewrite of that function (plus pickNextUnitType's cadence) instead of three overlapping diffs, per docs/improvement-roadmap.md's Phase 2 collision call, and adds the difficulty dial on top of the unified result. - entities.js: an optional `counteredBy` field on out-of-triangle unit defs (dreadnought/breacher/colossus/wraith -> skiff, aegis -> lancer), each empirically proven by a new cost- or supply-parity duel in balance.test.js (not guessed) so the field is self-verifying and can never silently go stale. Aegis specifically needs the supply-parity metric: cost parity actually favours it, but armies are supply-capped late-game, and at equal supply the Lancer wins. Leviathan is left to the soft-answer fallback below rather than a certified counteredBy. - aiMilitary.js COUNTERβ¦2bf7ef0 -
Doctrine research develops over time instead of landing instantlyβ
Claude +1: Claude Sonnet 5Unverified signatureGeneralizes engine/techtree.js's updateResearch to resolve its node table by building type (datacenter -> TECHS, refinery -> UPGRADES) instead of being hardcoded to the Datacenter, so the Refinery's doctrine upgrades now run through the same queued/timed development loop as the Odyssey tech tree. production.js's researchUpgrade becomes an enqueue: it pays up front, keeps the committedDoctrine lock and prereqsMet check, and adds a queued-dupe guard, instead of flagging the upgrade researched the instant it's paid for. Each UPGRADES entry gets a ~25-40s `time` field, scaled by researchTimeScale like every TECHS node, so a skirmish world's own tech rating finally matters for doctrine research too. Because the upgrade no longer lands in player.upgrades until it finishes developing, entities.js's committedDoctrine now also commits from a upgrade that's merely queued (paid for, still developing) at a Refinery β otherwise both doctrines could sit queued side-by-side during the development wiβ¦4b95948
-
Make the grievance / aggression cooldown part of the AI's personalityβ
Claude +1: Claude Opus 5Unverified signatureSouring was already flavoured by temperament β a Warlord world turns on you twice as fast as a patient one (grievanceMult) β but COOLING OFF was flat. Every world recovered from being bled at the same DRIFT_RATE, and `provoked`, the flag that lets a never-initiating strategy answer back, latched for good. So once you had fought anyone, every temperament held the grudge identically and for ever, which is the one place the three archetypes read the same. One `forgiveness` dial now drives both halves of that cooldown: - The stance drift is ASYMMETRIC. Recovery runs at DRIFT_RATE * forgiveness; souring keeps the stock rate, because that direction is already grievanceMult's job. A test pins the separation β the obvious implementation (scaling the whole drift) would have made a slow-forgiving personality also turn hostile slowly, which is not what was asked for. - Provocation is a TIMESTAMP, not a latch. It fades after PROVOKE_MEMORY / forgiveness seconds of quiet, β¦f8f75e7
-
Tier 4: Rusher graduation on Hard β the last tier of the proposalβ
Claude +1: Claude Sonnet 5Unverified signaturerusherGraduates (engine/aiDifficulty.js, Hard only) is the one identity- level dial in the whole section-08 proposal rather than a magnitude one. Past aiIndustry.js's RUSHER_GRADUATE_TIME (20 minutes) 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 OR-condition strategy.wantsIndustryAlways already is on that gate. Pure time-based, no wave-outcome tracking needed: if the game is still running this far in, the opening rush has clearly either succeeded or become a non-factor. Read test/aiArchetypes.test.js first, as planned β nothing in it assumes a Rusher never touches a Refinery, so this doesn't conflict with anything there. Skirmish is untouched by construction: aiIndustry.js already returns on !state.endless before this new condition is ever read, and a skirmish never runs anywhere near 20 minutes anyway. Medium never graduates, however long the game runs. This completes the tiered imβ¦8101484 -
Tier 3: market barter for the AI, gated by difficultyβ
Claude +1: Claude Sonnet 5Unverified signatureThe one genuinely new mechanic in the section-08 proposal, not just a multiplier on something that already exists. market.js's sell/buy can't be reused for the AI as-is β they're hardcoded to state.players.player.resources and the single shared galaxy.credits, and there's no AI-side credit account to plug into. Added aiBarter(state) instead: a same-planet CONVERSION, never touching galaxy.credits, priced off the world's own live market (unitPrice/applySlippage, same as a player trade). Surplus/bottleneck are measured on the same scale: coverage = resources held / the priciest single unit-or-building-or-upgrade purchase that ever costs that commodity (SPENDABLE, moved from aiWorkers.js to entities.js so market.js can share it without an aiWorkers.js -> market.js dependency running the wrong way). A commodity past 5x that yardstick is "surplus"; the lowest-coverage spendable commodity is the "bottleneck". One lot per think-cycle, same lot size the player's own trades use. Deterministic:β¦b02b34f -
Tier 2: AI-only research pace by difficultyβ
Claude +1: Claude Sonnet 5Unverified signatureresearchPaceMult (engine/aiDifficulty.js) speeds up or slows down the AI's own Datacenter research (engine/techtree.js updateResearch) β Easy 1.3x (slower), Hard 0.75x (faster). researchTimeScale itself stays untouched and per-WORLD as before (a high-tech world is fast for whoever researches there); the new aiResearchPaceMult is a further per-OWNER factor applied only when building.owner === "ai", so a player researching on the exact same world sees no change at all β covered by an explicit regression test. Doctrine upgrades (production.js's instant-on-afford researchUpgrade) aren't timed, so this dial has nothing to touch there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G73oK4p3bnreizCcMUHVEy9bb52d4 -
Tier 1: worker target, war patience, and a seeded economic edge by difficultyβ
Claude +1: Claude Sonnet 5Unverified signatureThree economic dials on top of Tier 0's scaffold, each composing multiplicatively with the archetype/strategy layers already in place: - workerTargetMult (aiEconomy.js) scales the AI's worker headcount target β Easy 0.8x, Hard 1.25x. - graceMult/grievanceMult (diplomacy.js) nudge the Odyssey war-onset timing β Easy a touch more patient, Hard a touch less; deliberately mild since it's a third multiplicative layer, and GRACE_FLOOR still bounds the opening window regardless. - economicEdge (Hard only) seeds a synthetic `hardEdge` UPGRADES entry (gatherYieldMult/produceTimeMult) straight onto the AI's own upgrades at creation. gather.js/production.js need no changes at all to pick it up β they already read any upgrade field generically via upgradeMult. That last one required hardening committedDoctrine (entities.js), which used to return on ANY UPGRADES membership rather than checking `.doctrine` specifically β harmless while every entry had a doctrine, but hardEdge doesn'tβ¦b28c2f0 -
Move DIFFICULTY_OPTIONS into engine/aiDifficulty.js, add state.ai.difficultyβ
Claude +1: Claude Sonnet 5Unverified signatureScaffold for tiered difficulty economics: the Easy/Medium/Hard table now lives in engine/ (mirroring how aiStrategy.js already holds STRATEGIES) with a difficultyFor(state) resolver alongside it, and every state.ai now carries the picked difficulty as a plain string, threaded through boot.js's skirmish/Odyssey paths and galaxy.js's per-planet seeding, with save/load round-trip support. setup.js re-exports DIFFICULTY_OPTIONS so boot.js and existing tests are unaffected. No economic field is added to any difficulty tier yet β every new/updated test proves this lands as a pure no-op. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G73oK4p3bnreizCcMUHVEy53ce1fb