Files
deepgrain-foundry/docs/agent-log.md
T
44r0n7andClaude Opus 5.5 0a8847594e A loaded world starts around the player first, the rest a few areas a tick (t21)
- Areas within 150 grains of the bot start at once; the others 12 a tick, nearest first, and any
  the bot flies toward start straight away. A Huge world is fully started after 17 s.
- Huge world, first 600 ticks: average 15.2 -> 5.5 ms/tick, worst 79.7 -> 16.2.
- New test/t_load.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:41:57 -04:00

838 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent log
What agents have done, newest first. Add an entry for every piece of work: the date, which
note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-25: t21: a loaded world starts around the player first (Claude Code, Codex)
- Owner: "lets speed up the first load speed. load in areas around the player first".
- `src/sim/save.js`: `deserializeWorld` no longer wakes every area; `startLoadWake()` keeps the
load-time heat scan's result per area, wakes those within 150 grains of the bot, and queues the
rest by distance; `loadWakeTick()` (from `simTick`) wakes 12 a tick and, every 8 ticks, any still
waiting within 150 grains of the bot. The queue is not saved.
- Huge world, ms/tick before -> after: ticks 0-600 average 15.2 -> 5.5, max 79.7 -> 16.2, 95th
percentile 21.0 -> 11.0; ticks 600-1800 average 7.3 -> 10.0 (the same work, later). Every area
has started by tick 1024 (17 s).
- `test/t_load.js` (19 checks). `run_all.py --browser` ALL PASSED.
## 2026-10-24: t21 phase 4: duplicates merged, largest files split (Claude Code, Codex)
- Merged seven real duplicates: wall refunds (`removeWallCell`, walls and molds), tube edges and
lanes (`entTubeEdge`, `entTubeLane`), pipe text, piece dimensions, font lookup, reach circles,
surface averaging (`meanSurface`). Look-alikes left alone: data tables, sprite rows, GLSL, and
the hot movement and heat code.
- Split by topic: `ui/previews.js` -> `preview-drill.js`, `preview-selection.js`;
`render/smooth.js` -> `smooth-shaders.js`; `ui/notebook.js` -> `notebook-market.js`,
`notebook-blueprints.js`; `sim/materials.js` -> `material-tables.js`; `sim/worldgen.js` ->
`worldgen-helpers.js`; `ui/selection.js` -> `selection-pickers.js`, `selection-panels.js`.
`render/world.js` left whole (one large function).
- Every suite's output identical apart from timings; seeds generate the same worlds.
`run_all.py --browser` ALL PASSED.
## 2026-10-24: t21 phase 3: speed-ups that change nothing (Claude Code, Codex)
- Profiled a Huge world (6144x2048) headless and in the browser. After a load every area is
awake: 8-15 ms/tick for the first 600 ticks, settling to about 0.7 ms/tick after 3 minutes. In
the browser, drawing is cheap; the full roof scan every 20 ticks was the largest single item.
- Kept: `updateDirtyRoof()` (`src/sim/ledger.js`) rescans only area columns marked by
`wakeXY`/`wakeHeatXY`/`simMove`/`heatChunk` (`roofDirty`); `updateRoof()` is still the full scan
for setup code. Quiet Huge world 4.60 -> 1.88 ms per scan, digging 4.67 -> 3.48.
`heatChunk` skips `react()` for solids and powders below their first reaction threshold
(`REACT_MIN`). Huge, ms/tick: digging 2.54 -> 2.34, quiet 1.39 -> 1.18.
- Reverted (no consistent gain): inlining `condPair`, one shared part rule for `enter`/`passable`.
- `test/t_sim_optimizations.js`: the dirty roof matches a full scan; `heatChunk` matches the
reference loop in `test/fixtures/heat-chunk.js` byte for byte, random state included (catches a
reaction added to `react()` but not to `REACT_MIN`).
- Every suite's output identical before and after apart from timings; seeds 777 and 4242
generate the same worlds. `run_all.py --browser` ALL PASSED.
## 2026-10-24: t21: piles near the bot sort quickly (Claude Code, Codex)
- Owner: "a pile should sort when the player is nearby. the sorting shouldnt take long anyway."
- `src/sim/cells.js`: `SORTP` 0.003 -> 0.06, `SORTP_FUEL` 0.003 for sinking through burning fuel
(at 0.06 dirt sank through a buried burning wood pile and `t_world`'s charcoal check failed);
`wakeBotPowder()` every 8 ticks from `simTick` wakes areas with loose grains (`powderArea`
hint, kept by `simMove`) within 100 grains of the bot. `worldSteps` settles at 0.003: seeds
unchanged.
- Cost on a Medium dig site: 0.108 -> 0.134 ms/tick. `t_physics` sorting checks: near the bot ore
sorts in 432 ticks then sleeps; far away it stays; sand on dirt never sorts.
- Tests: `run_all.py --browser` ALL PASSED.
## 2026-10-24: t21 phase 2: physics test pass for sand, molten metal and gases (Claude Code, Codex)
- `test/t_physics.js` (41 checks): heaps, digging under a heap, ore sorting through ash, sand
sinking in a pond, world edges; molten copper levelling (slower than water), setting, sinking in
a pond, slag over copper, filling a cavity; smoke rising and fading (longer under a roof), steam
condensing, fire going out, gas in sealed pockets, gas leaving at the top row. Every scenario
checks that its areas go back to sleep.
- Found and fixed: gas stuck in a pocket never finished fading or condensing because its area
slept (`gas()` keeps its area awake: steady cost 1.34 -> 1.37 ms/tick); a rain drop or blown
grain landing on steam deleted that steam's water (`landFlyer`/`depositNear` in
`src/sim/vacuum.js` now send it to the clouds).
- Found, owner decided: a resting pile never sorts. To follow: piles near the bot keep sorting,
and sorting gets quick.
- Tests: `run_all.py --browser` ALL PASSED; seeds 777 and 77 generate the same worlds.
## 2026-10-24: t21 phase 1: water test pass, faster pouring, rain spreads (Claude Code, Codex)
- `test/t_water.js` (20 checks): conservation, levelling and sleep, pools pouring off ledges,
world edges and corners, pumps, freezing and thawing, boiling and condensing, a pool draining
into a hot shaft.
- `liquid()`/`move()` in `src/sim/cells.js`: multi-cell steps (`LSTEP`, scaled by `FLUID`),
a longer reach for top-layer grains (`LFILM`), and waking the row ahead only when a top-layer
grain steps sideways (waking on every liquid move kept 207 of 2048 areas busy after a reload).
`worldSteps` settles with both at 1: seeds 777, 77 and 4242 hash the same as before.
- Found: cooled steam drifting under a roof could stay steam for ever, because its heat area
slept. `gas()` now keeps the heat pass on it.
- `weatherTick()`: raining clouds drift at half pace; rain falls over the band and one either
side. `t_weather`: 'close to where it gathered' now samples drops in the air (landed water
spreads); a new wind check.
- Test fixes, not weakening: `t_conveyors` pit check counts steam on its way up (the water total
was exact); `t_molds` burning-bed check seeds `RS` (its result hung on earlier tests' draws:
0.28-1.04 depending on the sequence, identical on the old and new code).
- Steady cost after a reload (2048x1024): 1.26 -> 1.24 ms/tick. Tests: `run_all.py --browser`
ALL PASSED. Screenshots `shots/t21-pour-120.png`, `shots/t21-pour-900.png`.
## 2026-10-24: t1: bars stack neatly, Rack blocks store them (Claude Code, Codex)
- `232f5af`: `entTick` snap-and-tip (`entSupportCount`, `src/sim/items.js`);
`test/t_bar_stacking.js` (12 bars fill a pit's bottom six rows; fails on the old code).
- Rack blocks: `src/sim/racks.js` (`rackState` per block, FIFO runs, `rackIntake` from `entTick`,
`rackTick` release, `rackRefill`), `BP_RACK`/`RACKB`, F toggle in `interactAt`, removal returns
bars with `giveOrDrop`, saved in meta `racks` and validated on load, shelves overlay, hover,
Machines tab, notebook. `test/t_racks.js` (29 checks), play.py menu check.
- Tests: `run_all.py --browser` ALL PASSED. Screenshot `shots/t1-racks.png`.
## 2026-10-24: Backlog combined and rewritten (Claude Code)
- On the owner's request: t12 (exotic ores) and t31 (choosing claims) merged into t13, now
"Distinct worlds: generation options, planet presets, exotic ores and a choice of claims";
t24 (water fixes) merged into t21, now "Code health: refactor, speed up and test the physics".
t2 and t14 rewritten with subtasks. t12, t24 and t31 archived (`tasks/archive/`), not deleted.
## 2026-10-24: t32: minimap and full map (Claude Code, Codex)
- `src/ui/map.js`: colour cache (`resetMap`, `mapArea`, `updateMap` from the frame loop),
minimap (`#minimap` in `#hud .br`, `SET.minimap`, `SET.minimapZoom`), full map modal `m-map`
(`openMap`, `zoomMap`, drag pan). `input.js`: M map, Shift+M mute. `#cards` moved to `.bc`.
- Frame time on Medium: 2.18 ms with the minimap off, 2.29 ms on.
- Tests: play.py map checks (buttons, zoom setting, cache updates after digging, fog black, M/Esc,
Shift+M, cards in .bc, setting hides it, frame time). `run_all.py --browser` ALL PASSED.
Screenshots `shots/t32-hud.png`, `shots/t32-map.png`, `shots/t32-map-fog.png`.
## 2026-10-24: New worlds get camp names too (Claude Code, Codex)
- The new-world dialog prefills `claimName(seed, existing names)` instead of "World N", and
follows seed edits until the player types their own name; a blank name falls back to it too
(`src/ui/loop.js`, `session.js`). play.py checks updated. All tests pass.
## 2026-10-24: t30: progress while generating, loading and saving (Claude Code, Codex)
- `worldgen.js`: `worldSteps` generator yields `[fraction, label]`; `generateWorld` (sync, tests)
and `generateWorldAsync` (yields every 30 ms) run it. Small and Medium mat/temp/flags hashes
match the old generator exactly.
- `session.js` `showLoading(on, p, label, title)` with `#loadstep`; `GAME.loading` stops the
frame loop's sim and drawing while worlds swap. `loadSavedWorld` for Continue and the world
list; `worldNewest`/`unpackBuffers`/`startFromSave` report progress; travel and new claims too.
`#saving` chip after 150 ms of saving.
- Tests: t_world progress labels and identical worlds; play.py overlay, responsiveness during
async generation, Continue labels, saving chip. All pass except t_smooth half-resolution
(fails under load, as before). Screenshots `shots/t30-loading.png`, `shots/t30-saving.png`.
## 2026-10-24: t3 follow-up: claim names; t30 added (Claude Code, Codex)
- New claims get a camp name from their seed ("Cinder Gulch"; `claimName` in `src/ui/claims.js`,
skipping names already in the world list) instead of "Claim N". The Claims tab shows "Claim N"
beside the name and has a Rename button on every row (reuses `renameWorld`).
- Owner's request added to Todo as t30: progress meters while generating, loading and saving.
- Tests: play.py claims checks extended; t_smooth half-resolution still fails under load.
## 2026-10-24: t3 fix: deep ore burned and melted after a reload (Claude Code)
- Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh.
Cause: reloading wakes every area, and t3's deep coal (ignites at 430°C, natural rock reaches
that at about 70% depth) and gold next to the magma burned and melted forever: about 2,100 of
12,288 areas stayed busy, 65 ms per tick. A fresh world hid it because nothing deep was awake.
- `worldgen.js`: an ore is not placed where `AMB[y]` is within 120°C of its ignition or 200°C of
its melting point; a cooler coal band in stone and granite at 45%+ depth replaces most of the
lost coal. Huge after a reload: 335 areas awake, about 5 ms per tick; Medium 104 (126 before
t3). Medium seed 424242 ore is now about 4.2x the original (coal 3.2x, gold 2.8x).
- `test/t_world.js`: no ore in too-hot rock; a reloaded world settles with no fires and under
10% of areas moving; ore balance ranges updated. Existing saves made with the bug keep it.
## 2026-10-24: t3: a world that lasts (Claude Code, Codex)
- `26d17c2` deeper is richer (`worldgen.js` ore thresholds drop with depth below fy 0.25).
Medium seed 424242 ore grains before -> after: coal 33,493 -> 170,437; copper 26,508 ->
127,298; iron 77,679 -> 361,683; zinc 11,797 -> 53,527; tin 7,723 -> 38,551; silver 3,638 ->
12,166; gold 1,681 -> 5,858; total 162,519 -> 769,520 (4.73x). Shallow ore unchanged. Codex's
first try stretched noise by depth (streaks) and gave gold 26x; redone.
- `4a88254` waste rock: `REFINE` granite/basalt about 2x; granite and basalt bulk orders; prices.
- `56c93ae` Huge 6144x2048 and gzip-packed save buffers (`packBuffers`/`unpackBuffers`, old raw
saves still load). Medium 29.0 MB -> 0.44 MB, Huge 92.8 MB -> 1.36 MB; Huge generation 5.9 s,
save 0.5 s, 0.39 ms per idle tick.
- New claims: `src/ui/claims.js` (career record `career:<id>` in IndexedDB, applied over each
loaded claim; `openNewClaim`, `travel`, Claims tab), `session.js` `setupNewWorld`, `saves.js`.
I switched claims.js to the repo's single quotes. Screenshot `shots/t3-claims.png`.
- Tests: play.py 268 checks and all headless suites pass. t_smooth's half-resolution check
fails under load on this machine; the build before t3 failed it 3 of 3 times under the same
load, so it isn't caused by this work.
## 2026-10-24: t4 follow-ups and t28 Brick press (Claude Code, Codex)
- `9739ca0`: Refiner and Stone cutter code removed; `save.js` skips unknown machine types on load
and refunds a refiner ($120) or cutter ($90). Their old t_pipes/t_stone checks now use blocks.
- `f5e5591`: `drawProcBlocks` (`render/machines.js`) draws Grinder wheels and the Saw blade over a
plain housing texture; turns while busy, no `rnd()`.
- `08d2d65`: `PLANK` item; the Saw cuts `LWOOD` too (`SAW_CUTS`, per-material `cut` counts).
"Stone brick" is "Stone block" in player text. Saw text explains the pile-up under it.
- t28: `BP_PRESS`/`PRESSB`, `GREENB` and `BARBR` items, `FIRETO` (items.js fires a hot green
brick into a Brick). Machines tab, hover, notebook, milestone hint.
- Tests: `run_all.py --browser` passed after each commit (t_smooth's flake rerun alone).
Screenshots `shots/t4-wheels{,-smooth}.png`, `shots/t28-press.png`.
## 2026-10-24: t4: Grinder and Saw hopper blocks (Claude Code, Codex)
- New block pieces `grinder`/`saw` (`BP_GRIND`/`BP_SAW`, materials `GRINDER`/`SAWB`, NMAT 97).
`procBlocksTick` (`src/sim/processors.js`) works the row above each stack top and passes the
result down the stack (`procFeed`), reusing `REFINE` and `CUT_PER_BRICK`; per-block state in
`procState`, rebuilt on place/remove/load. Refiner and Stone cutter are `hidden`; their ticks are
unchanged. Texture in `render/world.js`, hover text in `info.js`, Machines tab, notebook.
- Codex hit its output limit before finishing the tests; I fixed the test setup (a leftover brick
blocked the outlet), kept the old machine icons, stopped "blocked" flickering on idle ticks,
and added the play.py Machines-tab and drag-a-row checks.
- Tests: t_stone all pass, play.py 243 pass; t_smooth's half-resolution check fails inside
run_all at load ~9 and passes alone (known flake). Screenshots `shots/t4-hoppers{,-smooth}.png`.
## 2026-10-24: t25: air streaks for the Pneumatic nozzle (Claude Code, Codex)
- `drawAirStreaks` in `src/ui/previews.js`: 12 teal streaks spiral into the ring while sucking, 10
amber streaks run from the nozzle to the cursor in a cone while blowing. `performance.now()`
only, no `rnd()`; skipped with particles off or in fog. `test/play.py`: drawing both doesn't
change the sim RNG. Also commits the `docs/PROJECT_MAP.md` refresh missed in t27/t26.
- Tests: `run_all.py --browser` passed (play.py 240; t_smooth's half-resolution flake at load 9
passed on rerun). Screenshots `shots/t25-{suck,blow}.png`.
## 2026-10-24: t26: the Vacuum is renamed the Pneumatic nozzle (Claude Code, Codex)
- `TOOLS[1].name`, a title row in the tool card ("Pneumatic nozzle" plus a `width N` chip, moved
from the sub line), and player text in `session.js`, `selection.js`, `notebook.js` and
`progress.js`. Ids, function names and comments still say vacuum.
- Tests: `run_all.py --browser` all passed (play.py 239). Screenshots `shots/t26-card{,-900}.png`.
## 2026-10-24: t27: tool messages as toasts (Claude Code, Codex)
- `setStatus` still records `GAME.status` and now also shows a toast (`err` or `info` class), unless
told to stay quiet (the "New drill bit/bag" F messages, which have their own card). `toast`
restarts the timer of a showing toast with the same text instead of adding another, so per-tick
messages make one toast. The card's status line and its CSS are gone; `doInteract` no longer
toasts twice. Files: `src/ui/{state,selection,actions}.js`, `style.css`, `test/play.py`.
- Tests: `run_all.py --browser` all passed (play.py 237). Screenshot `shots/t27-toasts.png`.
## 2026-10-24: t20 follow-up: bag hint and Clear stay visible (Claude Code)
- The hint and Clear were inside the scrolling `#baglist`, so a long Ctrl list scrolled them away.
They now render into a new `#bagfoot` under the list (`src/shell.html`, `renderBag`); Clear is a
full-width, highlighted "Clear selection" button (`style.css`).
- `test/play.py`: a long Ctrl list scrolls while the hint and a full-width Clear stay visible.
Screenshots `shots/t20c-ctrl{,-900}.png`. Tests: `run_all.py --browser` all passed (play.py 232).
## 2026-10-24: t20 follow-ups: deselect when blown out, Ctrl + wheel, Ctrl+click (Claude Code)
- Blowing the last of a selected material removes it from the selection (`toolTick`); materials
selected before you have any stay. Window `wheel` listener: Ctrl + wheel never zooms the page
and scrolls the scrollable panel under the mouse (the Ctrl bag list). Bag clicks add when
`GAME.ctrl` is set even if the click has no `ctrlKey` (owner had to hold Ctrl+Shift; not
reproduced in headless Chromium, which passes ctrlKey).
- Files: `src/ui/{actions,input,selection,notebook}.js`, `test/play.py` (229 checks).
- Tests: `run_all.py --browser` all passed.
## 2026-10-24: t20 follow-up: one selection, full list while Ctrl is held (Claude Code, Codex)
- `GAME.blow` is now the selection for picking up and blowing out (`vacFilter` -2 = the selection).
`blowList()` returns only selected materials in the bag; no pruning, no auto-select. `GAME.ctrl`
(input.js) makes `renderBag` add a "Not in bag" section of every `seen` material the vacuum can
take; selected but empty materials always show there. Clear button, click-the-only-row, gamepad B.
- Files: `src/ui/{actions,selection,hud,input,state,gamepad,previews,notebook,session}.js`,
`src/sim/{vacuum,progress}.js`, `src/styles/style.css`, `test/play.py`, `test/t_basics.js`.
- Tests: `run_all.py --browser` passed (play.py 226) except `t_smooth` half-resolution at load 8.4,
which passed on rerun. Screenshots `shots/t20b-{noctrl,ctrl,ctrl-900}.png`.
## 2026-10-24: t20: pick up what you click, blow out what's in hand (Claude Code, Codex)
- After discussing options with the owner: one Vacuum, a drag locks onto the material it starts on
(Shift: everything), and `GAME.blow` is "the hand" (click/Q/E, Ctrl+click mix). Empty materials
leave the hand; an empty hand blows nothing; a pickup fills an empty hand. Removed `GAME.collect`,
R marks, Ctrl+right-click, `openVacuumMenu`, `ensureBlow` and the `.vpick` styles.
- Files: `src/ui/{actions,selection,hud,state,previews,build-menu,info,notebook,session}.js`,
`src/sim/{vacuum,progress}.js` (`toolVacuum` returns the first material picked up),
`src/styles/style.css`, `test/play.py`, `test/t_basics.js`.
- Tests: `run_all.py --browser` all passed (play.py 214 checks). Screenshots `shots/t20-*.png`
(empty hand, one item, mix, 900px).
## 2026-10-24: t17 follow-up: no jagged rim, unexplored is black (Claude Code)
- The owner still saw a jagged grey edge. Causes found by sampling pixels: the fade went to 0
(black) while unexplored was drawn (12, 10, 10), leaving a dark stepped rim; the ±40 noise lit
the faintest cells; rays that reached rock already dim dropped to 0 in 2–3 grains.
- `src/render/world.js`: unexplored and off-map are pure black (`OUT`, smooth-look buffers);
partial fog eases into black along a smoothstep, with noise scaled by s·(1−s). `src/sim/fog.js`:
sight into rock fades over 8 grains as a fraction of its brightness.
- `test/play.py`: the three unexplored-colour checks expect black (owner's decision).
`docs/systems/fog.md` updated. Screenshots `shots/fog-dig-black-{pixel,smooth,nolight}.png`.
- Tests: `run_all.py --browser` passed except `t_smooth` once at load 7 (passed on rerun).
## 2026-10-24: t17 follow-up: softer fog edges (Claude Code, Codex)
- The owner found the edges sharp and stepped. `src/sim/fog.js`: sense ring 24 grains with a
12-grain fade (`FOG_SENSE_FADE`), rays fade 8 grains into rock and over the last 16 of range,
surface fade 16, `fogRevealAround` gets a 6-grain soft edge. `src/render/world.js`: partly seen
grains get ±40 of per-grain noise (`noiseB`), so edges look grainy, not stepped.
- `test/t_fog.js`: a point in the fade band is between 0 and 128. `docs/systems/fog.md` updated.
Screenshots `shots/fog-edge-{before,after}{,-nolight}.png` compared.
- Tests: all pass except `t_smooth` half-resolution, which failed 1 of 3 runs with the change and
passed 2 of 2 without, at load 6–12; that check runs with fog off, so the drawing change does
not run there. Fog drawing 0.43 → 0.45 ms.
## 2026-10-24: t17 Fog of war reveals the ground around the bot (Claude Code, Codex)
- `src/sim/fog.js`: `FOG_SENSE = 20`; `fogLook` also reveals every fog cell within 20 grains,
through rock (full to 16, fading to 20). Claude rewrote Codex's per-grain `Math.hypot` loop to
work per fog cell. `fogLook` cost: 0.042 → 0.043 ms (every 4 ticks).
- `test/t_fog.js`: rock 12 grains away is seen, near the edge partly, 25 away still hidden.
- `docs/systems/fog.md`, DESIGN.md decision. Also: `.kandown/kandown_work.md` rule to write
tasks from chat in our own words (no owner labels, dates or quotes); t16/t19 descriptions
rewritten that way; `docs/kandown.md` notes that web-board descriptions were lost.
- Tests: `run_all.py --browser` all pass (220 `play.py` checks). `t_smooth` half-resolution failed
on two runs while the machine was loaded (load 6–10) and passed on the final run; timing
`fogLook` showed the change is not the cause. Screenshot `shots/fog-sense.png` checked.
## 2026-10-24: t19 Vacuum blow all visual (Claude Code, Codex)
- The owner's description was lost in kandown; chosen in chat: bag rows light up and the card
shows it while Shift is held. t18 was deleted at the owner's request (to decide later: what
blow does when the chosen material runs out).
- `hud.js` `blowingAll()` (Shift held and not flying, or blowing everything); `selection.js` card
("Everything · mix of N", no swatches so it stays one line) and bag `.on` rows; `previews.js`
cursor swatches.
- `test/play.py`: Shift preview on and off, flying doesn't preview (the bot is put back after,
or a later gamepad dig check fails). Tests: `run_all.py --browser` all pass (220 `play.py`
checks).
## 2026-10-24: t16 follow-up: Shift blows all, Ctrl+click in the bag, compact menu (Claude Code)
- The owner's subtask "shift to blow all" and two chat requests. Codex was out of credits, so
Claude made the edits.
- `input.js`/`actions.js`: `secondaryPress(shift)` sets `GAME.blowAll`; `hud.js` `blowAllList()`
(loose bag materials, no bars); `previews.js` shows it. `selection.js`: Ctrl+click a bag name
toggles blow, Ctrl+right-click toggles pick-up; the vacuum menu is one `.vrow` per material
(CSS in `style.css`). Text in the card, bag hint and `notebook.js`.
- `test/play.py`: Shift blow-all (replaces the old "Shift still blows only coal" check, per the
owner's change), Ctrl+click and Ctrl+right-click, plain right click does nothing, one line per
menu row at 1280 and 900.
- Tests: `run_all.py --browser` all pass (215 `play.py` checks). Screenshots checked.
## 2026-10-24: t16 Vacuum tool rework, again (Claude Code, Codex)
- The owner: too many modes; chose an F menu with two pick-up states.
- `src/ui/selection.js`: `openVacuumMenu()` (a tile per material with ⬇ Pick up / ⬆ Blow, plus
Pick up everything and Blow one at a time); `GAME.pickMode`, `setPickMode`, `cyclePickMode`,
`GAME.vacLocked` and the 'first' locking removed. Bag rows lose ⬇/⬆ buttons (a ⬇ tag shows
marks). `actions.js`: pick-up is everything unless marked; F with the vacuum opens the menu
when nothing in reach can be used. Card chips open the menu (`build-menu.js`). T and gamepad B
no longer change vacuum modes. Text in `notebook.js`, `progress.js`, `session.js` (Welcome),
`previews.js`; CSS `.vpick`.
- `test/play.py`: mode checks replaced (no marks takes everything, marks, Shift, menu toggles,
F order, card chip, no bag buttons, menu screenshots at 1280 and 900); `test/t_basics.js` name.
- Tests: `run_all.py --browser` all pass (208 `play.py` checks). Codex ran out of credits before
its final test run; Claude fixed one check (it mutated a mold's kind) and the suction line.
## 2026-10-24: t6 Rework the vacuum/blower tool, with t15 (Claude Code, Codex)
- The owner folded t15 (a material menu for the vacuum) into t6's chosen design.
- `src/ui/state.js`, `hud.js`, `actions.js`, `selection.js`: `GAME.pickMode` ('first', 'marked',
'all', saved in localStorage) and `GAME.blow` (a set) replace `dropMat`, `blowMix`,
`matChosen` and the Ticked mix. Pick-up marks stay in `GAME.collect` (same storage key).
- Bag rows with the vacuum: ⬇ pick-up toggle, name (blow only this), ⬆ blow toggle. Tool card:
a pick-up mode chip (T or click cycles) and the blow choice (Q/E). Gamepad button 1 cycles the
mode when nothing else uses it. Text updated in `info.js`, `notebook.js`, `progress.js` (the
copper hint), `src/sim/vacuum.js` (a status line), `docs/DESIGN.md`.
- `test/play.py`: modes, automatic switching, independent marks, mixes, persistence, toggles only
with the vacuum; `test/t_basics.js`: the new status. Screenshots checked at 1280 and 900.
- Tests: `run_all.py --browser` all pass (214 `play.py` checks).
## 2026-10-24: decisions for t1, t4, t6 (Claude Code)
- Options given in chat; the owner chose: t1 neat bar piles plus a Bar rack; t4 Grinder and Saw
blocks replace the Refiner and Stone cutter (old ones keep working); t6 the bag as the vacuum's
control panel with separate pick-up and blow marks. Recorded in `docs/DESIGN.md` and as
subtasks on each task. Nothing built yet.
## 2026-10-24: t8 Fog of war for unexplored areas (Claude Code, Codex)
- Decided with the owner: revealed by line of sight; unseen is black with a soft edge.
- New `src/sim/fog.js` (2×2-grain cells, 0-255, only ever rises): `fogInit` (sky and surface
seen), `fogLook` (256 rays, 48 grains, through air, gas and liquid, 4 grains into rock), called
every 4 ticks from `simTick`. Never read by the simulation.
- `src/sim/save.js`: fog saved; saves without it rebuild it (surface, 12 grains around built
things, the bot's view). `src/sim/world.js`, `worldgen.js`: allocate and initialize.
- `src/render/world.js`: grains scaled by a bilinear fog value; fully unseen grains drawn like
out-of-world grains; no glow from fog; thermal lens fogged too. `overlays.js`: rain and
particles hidden in fog. `src/ui/info.js`: "Unexplored". Setting `fog` (Game tab).
- Browser tests and tools turn fog off (`play.py`, `t_smooth.py`, `shot.py`, `perf.py`,
`playthrough.py`) except the fog checks. New `test/t_fog.js`; `docs/systems/fog.md`.
- Tests: all 12 headless suites, `play.py` (185 checks), `t_smooth` pass. Screenshots checked in
both looks. `tools/perf.py`: drawWorld 0.34 → 0.43 ms with fog on (no change with light off).
## 2026-10-24: t5 Material selection menu (Claude Code, Codex)
- Decided with the owner: F opens it for walls and molds, R keeps cycling.
- `src/ui/selection.js`: `openWallPicker` (a dialog of every wall material: what you have, the
cost for this piece, the heat it stands; owned first, unaffordable dimmed; glass says whether
it is from shards or bars); `pieceCostNow(P, m)` takes a material.
- `src/ui/actions.js`: F with a wall or mold piece opens it. `src/ui/build-menu.js`: the material
chip reads "· R · F" and opens it on click; placement help mentions F. `src/ui/notebook.js`:
Controls and Blueprints text. `src/styles/style.css`: `.wpick`.
- `test/play.py`: F and the chip open it, Brick picks RBRICK, R still cycles, molds have their own
menu and cost, it fits at 1280×800 and 900×700 (screenshots checked).
- Tests: `run_all.py --browser` pass, apart from the `t_smooth` half-resolution flake.
## 2026-10-24: t10 World name box for a new world (Claude Code, Codex)
- `src/shell.html`, `src/styles/style.css`: a Name box across the top of the New world panel.
- `src/ui/loop.js`: opening the panel fills it with the next `World N` and selects it; Enter in
Name or Seed generates. `src/ui/session.js`: `startNewWorld(seed, w, h, name)`.
- `src/ui/saves.js`: `cleanWorldName` (trim, 40 characters, else a fallback), shared with
`renameWorld`.
- `test/play.py`: a typed name reaches the Worlds list and the pause menu; the default and an
all-spaces name both give `World N`. Screenshot `shots/play/new-world.png` checked.
- Tests: `run_all.py --browser` all pass.
## 2026-10-24: t7 Option to turn off smooth pan following the mouse (Claude Code, Codex)
- New setting `cameraLead` (Display: "Camera looks toward the pointer", on by default):
`src/ui/state.js`, `src/ui/settings.js`; `src/ui/loop.js` gives the camera no look-ahead when
it is off (mouse and gamepad).
- `test/play.py`: the toggle saves; with it off the camera stays on the bot with the mouse in a
corner; with it on the camera moves toward the corner.
- Tests: headless suites and `play.py` pass; `t_smooth` half-resolution flake as before.
## 2026-10-24: t9 Script to run the local server for testing (Claude Code, Codex)
- New `tools/serve.py`: builds, serves `dist/` on 127.0.0.1 (port 8000, or the next free one up
to 8010), sends `Cache-Control: no-store`, and rebuilds when `src/` or `build.py` changes; a
failed build is printed and the old page keeps being served.
- `README.md`, `AGENTS.md`, `build.py` docstring, `docs/PROJECT_MAP.md`: point at it.
- Checked by hand: page served with no-store; touching a source file rebuilt it; with the owner's
old server on 8000 it moved to 8001; Ctrl+C stops it. Headless suites and map check pass.
## 2026-10-24: t11 Welcome and first milestone shown again on an older world (Claude Code, Codex)
- Cause: nothing cleared the discovery cards (`#cards`) when a world was entered, so a new
world's Welcome and milestone cards stayed up after loading an older world.
- `src/ui/session.js`: `enterGame` clears the cards, floaters, particles and the status line;
a new world still shows its Welcome card afterwards.
- `test/play.py`: a new world shows Welcome; loading a world from the list shows no cards.
- Tests: `run_all.py --browser` passes (153 `play.py` checks). `t_smooth`'s half-resolution
check flaked (passed and failed on reruns, with and without this change; the machine was busy).
## 2026-10-24: the to-do list moved from NOTES.md to a kandown board (Claude Code)
Request: initialize kandown (https://kandown.dev, 0.52.0) for the project, move all the notes
into it, and document how it works.
- Practised first in a scratch folder: `init --no-open` starts no daemon and makes no sample tasks
when `tasks/` exists; `create` prints only the id and names the file `<id>_<slug>.md`; a
multi-line `report: |` block is read (checked in kandown's parser) and `doctor` accepts it;
`list --json` doesn't include `report`; project instructions in `.kandown/kandown_work.md`
appear in `kandown work` (~1,100 tokens).
- A mistake on the way: `kandown init --help` ran `init` in the vault (it ignores `--help`) and
started a daemon there. Stopped it, and removed the vault's earlier kandown test files as the
owner asked. `docs/kandown.md` warns about it.
- `kandown init --no-open` in the repo. `.kandown/kandown.json`: default columns, Review's
instructions say only the owner moves work to Done, tags and priority shown. `.gitignore`:
`.kandown/kandown.html` (6.5 MB, regenerated).
- 14 notes became tasks t1-t14 (Todo: 6 tasks; Backlog: 5 tagged `discussion`, 3 tagged
`idea`), each with the owner's note word for word under `## Context` (checked by script: 14 of
14). `kandown doctor` is clean.
- `.kandown/kandown_work.md` holds the board rules (from AGENTS.md's old NOTES section);
`docs/kandown.md` explains how kandown works here. `AGENTS.md` (session start, a new "Working
from the task board" section linking both), `CLAUDE.md`, `README.md`, `docs/PROJECT_MAP.md`,
`docs/DESIGN.md` (decision). `NOTES.md` removed.
- `kandown list` and `kandown doctor` checked inside Codex's sandbox: they work.
- Open: the web board (`kandown`) wasn't opened here; the owner uses it.
## 2026-10-24: several worlds, with a Worlds list (Claude Code, with Codex)
Note: "support multiple saves". The owner chose a world list.
- `src/ui/saves.js`: each world has an id and its own `<id>:main` / `<id>:auto` saves, plus an
index record `worlds` (name, seed, size, created, saved, playtime). A save writes the world
and its index entry in one transaction. `GAME.worldId` is the open world.
- Migration: an old single `main` / `auto` save is copied to "World 1", every byte compared,
and only then are the old keys deleted (in the same transaction that writes the index). If
anything fails the old save is kept and a toast says so.
- Title screen: Continue loads the most recently saved world; a Worlds button opens a list
with Load, Rename (trimmed, at most 40 characters, escaped when shown) and Delete (with a
confirmation). New and imported worlds get new ids. Settings' "Delete saves" became "Delete
all worlds" with a confirmation. `shell.html`, `session.js`, `loop.js`, `settings.js`,
`state.js`, `style.css`, `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- `test/play.py` (149 checks): migration of an old save, two worlds listed, load switches,
rename survives a reload, HTML in a name shows as text, long names cut to 40, delete and
cancel, Continue picks the newest, import adds a world. All 11 headless suites pass;
`t_smooth.py` passed in Codex's run and failed its known flaky check in mine.
- Shots: `shots/worlds-{pixel,smooth}.png` and `-900` versions.
- Not committed: a line in `AGENTS.md` about running `kandown work` appeared in the working
tree during this task; it was not part of the task and is left for the owner.
- Open: nothing.
## 2026-10-24: screw tubes carry bars (Claude Code, with Codex)
Note: "how do you transport bars? belts work but tubes do not, should they?" The owner chose
tubes over a separate bar lift.
- `src/sim/items.js`: a bar whose eight cells are all tube cells is in the tube (derived from
its position, so saves need nothing new). On the tube beat it steps along the tube, snaps
into the next block's lane at corners, and leaves a mouth with a launch like grains. A bar
pushed by a belt into a tube's side or back, falling into a down tube or sitting on an up
tube's intake enters when the aligned cells are free. Grains and bars wait for each other.
Gas and liquid don't block a bar (Claude added this after review: smoke near furnaces stalled
bars in Codex's version).
- Text: `pieces.js` tube description, `notebook.js`, `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: `t_conveyors` (55 checks): belt into a lift and out the top, right tube, up-to-right
and right-to-down corners, blocked mouth waits without overlap, grains and a bar share a
lift with sand conserved, save mid-tube, smoke and steam in the tube, melting in a tube,
bars still ride and stop on belts and rest on top of sideways tubes. All 11 headless suites
and `play.py` (131) pass; `t_smooth.py`'s half-resolution check failed on the slow fallback
(known flaky).
- Shots: `tools/scenes/bar-lift.js`, `shots/bar-lift-{300,900}.png`: mold, belt, lift, corner,
belt; the bars arrive at the top.
- Open: nothing.
## 2026-10-23: screw tubes use all their lanes (Claude Code, with Codex)
Note: "maybe the belts can move faster/carry more than the tubes… the tubes only carry one line of
materials on either side." The owner chose to fill every lane; belt speed is unchanged.
- Measured first (`test/t_conveyors.js`, a belt with a sand heap into the side of a 6-block up
tube, 3,000 ticks): 1,459 grains out, lanes `[2940, 0, 0, 10]`; the edge lane was full (one
lane carries at most ~1,500). Spreading only at intake changed little (1,486): belts push
straight into the edge lane without going through `tubeIntake`.
- `src/sim/machine-tick.js`: on each tube beat a grain in a vertical tube may step sideways
within its block to a lane with clearly fewer grains ahead (a strict gap stops shuffling).
`src/sim/conveyors.js`: `tubeSpread`, shared by side intake (far lanes first) and corners.
- After: 2,071 out (92% of the 2,243 the same belt delivers to an open drop), lanes
`[1937, 1116, 486, 872]`, sand conserved. Checks: all four lanes, conservation, at least 90%
of the belt's delivery, a side-fed sideways tube fills more than one row. All 11 headless
suites and `play.py` pass. `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Found on the way: Codex's sandbox (`workspace-write`) blocks network sockets, and headless
Chrome crashes at startup without one (SIGTRAP core dumps in pairs). Codex runs now pass
`-c sandbox_workspace_write.network_access=true`.
- Open: nothing.
## 2026-10-23: the build card describes the build tool, not the piece (Claude Code)
Note: "instead of the current info box for the build tool showing the current building selected,
it should just show a description about the build tool in general." The owner kept the R/F chips.
- `src/ui/selection.js`: the build card reads "Build", then the R/F chips, Replace · G and
ghosts, a line on placing, the build keys and the status line. No piece name or cost.
- `test/play.py`: the card shows "Build" and the placing text, not the piece's name, cost or
description; the other card checks are unchanged. `play.py` and all 11 headless suites pass.
- Shots: `shots/build-card-general-{pixel,smooth}.png`. `docs/DESIGN.md`.
- Open: nothing.
## 2026-10-23: world sounds are heard around the bot, not the view (Claude Code)
Note: "the sound effect is still heard when the player's zoom level is farther out… sounds
shouldnt be heard based on the viewport, but rather in a radius from the player."
- `src/ui/audio.js` `sfxPlace`: gain from the distance to the bot's centre (full within 24
grains, silent at 96), pan from the horizontal offset; zoom no longer matters.
- `test/play.py`: the audio checks use points relative to the bot; a new check zooms out to 2
and plays a visible sizzle 200 grains away: silent (it played before). `play.py` and all 11
headless suites pass. `docs/DESIGN.md`.
- Open: nothing.
## 2026-10-23: F on an ingot mold does nothing and says nothing (Claude Code)
Note: "now that ingot molds empty themselves, F still makes a message…"
- `src/sim/actions.js` `interactAt` passes ingot molds by, so F reaches anything else nearby;
`src/ui/actions.js` shows no "Nothing to use there." when the cursor is on an ingot mold.
- Tests: `t_molds` (F on an ingot mold in every state returns nothing, changes nothing, no
sound) and `play.py` (F over an ingot mold leaves no status and no toast). All 11 headless
suites and `play.py` pass.
- Open: nothing.
## 2026-10-23: pixel-art silo for the pixel look (Claude Code, with Codex)
Note: "the silo art looks the same for both pixelated graphiocs and smooth."
- `src/render/machines.js`: `drawSilo` is the smooth (illustrated) version as before; in the
pixel look `drawSiloPixel` draws it grain by grain: a drum shaded by column with rib rows and
ladder rungs, a cone to the slide gate, legs, the roof hatch with its lid, and the sight glass
filling in the contents colour. Codex's first version read poorly (dark side columns made
the drum look narrow); Claude redrew the grain layout.
- `tools/scenes/silo.js` (run after `clear-sky.js`): three silos, empty, half sand, nearly full
copper ore with the gate open. Shots `shots/silo-*.png` at zoom 6 and 12; smooth shots match
the before shots pixel for pixel. `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: all 11 headless suites and `play.py` (129) pass. `t_smooth.py`'s half-resolution check
failed twice on the slow fallback at load ~5 (known flaky; it passed in the previous task).
- Open: nothing.
## 2026-10-23: world sounds are local (Claude Code, with Codex)
Note: "there is a sound effect that seems to happen randomly but constantly… maybe the sound
effects are not local?" The owner then confirmed a new world is quiet.
- Measured headlessly over 5 game minutes: a fresh world emits no sounds; a lava and water
contact 720+ grains from the bot emitted 2,889 sizzles (about 580 a minute), and `events.js`
played every one at full volume. The sizzle (`heat.js` boiling and quenching) is the only sim
sound with a position.
- `src/ui/audio.js`: `sfxPlace(x, y)` gives gain (1 in view, 0 one view width beyond it) and
pan; `playSfx(name, arg, x, y)` skips silent ones before the rate limit and plays the rest
through a gain and stereo panner (`AU.bus`). `src/ui/events.js` passes positions.
`docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- `test/play.py`: far off-screen is silent, on screen plays, just off-screen is quieter and
panned. All 11 headless suites, `play.py` and `t_smooth.py` pass.
- Checked afterwards with the owner's exported world (seed 622525): over one minute, 48 sizzles
and no other sounds, all from water boiling on naturally hot deep rock (a pool on 580°C basalt
and drips in a shaft and a cave, about 500 grains below the bot). Nothing built there. With
the fix every one of them is silent from the surface.
## 2026-10-23: fewer, smaller messages (Claude Code, with Codex)
Note: "lets change the engot mold and other status messages. they are way too large and just too
many messages…". The owner chose molds, machine warnings and shorter status lines.
- `src/render/overlays.js`: every mold always shows a small percentage (`moldLabel`), gold
`100% · F` for a set bit or tank, red when spoiled or when an ingot mold is blocked
(`mo.blocked`, set in `ejectIngot`, not saved). Details are in the hover panel (`info.js`),
which also carries the furnace-loading advice that F messages used to append.
- `src/ui/events.js`: no cards or toasts for bars dropping, blocked molds or set castings (the
sounds stay). Fan heat is a red ! marker on the fan (`mm.hot`, hover shows °C); melted tubes
and machines destroyed by heat get a short red floating label (`warn` event, coloured
`spawnFloater`) instead of a toast.
- Status lines shortened to one sentence across `src/sim` (molds, actions, blocks, blueprints,
drill, items, machines, tools, undo, vacuum) and `src/ui` (actions, selection).
- Tests: `play.py` records every status line and checks none is over 90 characters (longest
now 55); mold label and no-toast checks. `t_molds` (message lengths, `mo.blocked`), `t_build`
(hot fan: `mm.hot`, no toast), `t_world`. All 11 headless suites and `play.py` pass.
- Shots: `shots/messages-pixel.png`, `shots/messages-smooth.png` (filling, blocked, set bit,
hot fan).
- Open: the first-ever bar drop no longer shows an explaining card; the notebook still explains.
## 2026-10-23: ingot molds need no F; removing a mold returns its metal (Claude Code, with Codex)
Note: "now that the ingot molds drop the ingots, what is the purpose of F…". The owner chose no F
on ingot molds.
- `src/sim/molds.js` `ejectIngot`: when nothing over or in the mold is molten, junk and mixed
metal (as scrap) and plugs over the trough or frozen in the grate are written as loose grains
into the 4 rows under the mold; what doesn't fit waits. Slag and ash are thrown away. After a
drop or a clean-out the grate above is raked. Bars and the short stub work as before.
- `collectMold` on an ingot mold only says "Ingot molds empty themselves." Bit and tank molds
keep F. `removeMold` (every kind) gives the set metal to the bag: whole bars as bars, the rest
as scrap; molten metal is left alone.
- Text: `troughState` flaws, `info.js`, `overlays.js` (no "· F" on ingots), `pieces.js`,
`progress.js`, `notebook.js`, `docs/systems/stone-and-liquids.md`, `docs/DESIGN.md`.
- Tests: `t_molds` 60 checks (F does nothing on ingots; plate, slag and rake cases now
automatic; junk out, mixed metal out as scrap, junk waits when blocked and while molten;
removal returns bars and scrap, leaves molten metal). `furnace_play.js` adapted. All 11
headless suites and `play.py` (122) pass.
- Open: undoing a mold removal brings the mold back empty (its metal stays in the bag).
## 2026-10-23: compact build card top-left (Claude Code, with Codex)
Note: "build tool has no info box like the other tools." The owner chose a compact card.
- `src/ui/selection.js`: the build tool has a card again: Q, piece name, cost (red if short), E;
variant chips, Replace · G and ghosts waiting; one how-to line; the status line. No long
description; that stays in the hover popup.
- `src/ui/build-menu.js`: the `.bb-status` row is gone (its contents are on the card); the
variant chips are shared through `buildVariantChips(P)`. `src/styles/style.css`,
`docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- `test/play.py`: the card is visible while building with name and cost and without the
description; status and Replace · G are read from the card; no `.bb-status` (122 checks pass).
All 11 headless suites pass; `t_smooth.py`'s half-resolution check is still flaky under load.
- Shots: `shots/build-card-*` (belt and wall, both looks, 1280×800 and 900×700) look right.
- Open: at 900px wide the bottom-left hover panel still sits partly under the build bar.
## 2026-10-23: Replace is held G, not Ctrl (Claude Code, with Codex)
Note: "i see the issue was ctrl+w closing the tab… it happens for the bookmark hotkey too. maybe
change the replace key?" The owner chose holding G.
- `src/ui/input.js`: `GAME.ctrl` became `GAME.replace`, set while G is held. Ctrl no longer
does anything on its own, so WASD works while replacing. Ctrl+S, Ctrl+Z, Ctrl+wheel and the
leave-page prompt are unchanged.
- Flag users and text: `previews.js`, `actions.js` ("Hold G to replace it."), `build-menu.js`
(Replace · G chip, key hints), `loop.js`, `state.js`, `notebook.js` controls, `docs/DESIGN.md`.
- `test/play.py`: the replace checks hold G; a new check flies right with D while G is held
(121 checks pass). All 11 headless suites pass. `t_smooth.py`'s half-resolution check failed
on the slow fallback again (known, flaky under load).
- Open: nothing.
## 2026-10-23: Ctrl+W no longer closes the game without asking (Claude Code)
Note: "crash: hoplding crtl and pressing W crashes the whole browser."
- Cause: Ctrl+W is the browser's close-tab shortcut (the whole window if it is the last tab);
holding Ctrl for replace while pressing W to fly hits it. Chrome and Brave reserve it, so the
page never sees the key and cannot cancel it.
- `src/ui/input.js`: a `beforeunload` handler makes the browser ask before leaving while
`GAME.running`. The `visibilitychange` autosave no longer depends on the audio context
existing (it used to return early before any sound had played).
- `test/play.py`: 2 checks (leaving asks while a world is open, not on the title screen); 120
checks pass. All 11 headless suites pass; `t_smooth.py` passed on its rerun (the known flaky
half-resolution check failed once at load ~5).
- Open: the real Ctrl+W prompt can only be seen in a headed browser; the owner checks it.
## 2026-10-23: build menu hover popup, top-left card hidden while building (Claude Code, with Codex)
Notes: "mouse hover reviels a new info popup…" and "dont need the short description in the build
menu…". The owner chose to hide the top-left card entirely while building.
- `src/ui/build-menu.js`: `#bb-pop` above the hovered tile (icon, name, cost, size, power, full
description, how to place, filter help, build keys), clamped to the window; on a gamepad it
shows for the selected tile. The `.bb-tip` row became `.bb-status`: variant (material,
direction, filter) chips, Replace · Ctrl, ghosts waiting, and status messages. The bar now
updates its parts separately so hovering doesn't rebuild the tiles.
- `src/ui/selection.js`: `#selcard` is hidden for the build tool unless copying, cutting or
pasting; the build branch moved to the bar. `src/styles/style.css`: popup and status styles;
on narrow screens the bottom-right panel moves up while building.
- `src/ui/notebook.js`, `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: `play.py` 118 checks (popup on hover, switching tiles, hidden on leave, gamepad,
no `.bb-tip`, card hidden while building and back for tools 1 and 2, status in the bar; the
Ctrl replace check now reads the bar). All 11 headless suites pass. `t_smooth.py`'s
half-resolution check is still flaky under load (~4.8): 1 pass in 3 runs, as before.
- Screenshots in `shots/` (build-popup-*) at 1280×800 and 900×700 in both looks.
- Open: at 900px wide the bottom-left hover-info panel sits partly under the build bar.
## 2026-10-23: a backed-up heap on a belt no longer shears (Claude Code, with Codex)
Note: "belts with a big pile on top only move the grains on the top of the stack…". The owner's
setup: a belt ending in a screw tube going up; material mixed into the bottom layers never moved.
- Cause: `carryColumn` (`src/sim/conveyors.js`) skipped a blocked grain and carried on up the
column, so with the front backed up the top slid over a stuck bottom. Reproduced headlessly
first: over 4,000 ticks, 8,815 shear steps at the tube (1,415 against a wall) and only 57 of
192 bottom sand grains left the belt.
- Fix: on a belt, the walk stops at the first blocked powder grain, so the load above stands
still. On a filter the walk goes on only so chosen materials can still drop through; nothing
above a blocked grain is carried. After: no shear, 119 of 192 sand grains out through the tube.
- `test/t_conveyors.js`: 3 new checks (no shear into an up tube, none against a wall, bottom
layer leaves through the tube). `tools/scenes/belt-heap.js` sets up the owner's case for
`tools/shot.py`; shots at 30 and 900 ticks look right. `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: all 11 headless suites and `play.py` (109) pass. `t_smooth.py` failed its
half-resolution check once at load 6.5 and passed on the rerun.
- Open: `t_smooth.py`'s half-resolution check is flaky on a busy machine (the game drops to
pixel when frames are slow). It is worth making that test immune to the slow fallback.
## 2026-10-23: ingots drop out of the bottom of the mold (Claude Code, with Codex)
Note: "copper bars go into the furnace. also they shouldnt require the player to press F…".
The owner confirmed the first part was a bug: the knock-out threw bars up into the furnace.
- `src/sim/molds.js`: `ejectIngot` runs from `checkMolds` (every 15 ticks). Bars spawn directly
under the mold, only where `entFreeAt` says there is room; the rest wait in the trough. Metal
is taken from the top so the short stub stays on the floor. `ingot_drop` and `ingot_blocked`
events (blocked is said once per spell via `mo.blockedSaid`, not saved). F on a ready ingot
never launches bars upward; F on an empty, short or spoiled mold still cleans and rakes.
- `src/ui/events.js`: first drop gets a card, later drops a toast; blocked gets a toast. Ingots
no longer emit `cast_ready`.
- Text: `pieces.js`, `progress.js`, `notebook.js`, `info.js`, `docs/DESIGN.md` (with the
decision), `docs/systems/stone-and-liquids.md`, `docs/PROJECT_MAP.md`.
- Tests: `t_molds` (38 checks: downward drops, stub kept, top-up, blocked and warned once,
partial drops, full entity pool, slag), `t_basics`, `t_stone`, `furnace_play.js` and
`play.py` section 6 changed to the new behaviour. All 11 headless suites, `play.py` (109) and
`t_smooth.py` (8) pass. A shot of a mold with a belt one block below shows the bars riding off.
- Open: a belt touching the mold's underside counts as blocked; bars need one open block. The
owner may want a belt directly under the mold to work.
## 2026-10-23: Shift+Q/E build tabs, G pick key removed (Claude Code, with Codex)
Notes: "add shift + q/e to move tabs" and "having the "g pick" there is confusing".
- `src/ui/build-menu.js`: `selectBuildCat(c)` (shared with tab clicks) and `cycleBuildCat(d)`;
the menu hint now reads Q/E cycle · Shift+Q/E tab.
- `src/ui/input.js`: Shift+Q/E switch tabs with the build tool; G is unbound.
- Text: build card (`selection.js`), notebook controls (`notebook.js`), `actions.js` comment,
`docs/DESIGN.md`, `docs/PROJECT_MAP.md` (regenerated, `--check` passes).
- `test/play.py`: checks for Shift+Q/E, wrapping, the remembered piece per tab, plain Q/E,
and that G no longer calls pick while middle mouse still does (109 checks).
- Tests: all 11 headless suites and `play.py` pass; a `tools/shot.py` shot of the Moving tab
shows the new hint. `t_smooth.py` passes, but its "half-resolution smooth still draws" check
failed on several runs with the 'slow' pixel fallback while the machine was busy (load ~14).
The previous commit fails the same way under the same load, so this change didn't cause it.
- Open: that check is timing-sensitive on a loaded machine; rerun it when the machine is idle.
## 2026-10-23: browser tests run locally (Claude Code, with Codex)
Request: get the browser tests, Playwright and Chromium working on the owner's Arch machine
(Python 3.14 refuses a system-wide pip install).
- Playwright 1.64.1 and its Chromium 156 (revision 1248) are in an in-repo `.venv/` made with uv.
- Added `tools/use_venv.py` and called it before Playwright imports in `tools/shot.py`,
`tools/perf.py`, `tools/playthrough.py`, `test/play.py` and `test/t_smooth.py`.
The `sys.prefix` guard prevents an exec loop if the venv lacks Playwright.
- Updated `requirements.txt`, `.gitignore`, `README.md`, `AGENTS.md`, `docs/TESTING.md`
and `docs/PROJECT_MAP.md` for Playwright 1.64.1 and venv setup.
- Checks: regenerated the project map and ran `tools/map.py --check`.
- Tests run with plain `python3`: `test/run_all.py --browser` all passed (11 headless suites,
`play.py` 101 checks, `t_smooth.py` 8 checks) on the newer Chromium; `tools/shot.py` shots
of the stone-uses scene look right in both looks. With `.venv` moved away, `play.py` exits
with the setup message instead of a traceback.
- Open: nothing.
## 2026-10-23: export from claude.ai to a local repository (Claude, in claude.ai)
Request: split the source into small files, deduplicate and tidy a little, test fully, and
write the notes, docs and agent instructions for local development.
- **Source split.**
- `core.js` (3,688 lines), `main.js` (2,012) and `render.js` (938) became 52 files in
`src/sim/`, `src/render/` and `src/ui/`, each with a header line. The build order lives in
`src/manifest.json`.
- Checked that the joined code was identical to before, apart from comments and blank lines.
- Every headless suite gave the same output, line for line, before and after.
- Screenshots of two scenes, in both looks, were pixel-identical before and after
(`tools/shot.py --freeze`).
- **Tidying, behaviour unchanged.**
- One scan loop in `simMove` instead of two mirrored copies.
- A `drawHazardRims` helper replaced 3 copies.
- The belt and filter shader code shares `beltLineSD`, `bandRidge` and `bandColor`.
- Removed unused `refundCost`, `wakeArea`, `brushFor`, and the `PARTS` / `PART_BY_ID` aliases.
- The suites share `suite()` (check / done) from the harness, and failures now set the exit
code.
- **Tests.**
- Six broken old scripts were deleted; they used functions removed long ago.
- Experiments and reports moved to `tools/experiments/`.
- `t_v2` / `t_v4` / `t_v9` were renamed `t_basics` / `t_world` / `t_conveyors`.
- Added `test/run_all.py`.
- **Tools.**
- `tools/shot.py`: repeatable screenshots of a scene set up in JavaScript.
- `tools/where.py`: maps built-page line numbers back to source files.
- `tools/map.py`: the project map's index.
- `tools/scenes/`: example scenes.
- The game skips cursor previews when `GAME.hidePreviews` is set, for clean screenshots.
- **World files outside claude.ai.**
- Pause → Export world file only worked inside claude.ai, through its downloads hook.
- It now falls back to an ordinary browser download (`exportWorld` in `src/ui/saves.js`).
- `test/play.py` checks that an exported file downloads and imports back on the title screen
as the same world.
- **Build.** `build.py` writes `dist/index.html` (a full page), `dist/deepgrain-foundry.html`
(the same without the document wrapper, as the claude.ai artifact used) and
`dist/bundle-lines.json`.
- **Docs.** `AGENTS.md`, `CLAUDE.md`, `README.md`, `NOTES.md`, `docs/PROJECT_MAP.md`,
`docs/DESIGN.md`, `docs/TESTING.md`, `docs/systems/*`, `docs/history/requests.md`.
- **Tests run.** All 11 headless suites pass, as do `test/play.py` (101 checks) and
`test/t_smooth.py` (8 checks).
- **Open.**
- The owner reports that big piles on belts still only move at the top (NOTES.md); not yet
reproduced.
- The game's last claude.ai artifact was version 24.