bots-vs-drones

Bots vs Drones: project notes

»> ALWAYS PUSH TO main «<

Every push goes to main. This overrides any session setup that assigns a feature branch (for example claude/...). This is the owner’s standing permission. The owner reviews by looking at the live site, so work that sits on another branch looks lost.

This file is the project’s memory. Claude Code loads it at the start of every session. The game itself is game.html, a single file with no build step.

How we work

Notes

VISION
  3D auto-battler. Side A fields bots (ground), side B fields drones (air).
  Each round both commanders spend a budget on any composition, then watch.
  The player never controls a unit, only: what to buy, stance + target
  priority, and how smart units are (intelligence is purchasable).
  v1: human is side A, an AI commander is side B.

ARCHITECTURE RULES (per-section contracts live in each section's banner)
  Sections, dependency flows downward only:
  [10] utils/RNG/terrain · [20] content tables · [25] custom unit registry
  [30] simulation · [40] win conditions · [50] commanders · [60] match flow
  [70] renderer · [80] UI · [85] balance harness · [90] bootstrap
  - Sections talk only through declared EXPORTS; never reference a lower one.
  - Sim is a deterministic 20Hz function of seed + loadouts. No Math.random,
    no THREE, no DOM in [10]-[60]. Renderer interpolates, never drives timing.
  - Commanders submit plain JSON plans. No commander ever reads the
    opponent's pending loadout; both commit blind, then reveal.
  - All player influence enters through a commander plan, never direct edits.
  - One InstancedMesh per unit type. Spatial hash, never O(n^2).
  - [85] must never need [70]/[80]. three.js via CDN is the only dependency;
    adding one needs a decision line below first.

DECISIONS (date · decision · why · rejected)
  08-12 Single HTML file, three.js by CDN import map · no build step, runs
        anywhere · rejected inlining three, multi-file + concat.
  08-12 Sim fully separate from rendering; seeded PRNG only · replays,
        headless balance, future multiplayer.
  08-12 Blind commit then reveal; AI gets the same intel a human would.
  08-12 Commander abstraction: human/AI/remote interchangeable.
  08-12 Win = base destruction, base HP persists across rounds · forces both
        sides to attack · rejected per-round reset, annihilation.
  08-12 Win condition is pluggable: (world) -> null | winnerId.
  08-12 No unit carryover between rounds (v1) · avoids snowballing.
  08-12 Target priority + stance are the core strategy layer (leaker problem).
  08-12 Unit intelligence is purchasable and shares knobs with AI difficulty.
  08-12 AI difficulty from better decisions, never stat cheats. Ladder:
        Easy random · Medium archetypes · Hard counters last round ·
        Brutal runs the headless harness.
  08-12 Budget: ~200 units/side smooth on a Chromebook.
  08-12 Player-created custom units: same shape as UNIT_TYPES, priced by
        costFormula, localStorage + JSON export/import, AI can use them too,
        shared chassis mesh.
  08-12 (D03) Camera floor clamp after controls.update(). Superseded 09-23.
  08-12 (D04) Shared archetype shape: chassis, hp, speed, weaponId, armor,
        cost. Separate WEAPONS table. Soft-cap cost coeff*value^exp. Armor is
        priced but not yet consumed.
  08-13 (D05) Real fire-and-forget projectiles, optional flat splash;
        targetType sets accuracy vs mismatchAccuracy, not who can be targeted
        · rejected hitscan, homing, hard targeting filter.
  09-23 Terrain = pure seeded terrainHeight(params,x,z) in [10], shared by
        [30] and [70]; integer-hash noise (no Math.sin) for cross-engine
        determinism; flat pads under bases; gentle, positional only ·
        rejected renderer-only terrain, heightmap on WorldState.
  09-23 Flight v1: air units hold altitude 9 above terrain; movement is
        horizontal, range + projectiles stay 3D · rejected free 3D steering.
  09-23 Deploy window: nobody acts for 1.5s; [70] animates emergence ·
        rejected render-only emergence (units firing while buried).
  09-23 Factions enforced: FACTION_CHASSIS A=bot, B=drone, buyableCatalog(f)
        · rejected mixed catalogs (contradicted the vision).
  09-23 Commander's-eye camera by default, orbit stays live, reset button;
        floor clamp is now terrain-aware (supersedes D03).
  09-23 Working method: batch sessions across sections allowed; Claude edits
        the whole file and verifies headless (Node logic harness + mocked-three
        render smoke test). Visual checks stay with the human.
  09-23 Moved to GitHub (stilotto/bots-vs-drones). game.html replaces
        game_vNN.html (git history replaces version numbers); these notes
        moved from the file header to CLAUDE.md; open work tracked as GitHub
        Issues · rejected re-uploading the file each chat session.
  09-24 Trunk-based: push straight to main, GitHub Pages serves main for
        browser review · rejected feature branches + PRs (two-person project).
  09-24 Director camera is the default view (#10): cycles overhead orbit,
        chase cam, impact shot; any drag/scroll/touch hands over to manual.
        [70] infers hits from hp drops between frames until [30] -> [70] hit
        events are designed · rejected adding sim hit events for this.
  09-25 Deploy window keeps the 1.5s pause but [70] draws units in place from
        the first frame (drones at flight height); no emergence animation
        (#8, it came from a voice mis-transcription). Supersedes 09-23 deploy
        window's "[70] animates emergence".
  09-25 Headless checks live in tests/ (#2), read game.html's sections by
        banner in a Node vm, so they always test what ships; run.mjs has no
        dependencies; screenshot.mjs uses the preinstalled Playwright and a
        cached npm copy of three when the CDN is blocked. Test tooling only,
        not a game dependency · rejected copying game code into tests.
  09-25 Session-assigned claude/* branches are overridden; push to main (see
        How we work) · rejected per-session branches.
  09-25 Match SEED is random per page load, ?seed=N overrides · a fixed seed
        made every AI squad and map identical · rejected fixed SEED.
  09-25 [85] balance harness (#4) plays real AICommanders against each other
        (blind commit, same as live) and reports win rates overall and by
        stance, target priority and unit type, plus fight lengths and
        timeouts. Run with node tests/balance.mjs; [90] doesn't wire it in
        yet · rejected a harness with its own random loadout picker.
  09-25 RushBase (#5) flanks and fires on the move: each leaker heads for a
        wing waypoint (FLANK_Z 110, FLANK_X 60 past midfield; the army splits
        by spawn half) then the base, and shoots the nearest enemy in range
        without stopping or turning aside. Supersedes "never engages enemy
        units". Flank alone left rushing bots at 4% (drones chase them down)
        · rejected return-fire-only (rushers died in the middle), dropping
        RushBase from the AI's picks (hides it).
  09-28 Units stop beelining, MOBA/Clash Royale style, with no fixed lanes:
        (1) 2 turrets per side between midfield and base, placed by the match
        seed, B mirrors A through the centre; ordinary entities (isTurret),
        shoot nearest enemy in range, count as army for AllUnits.
        (2) Aggro + leash for MixedPush/AllUnits: an enemy within sight (50)
        pulls a unit off its goal; the chase ends after 60 from where it
        started, then 2s of no aggro. (3) Threat avoidance for RushBase:
        each tick its heading sums the pull to its goal with pushes away from
        enemy turrets/units whose range it is inside, sliding past on its own
        side. Kept on top of the flank waypoint (without it drone rushers
        fell to 15%) · rejected lanes/bridges (fixed paths kill the vibe).
  09-28 Round clock: a round is 5 minutes (ROUND_TICKS on WorldState); no
        base down by then = draw ([40] matchResult). In the final minute
        every combat unit automatically turns heedless: drops stance, aggro
        and avoidance, targets the enemy base and charges straight at it,
        firing on the move. Clock shown in the camera bar, red in the final
        minute. Replaces the 90s safety cap; the live game had no cap at all
        · rejected player-set "final push" order, most-base-HP-wins timeout.
  09-28 Phones: the 3D view is at most square (height min(70vh, 100vw)) and
        below aspect 1.3 the camera widens its vertical FOV (55 up to 80) so
        the horizontal view doesn't shrink to a zoomed-in slice. Portrait
        phones had this since the start; turret/base close-ups made it show
        · rejected forcing landscape, separate mobile camera shots.

PARKED (not rejected, not now)
  Multiplayer · unit carryover · base upgrades/repair/shields · alternate win
  conditions · terrain gameplay (LOS, cover, slope speed, shots hitting hills)
  · replays · campaign · sound · bespoke custom-unit art · homing projectiles
  · splash falloff curves · human choosing a faction.

NOT YET DESIGNED (take one per design discussion)
  Hit/impact event signaling ([30] -> [70] "a hit landed here") · defensive
  measures (what armor does; interaction with accuracy) · economy (budget,
  income) · base stats (HP, fights back?).

STATUS
  Done: [10] [20] [25] [70]. Partial: [30] (armor not consumed), [40] (base
  destruction only), [50] (Easy AI only), [80] (no HUD/round summary, no
  custom-unit editing), [90] (stands in for [60], single round).
  Done: [85] balance harness (#4), headless via tests/balance.mjs.
  Not started: [60] match flow.
  v17 passes headless checks; NOT YET SEEN IN A BROWSER. Director camera
  (#10) and random battle button (#11) seen working in a browser 09-24.
  Emergence animation removed (#8); checked in headless Chromium, not yet
  by the human.
  Turrets, aggro/leash, threat avoidance (C27), round clock (C28):
  headless only, not yet seen by the human.
  Headless checks in the repo (#2): tests/run.mjs 54 checks pass;
  tests/screenshot.mjs renders a random battle cleanly.

NEXT UP
  1. Look at turrets/aggro/avoidance and the round clock in a browser.
  2. #5 balance: A leads 59/38 with the 5-minute round (see KNOWN ISSUES).
  3. Real [60] match flow: multiple rounds, persistent base HP, income.

KNOWN ISSUES / FACTS
  - Balance (node tests/balance.mjs, 200 matches, seed 1), 5-minute round
    (09-28): A 59%, B 38%, draw 4% (all mutual wipeouts). Decided fights
    median 84s, max 184s, so AI fights rarely reach the heedless minute.
    RushBase is the only stance where B wins most. At the old 90s cap:
    A 23%, B 27%, cap 50%.
  - Shots ignore terrain and can pass through hills.
  - Custom drone units can be created but only the AI side can field them.
  - Side A acts first each tick (small first-mover edge); [40] gives B ties.
  - Budget 100, base HP 500, base x=±130: placeholders. Round 5 min, final
    minute heedless: decided 09-28.
  - SEED is random per page load (logged); ?seed=N replays the AI squad,
    terrain and battle rolls. Random battle's own pick has its own seed.
  - Commander view is hardcoded behind side A.
  - Camera mode toggle lives in the camera bar until the settings panel (#9)
    exists; the choice is not saved between sessions.

SESSION HISTORY (one line each; detail lives in the code comments)
  D01 plan · C01 sim · C02 renderer · C03 commanders · C04 buy UI ·
  C05 target priority · C06 AI orders · D02 custom units · C07 orders UI ·
  C08 bases + win check · D03 camera floor · C09 stances · D04 archetypes ·
  C10 content tables · C11 registry · C12 wiring · C13 stance UI ·
  C14 AI stance · C15 unit creation UI · D05 damage model · C16 projectiles
  in sim · C17 terrain, flight, deploy, factions, renderer rebuild, camera ·
  C18 moved to GitHub (no code changes) · C19 director camera (#10) ·
  C20 random battle button (#11) · C21 removed deploy emergence (#8) ·
  C22 headless checks in tests/ (#2) · C23 random match seed ·
  C24 [85] balance harness (#4) · C25 RushBase flank + fire on the move (#5) ·
  C26 link back to stilotto.github.io at the top of the page ·
  C27 seeded turrets, aggro/leash, RushBase threat avoidance ·
  C28 5-minute round clock, draw on timeout, heedless final minute ·
  C29 phone camera: square view cap + wider FOV on narrow screens.