Spaces:
Paused
Paused
Commit History
-
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 -
Tier 1, step 4a: five reproduced engine defects (A1-A4, A11)
Claude +1: Claude Opus 5Unverified signatureEvery fix was preceded by a test verified red against the real behaviour, and the two A1 tests were additionally re-run with the fix reverted to prove they bite rather than passing incidentally. A1 — keepFollowingLeader decided its leader was gone by hp alone. A follow-leader order holds a live object REFERENCE, and two removal paths delete a unit that is still at full hp: deployColonyShip and updateUnitRecycle. Escorting a colony ship to a site and deploying it is the canonical Odyssey play, and it left both escorts permanently pinned to a ship no longer in state.units, orbiting its frozen last position. They still accept new direct orders, so it read as "my units randomly stopped obeying the formation". The check now asks the state, not the field. (escort orders were always immune — escortSlot resolves through state.units.get every tick.) A2 — the squad-release loop spliced the array it was iterating. A for...of iterator is index-based, so every splice skipped the next element: dro…0da037f
-
Make scout mode lead a formation instead of abandoning it
ClaudeUnverified signatureReverses the previous fix: a Ranger leading a formation should stay with the group as a protective scout, not break away solo. issueScout now leaves its followers on their existing follow-leader order (instead of releasing them) and caps the scout order's speed to the group's slowest member via a new groupSpeedCap helper shared with dispatchFormation's existing speed-cap logic. scout.js now runs that cap through orderedSpeed instead of always using the unit's own full speed. A solo Ranger (no squadFollowers) is unaffected — it still scouts at its own full speed. Verified end-to-end via the real sim tick loop: a Ranger leading a Worker, once put into scout mode, travels together with it at the Worker's pace instead of pulling away.f71d721
-
Redefine formation leader as first-selected unit, add persistent follow, speed cap, and drag-to-face
Claude +1: Claude Sonnet 5Unverified signatureReworks the formation system per feedback on PR #9: - Leader is now whichever unit the player selected first (the one others get added to), not a stat-based pick. pickLeader() and layout() in engine/formation.js simply use array order instead of scanning for highest maxHp. - Selecting and moving just the leader now drags along everyone previously grouped with it, with no need to reselect the group. A new persistent "follow-leader" order (engine/commands.js, engine/movement.js, engine/sim.js, engine/combat.js) mirrors the existing escort-order pattern: followers re-seek the leader's live position every tick via object references (unit.squadLeader / leader.squadFollowers), set up by dispatchFormation() and torn down by any direct order that bypasses it (stop, hold, scout, build). - A formation's travel speed is capped to its slowest member so a fast leader can't stretch the group out of shape. dispatchFormation() computes the cap once at issue time and stores i…6648f62 -
Formation shapes, leader positioning, and nested formations for grouped units
ClaudeUnverified signatureMulti-unit move/attack-move used to just spread the group across a flat square grid (engine/commands.js formationSpots) with no leader concept and no way to hold that shape once stationary. This adds: - Four selectable formation shapes (engine/formation.js): Grid (the original spread, kept byte-identical for the default case), Line (abreast the direction of travel, leader nudged front or back), Wedge (a spearhead with the leader at the tip, or a protective chevron with the leader shielded at the rear and flanks leading), and Circle (a ring around the leader at its center — "protection of a formation," the group-scale sibling of the existing single-target Escort). - A deterministic "leader": the unit with the most maxHp, ties broken by id — never selection-order-dependent. - Spacing derived from the group's own hull sizes, not a flat constant, for every shape except the untouched default grid path. - Nested formations: a selection already spread into distinct spatial …4c8f450
-
Address remaining review defects: B3–B6 + load-hardening + static checks
Claude +1: Claude Opus 4.8Unverified signatureA fanned-out team of agents fixed the remaining SAFE items from the architecture review, partitioned by file so they never collided; verified and committed centrally. All identity for valid data (guarded on field presence/range), so determinism + round-trip guarantees hold — behavior changes only for corrupt/tampered/NaN input. Load hardening (engine/persist.js, test/save-hardening.test.js): - B3: clamp a wonder's `charge` to [0,1] on load — a hand-edited charge:1 no longer trips an instant standalone-endless win (victory.js). - B4: recompute the galaxy `g`-id counter (`entitySeq`) from live g-ids across all worlds on load, mirroring maxOwnEntityId for the u/b counter — a low/missing entitySeq can no longer mint a colliding g-id over an in-flight colony ship / freighter / relief ship. - B5: coerce untrusted order/orderQueue coords (finite, in-map) and worker cargo (finite qty, real commodity) so a tampered save can't NaN-poison the movement/gather math. - Non-array `researchQu…fdea120
-
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
-
Escort order: right-click a friendly ship to guard and follow it
Claude +1: Claude Opus 4.8Unverified signatureRight-clicking a friendly ship with a group selected now puts the group into a protective escort formation: each unit takes a stable slot on a ring around the target and follows it wherever it's ordered, holding station until given another order. A combat escort still auto-acquires threats that come near the target, then reforms — so it actually protects the ship, not just trails it. - movement.js: escortSlot() — the deterministic per-unit point on the ring around the guarded ship (the ring expands to seat the whole group). Null when the target is gone, so callers drop the order. - commands.js: issueEscort() assigns each unit an {type:"escort", targetId, slot, slots} order (stable slots → deterministic formation). - combat.js / sim.js: handle "escort" for combat, worker, and support units — a persistent follow (never cleared on arrival). Combat escorts fall through the normal auto-acquire first, so they defend then reform. - input.js: a right-click on a friendly UNIT (not bu…b1fda0e
-
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: terrain — rough ground, high ground, and no-build zones
Claude +1: Claude Fable 5Unverified signatureGives the map spatial identity without any pathfinding rewrite: a static per-cell terrain field (a flat Uint8Array, fog-grid idiom) drives three purely local effects. - map.js: TERRAIN table + generateTerrain (fixed fractional feature specs, mirrored for fairness, scaling self-similarly) + sampleTerrain (O(1), OPEN out of bounds). Consumes ZERO rng draws, so the node layout stays byte-identical and determinism is untouched. Terrain is static for the whole match. Per-world specs on Glacius (rough ice fields flanking a central lane — a soft choke), Forge (rough industrial sprawl), and Pyralis (a high-ground mesa); the original three worlds stay all-open. - movement.js: rough ground slows the step (never to zero — no unit can ever be trapped, so a wave can't deadlock and the resolves-to-a-winner guarantee holds). Honours an optional per-unit allTerrain flag (forward-compat). - fog.js: high ground sees farther (per-source sight scaled by terrain). - combat.js: an attacker …03f71c7 -
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 4 AI parity + planet roster with per-world rule modifiers
Claude +1: Claude Fable 5Unverified signatureThe AI now plays all of Tier 4, driven by extended archetype profiles rather than hardcoding: it fortifies with Sentinel Turrets on the approach vector (Economist 2, Balanced 1, Rusher 0), expands to a second Command Center when home ore runs thin (banking for it via an ore reserve that pauses the unit mix instead of starving it), runs one shared production cycle across multiple Barracks (fixing the one-unit-at-a-time ceiling), and folds the Breacher into the patient mixes where the map can pay for it. A spendable-node worker filter keeps a poor-economy world's AI off dead-end commodities. Every new field is read with a use-site default, so a legacy profile still runs. Opens the picker from 3 to 9 curated worlds, each assigned an archetype and up to one data-driven rule modifier threaded symmetrically through the engine: Glacius slows all units, Nimbus/Pyralis shorten/lengthen sight and aggro, Helix adds a crystal cluster, Oort enriches deposits, Forge speeds construction. All modifie…ac7d42f -
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 -
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