Spaces:
Paused
Paused
Commit History
-
Tier 2b: four correctness fixes with a wider blast radius
Claude +1: Claude Opus 5Unverified signatureA yield-multiplied deposit could overfill a collection-point hold. The clamp was applied to the RAW quantity moved and the bonus to the CREDITED quantity, so a PARTIAL deposit landed more than fits — reproduced at 251 into a 250 hold at the Logistics Network's 1.25x. It breaks the hold's capacity and "a save round-trips identically" at once, because persist.js truncates the excess on load: continuing a game stopped being the same as saving and continuing it. That second half is invisible to every replay test, since both runs agree until someone saves. haul.js's own depositToFreighter never had the asymmetry — it moves and credits the same number — and gather.js now does the same. garrisonSlots' corrective pull-back never converged. Its header claims it "keeps the WHOLE parked group, not just its middle, inside the radius", but the anchor is pulled back RADIALLY while the farthest slot is an off-axis grid CORNER, so max|slot - cc| is not a linear function of the ring radius and one pas…2b50b54 -
Tier 2a: collapse the cross-module duplicates, pin the tie-breaks
Claude +1: Claude Opus 5Unverified signatureradiusOf existed THREE times in engine/ and bomb.js's comment asserted the copies were "the same". They were not: only bomb.js's handled a building, by reading the instance radius state.js stamps at construction; movement.js and formation.js returned the magic constant 9 for any building — while movement.js is a // @ts-check'ed file whose JSDoc advertises @param {Unit|Building}. The type layer was actively telling callers a Building was supported on a path that silently under-sized it, which is the exact class of bug MAX_UNIT_RADIUS exists to prevent and which no determinism test can see, being wrong-but-stable on every seed. One shared helper now lives in colliders.js, which already reasoned about def.radius and b.radius. Consolidating it surfaced a real subtlety the plan didn't predict: keying on `kind` alone broke formation.js, whose callers are frequently unit-shaped literals carrying no `kind` field at all. The helper resolves by TYPE first, with a comment recording why. clamp a…e847942
-
Let gather/wreckage/rig-haul workers bank into a nearby collection-point Hauler
ClaudeUnverified signaturenearestGatherDrop (engine/gather.js) — the single function that decides where a gathering worker, a wreck-salvager (same gather order, just targeting a node tagged wreck: true), and a Plasma Rig's own hauler (engine/haul.js's updateHaul reuses this exact function) all walk to deposit their load — only ever scanned buildings. A landed, player- toggled COLLECTION-POINT Hauler/Freighter (unit.collectPoint) was already a valid collection point for a FACTORY's backlog (the Ferry mechanic), but completely invisible to any of these three job types: directing a worker onto a mining site or a wreck, with a Hauler parked right next to it, still sent every trip all the way to the Command Center. nearestGatherDrop now also scans state.units for an owned, collectPoint-flagged, cargo-capable unit with room to spare — same eligibility assignFerry's own scan already uses — and returns whichever candidate (building or unit) is closer. Both call sites' deposit step now branch on the result: a building …e5f7f16
-
Gatherers roll to the next seam when a node runs dry
Claude +1: Claude Sonnet 5Unverified signatureengine/gather.js's updateGather used to null the order the instant node.amount <= 0 (both when the order is picked up on an already-dry node, and when the deposit that drains the last of it lands), so player workers idled at every depletion — the AI already self-heals via aiWorkers.js's assignIdleWorkers, but the player had no equivalent. Add nextNodeAfterDepletion(state, unit, node), called from both depletion exits: retarget to the nearest same-commodity node the owner has discovered (fog-gated via isNodeDiscovered, so a hidden cache stays hidden) within a ~600px radius, preferring one still under UNITS.worker.minerSoftCap over a nearer saturated one. Deterministic distance-then-id tiebreak. Falls back to idling exactly as before when nothing qualifies (out of radius, wrong commodity, or undiscovered). Applies to both owners, since the AI reassigns anyway on its next think. Updated the one existing test that pinned the old always-idle-on-depletion behavior to isolate the genuine no…6fcb8ac
-
Remove the Refinery/Foundry/Arsenal forward drop-off capability
Claude +1: Claude Sonnet 5Unverified signatureThey no longer double as decentralized resource collection points — the Command Center is the only drop-off. Removes the now-dead finite-intake buffer branches in gather/haul, the AI's forward-Refinery placement logic, and updates HUD/docs/tests accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HW46d81A9V6VumZWqw7XZU4ed9122
-
Make an explicit home-base assignment a hard zone boundary
Claude +1: Claude Sonnet 5Unverified signatureA player-assigned home base (unit.homeCC) fell back to a global search just like the default nearest-CC guess whenever its zone came up empty -- meaning a hauler pinned to one Command Center could still get poached into another base's territory, defeating the point of assigning it in the first place. zoneFirst now tracks whether its home zone came from a valid override or the default distance guess, and only widens to the whole empire in the latter case. A unit pinned to a Command Center now cycles through whatever other jobs that zone has to offer and simply waits when it has none, until the player reassigns it to a different CC or clears the override -- while a building-level manual assignment (service/repair/ ferry) keeps behaving as the single, hard-locked target it already was. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYFtYzV8C6Scdpz5dMJpoWee2f17b -
Add player-assigned home base override and worker repair of wounded units
Claude +1: Claude Sonnet 5Unverified signatureCloses the two items flagged as future work in the previous zone-affinity commit: a home-base override, and repairing mobile units. engine/gather.js zoneFirst now takes an optional homeId that, when it resolves to a live owned Command Center, wins over the usual nearest-CC distance guess. Right-clicking a completed own CC with eligible units selected (worker, Mender, freighter) pins unit.homeCC to it (issueSetHomeBase) — a passive assignment that only changes the unit's next idle job search, never its current order. A stale override (its CC destroyed) is ignored and falls back to the distance guess. Threaded through every zone-first call site in haul.js and repair.js's pickRepairTarget, so it covers haul/service/ferry job search and both the worker repair job and the Mender's own autoRepairRoam. assignRepair no longer restricts itself to buildings: a worker can now be assigned to chase down and patch a wounded mobile unit the same way it patches a turret, walking toward the target's …06de929 -
Add Command Center zone affinity for haulers, plus a worker repair job
Claude +1: Claude Sonnet 5Unverified signatureHaulers/servers/ferriers used to pick jobs by raw nearest-distance across the whole empire, with no notion of "which base do I belong to." Once a busy base's own ≤2-per-target caps filled up, idle labour got routinely poached across the map by another base's job. engine/gather.js zoneFirst now restricts each search to the worker's own Command Center zone first, only widening to the whole empire when the home zone has nothing to offer (byte-identical to before on any single-CC game). The auto-repair Mender's roam target-pick adopts the same zone-first logic via a shared pickRepairTarget helper (engine/repair.js). Buildings could previously only be healed passively by a Mender in range. Workers can now be assigned a repair job (auto, zone-first, after ferry/haul/service in priority; or manual via right-click, issueRepairBuilding), patching a damaged own building at a flat rate, capped per building like the existing haul/service caps. Both changes are pure functions of state and reused …32d2559
-
Fix hauler ping-pong loop when its source is itself a drop-off (#27)
Unverified signatureA Foundry/Refinery/Arsenal is both a haul-able collection point and a valid forward drop-off. Once a worker loaded cargo there, it was also the nearest drop-off (distance ~0, closer than a far-off CC), so updateHaul delivered the load straight back into the pile it just took it from, then reloaded — forever, with zero net progress. nearestGatherDrop now takes an excludeId so updateHaul's "toDrop" phase can exclude the job's own source building from the drop-off search. Co-authored-by: Claude <noreply@anthropic.com>fca5d94
-
Let a hauler deliver to the nearest forward drop-off, not just the Command Center
ClaudeUnverified signatureA worker hauling a Plasma Rig's (or any producer's) backlog always walked all the way to the Command Center, even with a Refinery/Foundry/Arsenal built right next to it — ignoring the "forward drop-off shortens a distant run" mechanic raw ore-gathering workers already get. updateHaul now picks the nearest place with room to take the load, Command Center or forward drop-off alike, re-checked every tick of the walk so a nearer option that appears (or frees up room) mid-trip is taken immediately instead of only at the moment the load first filled.454fb70
-
Logistics Phase C: finite forward drop-offs + gather reroute (player-only)
Claude +1: Claude Opus 4.8Unverified signatureThe last piece: collection buildings get finite storage too. A PLAYER's forward drop-offs (Refinery/Foundry/Arsenal) now have a finite intake buffer (storeCap 100). A gatherer banks its haul into that buffer; when it's full, nearestGatherDrop skips it so the gatherer reroutes to the next collection point — and the Command Center, the bottomless treasury/warehouse, never fills. Phase A haulage clears a forward buffer to the CC automatically. Goods now flow strictly through the warehouse: gather → forward drop-off buffer → haul → CC treasury → supply → factory → haul → CC. Haulage delivers and supply picks up at the nearest Command Center (nearestCommandCenter), never sideways between drop-offs. Factories are excluded from gather drop-offs (isGatherDropOff): their `store` is refined output, not a raw-intake bin. - entities.js: storeCap on refinery/foundry/arsenal; isGatherDropOff helper. - gather.js: player forward-drop-off banks to the building's buffer (capped, leftover reroutes); …89beffa -
Logistics Phase A: finite rig output buffer + worker haulage
Claude +1: Claude Opus 4.8Unverified signatureStorage stops being an infinite sink and workers get a job that lasts to the end of the game. The Plasma Rig (a leaf producer of raw goods — the safest surface to prove the mechanic, since its output feeds no other factory) now banks each dig into a FINITE local buffer (building.store, capped at def.storeCap 120) and stalls the moment it's full. Those goods aren't spendable until hauled to a Command Center. New worker haulage (engine/haul.js): an idle PLAYER worker auto-assigns to the nearest own producer whose buffer is ≥34% full (≤2 haulers per producer, ties broken by id), walks there, loads a cargo, carries it to the nearest drop-off, and banks it into the treasury 1:1 — no gather multiplier, since the rig already extracted it. Run short on haulers and the buffer fills and the rig stalls: logistics is now a live demand on labour, exactly as intended. - entities.js: storeCapOf / storeTotal / storeRoom helpers; storeCap on the rig def. - rig.js: bank to store (top off the final dig…91ad84b
-
Add JSDoc typedefs + // @ts-check for the core sim shapes (no build step)
Claude +1: Claude Opus 4.8Unverified signatureAddress the top architectural-review finding — an untyped mutable `state` bag threaded through ~360 functions, where a mistyped or renamed field reads undefined silently and the same-seed determinism test can't see it. Now that class of bug is a check-time error. Zero runtime cost and no build step: the shipped code stays plain ES modules. - engine/types.js: JSDoc @typedefs for the core shapes (State, Unit, Building, Player, AiState, Resources, Cargo, Order, ResourceNode, GameMap, Fog, Market, Diplomacy, Galaxy, …). No runtime code, never imported — global typedefs the checker and any editor's TS language service resolve project-wide. - jsconfig.json + `npm run typecheck` (tsc, noEmit): checking is OPT-IN per file via a `// @ts-check` pragma, so coverage grows file-by-file. Editors check the annotated files live with no install; the script needs a tsc available. - Enabled + annotated on the core data + hot path: state.js (the factories bind the types to the real objects), …e3658fa -
Tier 5: dedup and derive engine helpers (byte-identical replays)
Claude +1: Claude Opus 4.8Unverified signatureRemove copy-paste and stale magic numbers from the engine, keeping every change deterministic (same-seed replays stay byte-for-byte identical, the determinism + engine-purity guards stay green). - rng.js: centralise the string hash (h=7, h*31+c) as hashStr(). It lived byte-identical in four modules (combat/gather/movement/separation) for a worker's orbit angle, an avoidance dodge, a spread-fire pick and the coincident-pair nudge — one home so a change can't drift between copies. - movement.js: derive MAX_UNIT_RADIUS from the roster instead of a stale literal (10, "Breacher") that under-sized the avoidance query the moment a wider hull (Bulk Freighter, 15) was added; separation.js's SEP_RADIUS now keys off it too. A larger query returns a superset the exact-distance check still filters identically, so replays are unchanged. - movement.js: add keepEscortStation() next to escortSlot() and route the three identical escort branches (combat ship, worker, support drone) thr…8583c67
-
Tier 2: a third upgrade doctrine — Logistics (economy + tempo)
Claude +1: Claude Fable 5Unverified signatureUpgrades were only ever ±damage; add a macro path so "out-produce them" is a real strategic fork against "out-fight them". Logistics is a third mutually-exclusive doctrine (via the same committedDoctrine lock) with two tiers: Logistics Network (T1) — +25% resource yield from every haul Rapid Fabrication (T2) — -20% unit & building production time Committing to it locks Assault AND Bulwark, and vice-versa — so it's a genuine opportunity cost, not a free third buy. Plumbed data-driven: a new shared entities.upgradeMult(upgrades, field) is the one place combat/gather/production read upgrade effects, so a new upgrade is pure data. combat.js's two inline damage-mult loops are refactored onto it (behaviour identical); gather.js reads gatherYieldMult, production.js reads produceTimeMult (construction and unit production), both optional-chained so map-less test stubs still pass. The Refinery UI is already data-driven — it picks up the new doctrine from the UPGRADES table plus a label …3195ab6 -
Industrial buildings double as resource drop-off points
Claude +1: Claude Fable 5Unverified signatureDecentralize the economy so reaching a distant deposit no longer forces a whole second Command Center. The Refinery, Foundry and Arsenal — the industrial line — now double as resource drop-offs that proxy the CC: workers bank their haul at the NEAREST completed drop-off, so planting one of these cheaper, faster buildings forward shortens the mining run. A forward Refinery (200 ore / 16s) collects for a claim instead of a full Command Center (400 ore / 30s), while the CC keeps its unique roles — training workers and granting supply — so it's shortened hauls, not an obsolete CC. - entities.js: `dropOff` flag on refinery/foundry/arsenal + an `isDropOff` helper (CC via isCommandCenter, or the flag) as the single source of truth. - gather.js: the deposit phase routes to the nearest drop-off, not only a Command Center. Still requires a COMPLETED building. - main.js: build-button tooltips note "resource drop-off" so the role is discoverable (and Habitat/CC now show their supply grant …eb17bae -
Asymmetric world modifiers: two starts that differ
Claude +1: Claude Fable 5Unverified signatureWorld modifiers could only tilt both sides equally. A world may now carry an `asym: { player, ai }` block that overrides a modifier for just ONE side, so the two corners play as a real asymmetric matchup. - map.js: sideMod(state, owner, key) resolves the owner's asym override, then the shared modifier, then the default — so symmetric worlds and map-less test stubs behave exactly as before. Movement (speed), fog (sight), combat (aggro sight), production (build time), and gather (a new per-side gatherMult economy edge) all read it with owner context. - Two asymmetric worlds, telegraphed on the map card: - Oort "Contested frontier": player banks 20% more per haul (rich claim); the AI builds 18% faster (war factory). Out-mine vs out-build. - Nimbus "Storm front": player sees almost normally (0.95) where the AI stays half-blind (0.75), but the AI's units are 12% faster. Out-scout vs out-tempo. Safe for the resolves-to-a-winner guarantee by construction: an AI-side edge…d228ca7 -
Content: economy saturation (diminishing returns on over-mined nodes)
Claude +1: Claude Fable 5Unverified signaturePiling workers on one deposit now has diminishing returns, so spreading out and expanding to a fresh field is a real economic decision instead of a free lunch. - entities.js: worker gains minerSoftCap (3) and minerFalloff (0.4). The first three miners on a node run at full rate; each extra pulls only 40% of a share, so the node's per-worker efficiency is the average — floored above zero, so a lone remaining seam never softlocks a poor world. - sim.js: countMiners() tallies gather assignments per node once per tick (frozen before any worker mines, so every miner reads the same count regardless of Map order — determinism-safe). - gather.js: miningEfficiency() applies the falloff at the take; no field / no count reads as full rate, so direct-call unit tests are unchanged. - ai.js: assignIdleWorkers now fills a seam to the soft cap then hops to the next-nearest (a saturation-steer term added to the distance score), shipped atomically so the AI doesn't pile everyone on one …1b1f280 -
Tier 6: performance & scale for large maps
Claude +1: Claude Fable 5Unverified signature- Uniform spatial grid (engine/grid.js), rebuilt once per tick, retires the three per-tick O(n^2) neighbour scans: separation, movement avoidance, and combat target acquisition now query only nearby cells. ~2.6x faster on the separation pass alone at 500 units, and the gap widens with scale. A full-scan fallback keeps every direct unit test byte-for-byte unchanged. - Viewport culling in render.js: fog cells, nodes, buildings and units outside the camera's world rect are skipped, so draw cost tracks what's on screen rather than total map size (16k+ fog cells on a 4x map). - Cache each resource node's silhouette (the seeded-PRNG polygon) instead of recomputing it every frame; only its radius changes as it depletes. - Index map.nodes by id (map.nodesById) — O(1) node lookups in gather, the AI, replacing per-tick linear .find scans that grew with map size. - Loop substep cap so a heavy tick degrades to slow-motion instead of spiralling into an ever-deepening backlog. - Pr…6d7e8fa -
Tier 0: fix latent defects, add determinism/perf tests and CI
Claude +1: Claude Fable 5Unverified signature- Building construction no longer heals away combat damage every tick: updateBuildingConstruction now ADDS the hp built this tick (capped at the progress ceiling) instead of overwriting hp downward, so sustained fire can actually raze a half-built structure — raiding buildings-in-progress works for both sides now. - createGameState resets the entity-id counter so a fresh game is a pure function of its seed; same-seed runs replay byte-identically. - A worker re-tasked to a different-commodity node hauls its existing load home instead of silently discarding the mined resources. - render.js prunes dead entities from the facing Map each frame and exposes resetFacing(), wired into new-game startup — no more unbounded growth. - New tests: full-sim determinism, id-reset reproducibility, a perf guard, construction-under-fire persistence + destruction, cargo preservation. - Add GitHub Actions workflow running `node --test` on push/PR. Co-Authored-By: Claude Fable 5 <noreply@ant…430c532
-
Give units proactive local avoidance, not just after-the-fact separation
Claude +1: Claude Sonnet 5Unverified signaturePreviously the only anti-stacking logic was separation.js's reactive pass, run after movement each tick to push already-overlapping units apart. Units still walked straight through each other to get there — correct eventually, but visibly "stepping on" one another en route. stepToward() now senses same-owner units ahead of it (within a radius beyond their combined size) and steers laterally around them while moving, so a group naturally routes around a neighbor instead of overlapping it and getting shoved apart afterward. Opposing-owner units are still ignored, same as separation.js, so combat can still close to weapon range. Caught a real deadlock while testing this: pure radial repulsion (push directly away from the neighbor) has zero sideways component when the neighbor sits exactly on the straight-line path — by symmetry there's no "which way to dodge" signal, so a unit just stalls dead behind it forever. Fixed by steering perpendicular to the direction of travel instead of radia…9175b1f -
Fix worker stacking on a shared node; add a battlefield picker
Claude +1: Claude Sonnet 5Unverified signatureWorkers sent to the same node all walked to its exact center and rendered fully stacked. gather.js now sends each one to a stable per-unit point on a small ring around the node instead, so a group visibly spreads out around what it's mining. Also replaced the hardcoded single map with a pre-game picker (three ore-bearing worlds pulled straight from data.js: Ferros Prime, Korrath, Vesper), each with a genuinely different resource layout since node cluster count scales with the planet's own deposit yields. The post-game overlay now offers "Choose another battlefield" to start a fresh skirmish on a different world without reloading the page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmJ8vc9jxc6TUegK9F2YJ6fe5e5b -
First playable slice: 1v1 skirmish vs scripted AI on Ferros Prime
Claude +1: Claude Sonnet 5Unverified signatureGather ore/crystals/radioactives with Workers, build a Barracks, produce Skiffs, fight, win by destroying the enemy Command Center. engine/ holds the sim: a fixed-timestep loop decoupled from render rate, movement, the gather/deposit loop, combat with auto-acquire, building construction + unit production queues, a scripted AI opponent, and the win condition. render.js/input.js are the canvas view and mouse controls (drag-select, right-click context orders, build placement); main.js wires it to a DOM HUD. Converted data.js's top-level tables to ES module exports so the engine can import them directly. Covered by 23 node:test cases including a full simulated skirmish that plays to a winner, and manually playtested end-to-end in a real browser (gather -> build -> produce -> victory/defeat UI) via Playwright. One real bug caught in that process: constructed buildings never got a `radius` copied onto the instance, which went on to poison unit spawn points and building hit-testing with Na…17641e3