101 KiB
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-27: t88 phase 1, serving and capability checks (Claude Code + Codex)
- The game must be served with COOP/COEP (README has nginx and Caddy lines; opening from disk is
dropped). The title screen checks
crossOriginIsolatedand a WebGPU adapter (CAPS,checkCapabilitiesinsrc/ui/title.js) and shows a notice; warnings only untilREQUIRE_GPU_AND_THREADSis flipped in Phases 2-3. - Browser tests and tools load through
tools/browser.py+tools/server.py, on the real GPU by default (--gpu soft/DEEPGRAIN_GPU=softfor SwiftShader).tools/caps.pyprints isolation and WebGPU per browser;test/firefox_smoke.pyruns inrun_all.py --browserwhen Firefox is installed. Chromium 156: isolated, WebGPU on the RTX 5070. Playwright Firefox 157: isolated, no WebGPU (Linux). - Tests:
python3 test/run_all.py --browserall passed on the real GPU.
2026-10-27: t88 phase 0, profiler and baseline (Claude Code + Codex)
tools/perf.pyrewritten: real GPU (ANGLE Vulkan flags), served by the newtools/server.pywith COOP/COEP (tools/serve.pysends them too), zooms 12/6/4/3/2 x pixel/smooth x scenes settled/factory/heat (tools/perf-scenes.js), drawWorld split byPERF_MARKS, per-step sim timings, 10 s real runs with p50/p95/p99,--json/--compare. Baselinetools/perf-baseline.json. In-game perf readout shows world, light, overlays, move, heat, rest (src/ui/perf.js).- Baseline (1920x1080, RTX 5070): zoom 2 smooth heat scene drawWorld 27.4 ms (colour loop 21.1, light 5.5, upload 0.6); sim 3.7-4.9 ms/tick (simMove 2.0-2.3, heatPass 1.5-2.6); real loop at zoom 2 runs at 30 fps (CPU work p50 30-37 ms), at zoom 6 at 60 fps (7-9 ms).
- Files:
tools/perf.py,tools/server.py,tools/serve.py,tools/perf-measure.js,tools/perf-scenes.js,tools/test_perf.py,tools/perf-baseline.json,src/ui/perf.js,src/render/world.js,src/render/smooth.js,src/ui/loop.js,src/styles/style.css,src/manifest.json,test/t_perf.js,docs/TESTING.md,docs/PROJECT_MAP.md. - Tests:
python3 test/run_all.py --browserall passed. Not measured yet: GPU time (timestamp queries come with WebGPU), Firefox.
2026-10-27: OS pointer hidden over the game (Claude Code)
- The canvas used
cursor: crosshair, so the system pointer showed on top of the game's own aim; it was hidden only while a gamepad was in use. Nowcursor: noneon the canvas always; the gamepad'snocursortoggling is gone. Files:src/styles/style.css,src/ui/input.js,src/ui/gamepad.js.
2026-10-27: t77 identify ores with the cursor (Claude Code + Codex)
- New
src/ui/cursor.js:oreLike,bitNeeded,pingVisible(a ping's cells, while showing),hoveredVein(cached capped flood fill and edges),cursorLabelAlpha(2 s then fade).hoverInfoidentifies pinged cells under fog and says "needs ";drawCursorLabelhonours the new Off / Ores only / Everything setting (SET.cursorLabel, booleans migrated);drawHoveredVeininsrc/render/overlays.jsdraws the outline. - Tests:
test/t_cursor.js,test/t_smooth.py,test/play.py;python3 test/run_all.py --browserall passed. Open: the outline keeps its old shape if the vein is drilled while hovered, until the cursor moves to another cell.
2026-10-27: t75 ping recharge, natural ore colour, glints (Claude Code)
- 10 s recharge (
PING_RECHARGE,pingWait) with a drill panel chip.pingImagedraws veins in their lit colour (base + per-grain noise) with a lighter rim; the ring is dimmer; up to 60 glints per ping flash as the ring passes. Follow-up: veins fade by distance (lit 1.2 s, fade 0.8 s, drawn in 6 alpha bands) anddrawPingRechargeshows an arc around the bot with the drill. Files:src/ui/actions.js,src/ui/selection-panels.js,src/render/overlays.js,src/ui/loop.js. Tests: full suite with --browser passed. The ring and glints were not caught in a screenshot (they finish before the frame is taken).
2026-10-27: bag counted things its list didn't show (Claude Code)
- Owner's exported save: bag total 1,522 but the list showed nothing for 1,000 basalt (from the
dev console's give), 200 clay (source not traced) and 322 molten slag (vacuumed with the Flask;
not in BAG_ORDER).
bagAddnow stores solids as their drill yield;deserializeWorldconverts solids already in a saved bag;bagMaterialsappends anything outside BAG_ORDER; devgivereports what it added and refuses gases and solids with no yield. - Files:
src/sim/items.js,src/sim/save.js,src/ui/hud.js,src/sim/devconsole.js,test/t_basics.js,test/t_dev_console.js. Tests:python3 test/run_all.py --browserall passed.
2026-10-27: t50 resonance ping (Claude Code)
- Right click with the drill:
firePing(src/ui/actions.js) callsresonancePing(src/sim/drill.js), which lists natural vein cells (coal seam and the six ore veins) within 3x the bit's reach.drawPing(src/render/overlays.js) draws an echo ring and the veins inside it over fog, from a one-time offscreen image (one clipped drawImage per frame), fading over 4.5 s. 3 s recharge. - Files:
src/sim/drill.js,src/ui/actions.js,src/render/overlays.js,src/ui/loop.js,src/ui/notebook.js,src/ui/selection-panels.js,test/t_drill.js,docs/PROJECT_MAP.md. - Tests: 4 checks in
test/t_drill.js;python3 test/run_all.py --browserall passed.
2026-10-27: t64 build range about the screen; out-of-range replace ghosts (Claude Code)
setBuildRange/inBuildRangeinsrc/sim/tools.js: the UI sets the view box each frame (src/ui/loop.js); half-sizes clamped to REACH..BUILD_MAX (64..200 grains); with no view (tests) a REACH box around the bot. Used byblockInReach, machine/mold placement and previews, right-click take-back, hover reach, and ghost building for blocks, machines and molds.- A replace (G) placed beyond range stores a replace flag on the ghost (
[code, v, extra, 1]);ghostTick0removes the old piece (refunded) and places the new one (charged), or re-sets a filter's settings, when the bot comes in range. - Files:
src/sim/tools.js,src/sim/blocks.js,src/sim/undo.js,src/ui/actions.js,src/ui/previews.js,src/ui/info.js,src/ui/loop.js,test/t_build.js,docs/DESIGN.md,docs/PROJECT_MAP.md. Tests:python3 test/run_all.py --browserall passed. Not played by hand.
2026-10-27: t49 throughput overlay (Claude Code)
- The I overlay also shows grains per second at each line's open outlet, an orange glow on
transport blocks that are loaded but not moving, and the rate in the hover.
src/sim/flow.jscounts block-edge crossings per carrier block whileflowOn(set by the I toggle) and folds them into a smoothed per-second rate once a second. Hooks:carryColumn,machinesTickscrews,screenNudge, uphill cleat runs (only runs that commit), and the uphill face drag. - Not counted: bars. Not built: money per minute.
- Files:
src/sim/flow.js,src/manifest.json,src/sim/conveyors.js,src/sim/machine-tick.js,src/sim/slope-conveyors.js,src/sim/tick.js,src/render/overlays.js,src/ui/loop.js,src/ui/input.js,src/ui/info.js,src/ui/notebook.js,test/t_flow.js,docs/PROJECT_MAP.md. - Tests:
test/t_flow.js(including a check that counting changes nothing in the simulation);python3 test/run_all.py --browserall passed.
2026-10-27: cooled magma visible and drillable (Claude Code)
- Owner report: bag magma in a brick box left invisible basalt that couldn't be picked up, drilled or built on; melted screws left similar bits. Cause: melts froze into non-natural rock unless the liquid was natural, the drill only breaks natural rock, and the smooth look's terrain field skipped non-natural solids.
src/sim/heat.js: non-metal, non-glass melts freeze into natural rock; bag water into natural ice.src/sim/drill.js:crumblyno longer needs F_NAT;builtCellprotects slope corner cells.src/render/world.js: the terrain field covers any terrain-class solid without a block or machine. Note: the bot still passes through old stuck basalt until it is drilled (it only collides with natural solids).- Tests: 7 checks in
test/t_drill.js;python3 test/run_all.py --browserall passed.
2026-10-27: t67 phase 4, "doesn't connect" marks (Claude Code)
nearMiss(bx, by, end, planned)insrc/sim/joins.js: an end with no join that would join seamlessly, with a diagonal piece involved, one block up or down in empty space. Drop joins count as connected; straight beside straight is never marked. It checks the moved block through a small override view ofplanned, so ghost maps are never copied.- Marks: a red ring with a slash at the end, on built pieces and ghosts (
drawJoinMarksinsrc/render/overlays.js, called every frame fromsrc/ui/loop.js) and at the ends of a drag preview (src/ui/previews.js). Hover says "doesn't connect: place it one block higher/lower" (src/ui/info.js). This covers t58's mark for belts and the same cases for screws. - Tests: 7 nearMiss checks in
test/t_joins.js;python3 test/run_all.py --browserall passed.docs/PROJECT_MAP.mdregenerated.
2026-10-27: t67 phase 3 shared drag placement, and snapping (Claude Code)
dragPlan(P, a, b, shift)insrc/ui/selection.jsgives code, direction, variant, blocks and a planned map for every block piece.primaryReleaseand the drag preview both use it (the duplicatedblockLine(...)expression and the screw/slope-only planned map are gone).- Snapping:
joinSnapinsrc/sim/joins.jsmoves a line one block up or down when its first block would not join but would join seamlessly there with a diagonal piece involved, and every block there is free. Fixes the owner's report that an uphill belt started beside a flat belt on the same level did not join like screws do: it now snaps up to the seamless join. Also covers t58's flat belt level with a downhill low end (snaps down) and a diagonal screw started one block off. Straight beside straight never snaps.joinIsDrop(same-row feed into a slope's low end) is now the one source for the drop, used by snapping and belt drawing. - Files:
src/sim/joins.js,src/ui/selection.js,src/ui/actions.js,src/ui/previews.js,src/render/slope-belts.js,test/t_joins.js,test/t_slope_belts.js. - Tests:
python3 test/run_all.py --browserall passed; 9 snap checks plus a realprimaryReleasecheck. Open: phase 4's "doesn't connect" mark is not built yet.
2026-10-27: t66 dev console (Claude Code + Codex)
- Backtick opens a dev console in any build. The first time in a world it warns that the save
will be marked; accepting saves
dev: trueand adevLog(last 200 commands) in the world, and the worlds list shows a "Dev console" badge. Commands: help, money, debt, give (display or internal name, past the bag limit), bit, bag, unlock, tp (cursor or x y), free on|off, speed, pause, step, weather clear|rain. Command logic is sim-side (devCommandinsrc/sim/devconsole.js); the panel issrc/ui/devconsole.js. - Not built:
time(no clean way to set the time of day) and undo (commands are logged instead). Free building also skipspayCost, so ghosts built later are free too; removing a piece built for free still refunds its normal cost. - Also fixed a flaky build-card check: the wall-material chip markup (
data-wall-picker="") made the selection card rebuild its button every refresh. - Files:
src/sim/devconsole.js,src/ui/devconsole.js,src/styles/devconsole.css,src/shell.html,src/manifest.json,src/sim/items.js,src/sim/save.js,src/sim/world.js,src/ui/input.js,src/ui/loop.js,src/ui/saves.js,src/ui/session.js,src/ui/selection.js,src/ui/notebook.js,src/ui/build-menu.js, testst_dev_console.js,t_dev_console.py,test/run_all.py,docs/PROJECT_MAP.md. - Tests:
python3 test/run_all.py --browserall passed. Built in a parallel worktree; Codex hit its usage limit near the end and Claude finished it (the payCost fix).
2026-10-27: t67 belt drop drawn as two capped ends (Claude Code)
- A flat belt feeding an uphill belt's low end on the same row (and two uphill slopes side by
side) stays joined, but grains really drop about 3 grains onto the face there, so it now
draws as two capped ends instead of the bridge phase 2b drew (it overlapped and stepped
badly).
beltDropinsrc/render/slope-belts.jsdecides it forslopeBeltConnectionsandflatBeltCaps; the shader andworld.jspacking went back to their pre-2b versions. - Tests:
t_joins.jsandt_smooth.pydrop checks updated; the existing sand test confirms the same-row feed carries sand up.python3 test/run_all.py --browserall passed.
2026-10-27: t67 phase 2b, joins follow the travel direction (Claude Code + Codex)
- The owner tested phase 2: horizontal → uphill and vertical-up → diagonal didn't join like the rest. New rule (DESIGN.md): a piece's outlet points at its block plus its travel vector, and the next piece joins when it sits there travelling the same general way. Belts bend it where grains can't follow: an uphill belt's top feeds a flat belt on the same row, and a flat belt one block below an uphill low end still joins.
src/sim/joins.jsreworked around inlets and outlets; screw drawing, belt drawing, screw grain flow and bars follow it. Straight-screw mouths no longer reach a diagonal across a corner; straight-to-straight transfers (lift side-feeding) unchanged.- Files:
src/sim/joins.js,src/sim/conveyors.js,src/sim/items.js,src/render/diagonal-screws.js,src/render/slope-belts.js,src/render/slope-belt-shader.js,src/render/world.js,src/ui/previews.js,tools/scenes/joins.js, testst_joins.js,t_diag_screws.js,t_slope_belt_art.js,t_smooth.py,docs/PROJECT_MAP.md. - Tests:
python3 test/run_all.py --browserall passed. - Open: Codex hit its usage limit before finishing; the belt drop drawing was finished in the next entry. Not playtested.
2026-10-27: t67 phase 2, screw conveyors follow the join rule (Claude Code + Codex)
- Screw drawing, grain flow and bars now all go through
endJoin. New joins: a horizontal in E at a diagonal's low end, and a vertical in E at either end; both draw an elbow that bends inside the straight piece's end block. Same-row high-end elbows are unchanged. - No longer joined (moves nothing): a same-row horizontal at a low end, a vertical directly
above or below a diagonal's last block, and any other side contact. Joined belts and filters
take discharge on their carrying face at either end. A horizontal screw feeding the side of a
vertical lift still works (
straightScrewNextkeeps straight-to-straight transfers). - The opposite-direction check is one helper,
screwAccepts. - Files:
src/render/diagonal-screws.js,src/render/smooth-shaders.js,src/render/world.js,src/ui/previews.js,src/sim/conveyors.js,src/sim/machine-tick.js,src/sim/items.js,src/sim/cells.js,tools/scenes/joins.js(replacest62-joins.js),test/t_diag_screws.js,test/t_smooth.py,docs/PROJECT_MAP.md. - Tests:
python3 test/run_all.py --browserall passed; determinism diff changed only screw checks. Screenshots:shots/joins-*.png. - Open: inside the new elbows the flight stripes of both pieces overlap, which looks a little busy in the smooth look. Layouts built on the removed joins need moving.
2026-10-27: t63 vacuum picks up grains in mid-air (Claude Code + Codex)
toolVacuumnow takes flyers (grains a screw launches or that were blown out) inside its brush and reach before loose cells, with the same pick filter, flask rule, bag-full stop and 48-grain budget.vacuumTargetAtalso considers them. Rain is skipped: it belongs to the clouds, and taking it would make water.- Files:
src/sim/vacuum.js,test/t_basics.js. Built on a separate worktree branch in parallel with t67 and fast-forwarded. - Tests:
python3 test/run_all.py --browserall passed; new checks only in the determinism diff.
2026-10-27: t67 phase 1, shared join module (Claude Code + Codex)
- Owner decision (DESIGN.md, replaces the t62 same-row rule): one join rule for every transport
piece. New DOM-free
src/sim/joins.js:pieceEnds,endJoin,pieceJoins; rule tables are compiled once and joins must be mutual. Belt drawing now uses it:slopeBeltConnections,flatBeltCaps(now also for ghosts), and the pixel filter frame (beltLike(b, end)). Sloped belt drag previews now pass a planned map. - Deliberate drawing changes where the old belt rules disagreed: flat belt caps no longer treat any same-row diagonal screw as joined, and filter frames open only at a real join.
- Files:
src/sim/joins.js,src/manifest.json,src/render/slope-belts.js,src/render/setup.js,src/render/world.js,src/ui/previews.js,test/t_joins.js,test/t_slope_belt_art.js,docs/PROJECT_MAP.md,docs/DESIGN.md. - Tests:
python3 test/run_all.py --browserall passed; t_joins checks 4,232 placements, symmetry, planned maps, and that belt sand flow agrees with the rule. Sim output unchanged. - Open: screws still use their old rules until phase 2.
2026-10-27: t62 corner placements stay separate (Claude Code + Codex)
- Owner tried the corner join and found the jog odd; decision (recorded in DESIGN.md): only
same-row joins connect. Removed join kinds 5/6 and
straightScrewNext;diagonalScrewNextandentDiagonalScrewTicknow continue across a corner only into a diagonal running the same way, so a corner straight piece no longer receives grains or bars. Kept the shared geometry file and the smooth-look elbow seam fix. - Files:
src/render/diagonal-screws.js,src/render/smooth-shaders.js,src/sim/conveyors.js,src/sim/items.js,src/sim/machine-tick.js,test/t_diag_screws.js,test/t_smooth.py,tools/scenes/t62-joins.js,docs/PROJECT_MAP.md,docs/DESIGN.md. - Tests:
python3 test/run_all.py --browserall passed; determinism diff changed only the replaced corner checks.
2026-10-27: t62 screw conveyor joins (Claude Code + Codex)
- Straight screws touching a diagonal's end only at the corner now join, in both looks and in ghosts/previews, and grains and bars cross the joint. Same-row joins were already elbows. Also removed the seam across the top of connected elbows in the smooth look.
- Files: new
src/render/diagonal-screws.js(join and casing geometry moved out ofsrc/render/world.js),src/render/smooth-shaders.js,src/ui/previews.js,src/sim/conveyors.js(straightScrewNext),src/sim/machine-tick.js,src/sim/items.js,src/manifest.json,docs/PROJECT_MAP.md,tools/scenes/t62-joins.js, teststest/t_diag_screws.js,test/t_smooth.py. - Tests:
python3 test/run_all.py --browserall passed; determinism diff showed only timing lines and the new checks. - Open: the corner join doesn't check that the straight piece ends there, so a straight run passing over the diagonal's corner also draws a jog. Not playtested by hand.
2026-10-27: t66 dev console in every build (Claude Code)
- Owner decision: the console ships in production too, with a warning when it's turned on that
the save gets marked (and future achievements won't unlock). Recorded in
docs/DESIGN.md; t66 gained subtasks for the warning, the save mark, and game speed and fun controls. No code changed.
2026-10-27: t66 dev console added to Todo (Claude Code)
- Owner request: a dev console to add money, items and upgrades quickly for testing. Subtasks cover how it opens (and hiding it from real players), money and debt, bag items, bit and bag tiers, other shortcuts, and marking changed worlds. No code changed.
2026-10-27: Save compatibility rule dropped from AGENTS.md (Claude Code)
- Owner: old saves don't matter; only they play, for testing, and can start a new game.
AGENTS.mdstill said to keep saves loading (never renumber ids, add migrations), which contradicted the DESIGN decision "No backward compatibility for now". It now matches. The t65 subtask no longer asks for saves to load.AGENTS.mdis local (gitignored). No code changed.
2026-10-27: t65 Rust conversion added to Todo (Claude Code)
- Owner request: a task to convert the game to Rust for performance, after the base game is more playable (sooner if performance becomes a problem). Subtasks: settle the scope (whole game or the hot sim code; WebAssembly or native), profile for a baseline, keep tests and saves working. No code changed.
2026-10-27: Shift-drag draws a line for transport pieces (Claude Code)
- Owner request: Shift shouldn't fill a rectangle for belts, conveyors and similar. New
lineOnly(code)insrc/sim/pieces.js(belts, filters, screw conveyors, shafts, pipe pieces);primaryRelease(src/ui/actions.js) and the drag preview (src/ui/previews.js) ignore Shift for them, andpieceCode(src/ui/selection.js) no longer flattens a diagonal belt drag on Shift. Build-menu "How to place" and the notebook say so; the notebook's stale "cleats up or down" line now says uphill only. - Tests: play.py checks a Shift-drag with a belt lays a 3-piece sloped line. Full
run_all --browserpasses.
2026-10-27: t61 sticky materials slide freely on built surfaces (Claude Code)
- The owner's video showed clay lumps creeping down a sloped wall; removing a piece was
unrelated. Cause: STICK gates every diagonal step, so cohesive materials crawl on 45°. In
powder()(src/sim/cells.js) a grain resting on a built solid (not F_NAT) now always tries the diagonal; the random draw is unchanged, so generated worlds are identical. - Measured: a clay grain down a 20-piece built slope took 77 ticks, same as sand (before: never arrived in 2000 ticks). Heaps on a built floor and on natural ground are unchanged.
- Test:
t_slopes.jsclay vs sand transit.python3 test/run_all.py --browser: ALL PASSED.
2026-10-27: Unremovable one-cell belt left after cutting slopes (Claude Code)
- The owner's exported save had a lone belt cell no block owned, so nothing could build there or
remove it. It was a sloped belt's seal: a seal may sit in a cell its neighbour leaves open (a
slope's open corner, or under a grate, screw or screen), but removal only let it go when the
neighbour was a sloped wall. Now
blockOwnsCelldecides (src/sim/blocks.js), and loading a world clears belt and screen cells no block or seal owns (clearStraySeals,save.js). - Found with a fuzz of random slope/belt/filter/wall/grate builds removed in random orders and by
cut (0 strays in 4000 runs after the fix). The owner's save loads clean and the spot is
buildable. Tests in
t_slope_belts.jsfail on the old code.run_all --browser: ALL PASSED.
2026-10-27: t42 feedback: downhill belts drop the sidewall too; new tasks (Claude Code)
- Downhill sloped belts now draw as plain belts: no far wall and no ribs, placed or as ghosts or
drag previews (
slopeWallAt, ghost drawing inrender/world.js,previewBlocknow gets the belt direction frompreviews.jsandpreview-selection.js).slopeUphilltakes the direction bits instead of a block. - Test:
t_slope_belt_art.jschecks a downhill run draws no wall or ribs. Drag preview checked headlessly with a counting canvas (downhill draws the band only); placed belts and ghosts checked in screenshots.python3 test/run_all.py --browser: ALL PASSED. Rebuilt dist. - Filed t59 (world longevity audit), t60 (renewable resources, discussion), t61 (sloped walls sticky after removing a piece mid-flow: not reproduced headless, see its report), t62 (screw conveyor horizontal/diagonal joins), t63 (vacuum falling grains).
2026-10-26: t42 feedback: uphill stall at a seam, no cleats downhill, slope pick (Claude Code)
- The owner's long uphill run stalled with a few grains, with a flashing seam between pieces 9
and 10. Cause: each block kept its own cleat phase, so a piece added to a running belt was out
of step; at the seam a rib started one or two cells behind another and pushed its pocket into
that rib's back, stalling the run for good. Every block of a run now takes the phase of the
run's lower-left block (
slopeCleatsTick, memoisedslopeRunKey). - Owner decision: downhill belts have no cleats. They carry like a flat belt (
carryColumn) and draw only the sidewall; ribs, friction drag and pushes are uphill only (slopeUphill).pushRow's downhill levelling is gone. Downhill now 90 grains/s (was 38). - Middle click on a sloped belt's band, where it is drawn into the empty block below, now picks
the belt (
pickUnderCursorfalls back toslopeBeltAt). - Tests:
t_slope_belts.jsadds pieces added 3/7/11/13 ticks later (13 stalled before) and the band pick (fails without the fix); downhill floor back to 60; cleat checks uphill only.t_slope_belt_art.jsdraws ribs on uphill runs.python3 test/run_all.py --browser: ALL PASSED. Screenshots of an up-and-down run in both looks opened. Rebuilt dist.
2026-10-26: t42 feedback: cleats that really hold and push their load (Claude Code)
- The owner saw ribs pass through heavy loads and the load climb as before. Rewrote the slope
physics in
src/sim/slope-conveyors.js: no column carry on slopes; each step every rib moves one face cell and pushes the rows of grains in its way (pushRow, front first, never between two rib cells or into a machine); a contact-layer friction drag that gravity beats on 45°; a boot-section scoop at the foot of uphill runs; a run stalls (all its moves undone) if a rib can't push; grains under a newly placed rib are lifted out. Ribs stop short of built cells. Slopes now step before flat belts (machine-tick.js), so a feeding belt finds the room. - Drawing: ribs drawn by the smooth wall shader at the simulation phase (rubber bars with a
light cap); pixel ribs lighter with a cap; far wall a little lighter (
render/world.js,slope-belt-shader.js,slope-belts.js). - Rates: uphill 78.7 (suite) to 86 grains/s (heavy probe), downhill 38, flat 90. Perf: 576 loaded slopes 1.03 ms/tick; 50 slopes in a generated world 2.19 vs 1.83 ms/tick without.
- Tests:
t_slope_belts.jsadds "no grain ever inside a rib under heavy feed", "a grain on the bare face slides back to a rib, then rides up with it"; downhill rate floor lowered to 30.python3 test/run_all.py --browser: ALL PASSED. Open: downhill rate; t58 (low-end joins).
2026-10-26: t42/t43 drawing and performance follow-up (Codex)
- Filled the far sidewall down to the belt, behind grains, with two-tone pixel corrugation and a continuous smooth 45-degree band. Rubber ribs use the physics mask in all eight phases. Fixed smooth wall seams; matched ghosts, cursor previews and menu icons. Smooth screens retain tinted mesh, now between solid rails without the pixel overlay; loaded-only shake and transport are unchanged.
- Empty cleats only wake nearby load; per-chunk flags bypass mask reads for distant powder.
Empty eight-block slope: 3 -> 0 moving areas after 120 ticks. Uphill sand/gravel/copper ore
71.3 grains/s, coal 71.2; exact sorting retained. Isolated
1f8a89d-> current, median five generated falling-world samples: no slopes 2.006 -> 2.063 ms/tick; 50 slopes 1.952 -> 2.076. Powder probe with cached zero flag: 45.02 -> 50.56 ms (2.14M calls); residual 12 percent microbenchmark overhead, about 3 percent in the generated no-slope world. Existing loaded 576-block stress: 0.515 -> 1.064 ms/tick. slope-cost.js records the reproducible methodology; raw results /tmp/t42-perf-{pre,after}-isolated.txt. Pre-t42 loaded slopes use old grip physics. - Files: sim cells/slope-conveyors/world; render slope-belts/slope-belt-shader/world; UI previews/build-menu; t_slope_belt_art.js, t_smooth.py, slope-cost.js; PROJECT_MAP, DESIGN and t42/t43 reports. Reused enterFree for the ordinary entry rules.
- Final python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 360 play, 16 smooth checks; art suite 57). Before/after suite outputs differ only by five new checks (/tmp/t42-output.diff). Opened shots/t{42,43}-follow-{belt,screen}-{pixel,smooth}-z{4,8}.png; rechecked smooth belt after fixing seams; opened t42-follow-ghosts-{pixel,smooth}-z8.png with both updated menu icons visible. Added sleep, border/load wake, wall geometry, grain colour and smooth grain-class checks. Rebuilt dist, regenerated map; both tasks Review. Owner playtest and hardware GPU appearance remain unverified. No commit.
2026-10-26: t42: sidewall belts with moving cleats (Codex)
- Continued the uncommitted prototype.
sim/slope-conveyors.jssupplies moving barrier lines (8 face cells apart, 6 grains tall), finite periodic pocket capacity and saved phase. Removed slope grip; natural powder movement runs between steps and surplus rolls back. Fixed the shared slope-to-screw mouth boundary so grains cannot fall under its casing. Pixel/smooth drawing uses the actual mask, continuous dark far walls, matching previews/icons. - Files: manifest; sim cells/conveyors/slope-conveyors/machine-tick/items/pieces/blocks/world/save; render slope-belts/slope-belt-shader/world; UI actions/selection/selection-pickers/previews/ build-menu/notebook; slope test suites and play.py; slope-flow/slope-cost experiments, slope-conveyors scene, PROJECT_MAP, DESIGN and t42/t43. Removed both requested scratch files.
- Reproducible
node tools/experiments/slope-flow.js: sand, gravel, copper ore 71.3 and coal 71.2 grains/s uphill, each 90.0 flat; 20-second window after 220 warm-up ticks. Measured settled pile angles 33.7 degrees for all four. Final HEAD -> current stress measurement: 0.486 -> 1.038 ms/tick for 576 loaded slope blocks (1024x512); no-slope powder microbenchmark, 2.14M calls: 45.07 -> 65.55 ms. Median of five samples after warm-up; scripts use DG_HARNESS for the HEAD snapshot in /tmp/t42-baseline. Barriers skip mask reads when none exist, but still have a CPU cost. - Opened shots/t42-belt-{pixel,smooth}-z{4,8}.png. Headless checks pass, including pocket overflow, collision masks, unstamped bare-face sliding, joins, screw mouths, water, bars, save phase, all eight drawing phases and unchanged seed hashes 777/77/4242. Full python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 360 play checks, 15 smooth checks). Rebuilt dist and regenerated PROJECT_MAP. Both tasks moved to Review. The owner still decides whether the lower rate feels right. Pocket movement is a discrete envelope, not a calculation of individual forces. No commit.
2026-10-26: t43: inclined vibrating screens (Codex)
- Diagonal filters always run downhill through all placement/settings/undo/paste paths; R leaves a picked slope unchanged. The picker shows fixed downhill travel rather than two misleading direction buttons. Flat filters retain their original carry. Slope grains get gravity plus one mesh-contact nudge every 4 ticks (15 steps/s); unselected bars use the same cadence. No whole-column carry or between-step grip on slopes.
- Pixel and smooth mesh frames shake a little while loaded, with stable idle drawing. Updated ghosts, icons, build text, notebook and the relevant sim/UI/render files listed in t42 above. Added real browser drag/R/picker checks and art shake checks.
- Both shapes and both sorting modes deliver the complete 64 sand + 64 gravel mix exactly. Generous-feed discharge is 60.0 grains/s down-left and 59.3 down-right; rest passes through. Opened shots/t43-screen-{pixel,smooth}-z{4,8}.png. Headless and smooth suites pass; the first browser run found a hover fixture above the mesh and a later timeout. Corrected the coordinate and reset picked slope state between fixtures. All new browser checks pass. A subsequent run caught an existing 200 ms silo HUD timing race; wait for the expected HUD text before the same assertion. Final full browser run: ALL PASSED, 360 play checks plus 15 smooth checks; all 26 headless suites pass. t43 moved to Review. No uphill-save migration. No commit; owner playtest remains.
2026-10-26: t44: rename screw tubes to screw conveyors (Codex)
- Player text uses Screw conveyor; code uses screw/SCREW, including the piece id, part constants, direction tables, transport helpers, shader variable, build icons and settings access. Numeric ids and save/blueprint versions unchanged; no aliases or migrations added.
- Updated affected files across src/sim, src/render, src/ui, test and tools, plus README, AGENTS and current DESIGN/TESTING/system docs. Regenerated PROJECT_MAP and built dist. Renamed test/t_diag_tubes.js to test/t_diag_screws.js with git mv; no scene filename needed renaming. Past log entries, docs/history and task title/Context preserved.
- All 26 suite outputs saved in /tmp/t44-before and /tmp/t44-after. Raw diff: /tmp/t44-output.diff; name/timing-normalized diff: /tmp/t44-output-normalized.diff (empty). No numeric output differences except ms timings; numeric literals unchanged in all 39 edited JavaScript/Python files. Only tube matches in scoped source/docs are the physical casing description in pieces.js and casing/flight rendering comment in world.js.
- python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 352 play checks, 15 smooth checks). Focused menu, tooltip, hover, notebook, screw blueprint-code round-trip and both starter blueprint-code checks pass in pixel and smooth; no page errors. Opened shots/t44-{menu,hover,notebook}-{pixel,smooth}.png and the play diagonal screenshot. Temporary UI fixture initially lacked SX/SY; corrected to match shot.py before these checks.
- Task t44 moved to Review. Owner playtest remains. No commit.
2026-10-26: t38: show silo contents at the cursor and in a wide window (Codex)
src/ui/info.js: contents were confined to the bottom info plate (hidden below 720 px), omitted from cursor labels, and cone gaps lacked mach ownership. Hover now resolves the full 12x24 outline and shows contents beside the cursor; physical footprints stay unchanged.src/render/machines.js: shared framed three-grain window in both looks, fixed pixel grain noise, visible near-empty row, lifted dark fuel colours, and stencils fitted beside the glass.tools/scenes/silo.jsadds snow and one grain of coal. All 31 accepted powders checked.test/t_silo_contents.js: 5 checks confirm snow intake, identity, water accounting, warm storage and release.test/play.pychecks hover/label at five outline points empty/filled, narrow HUD, fill pixels at zooms 2-12 and stable drawing for all powders in both looks.- Opened and inspected
shots/t38-silos-{pixel,smooth}-z{2,4,8}.png. Updated t38 and DESIGN; regenerated PROJECT_MAP. Finalpython3 test/run_all.py --browser: ALL PASSED (26 headless suites, 352 play checks, 15 smooth checks). Task moved to Review; owner playtest remains.
2026-10-26: t41: distant resonance-drill aim is normal (Codex)
src/ui/preview-drill.js,previews.js: keep tuned fan and light crosshair, add a solid far edge in pulse colour, remove the drill-only red reach circle.info.jsomits drill distance warnings; other tools retain their warning, including beside silo contents.src/sim/drill.js: removed only the distance status; too-hard/cannot-be-drilled wording andtoolDigare unchanged. Notebook drill text remains accurate; gamepad code untouched.test/t_drill.js: new distance regression failed before fix, now passes; two former warning expectations updated intentionally.test/play.py: held mouse aim makes no toast/warning, hard granite still toasts, no red reach circle, gamepad aim stays within reach.- Opened and inspected
shots/t41-drill-beyond-reach.png. Focused browser checks pass. Initial full browser run exposed test timing/paused-input fixture mistakes, corrected without changing gameplay. Finalpython3 test/run_all.py --browser: ALL PASSED (26 headless suites, 352 play checks, 15 smooth checks). Updated t41 and DESIGN; moved to Review. No commit.
2026-10-26: t39: seal touching square-block corners (Codex)
cells.js: six inline checks reject diagonal moves between two PH_SOLID neighbours, with no new random calls.worldgen.jssaves/disables/restores CORNER_SEAL alongside LSTEP/LFILM while settling. No material ids, save fields or piece geometry changed.- Reproduced two further leaks: free flyers (
vacuum.js) and diagonal tube mouths pulling sand between external walls (conveyors.js); blocked those steps only. Horizontal liquid scans already check each intervening cell; vacuum collection removes grains into the bag. Bars resolve ordinary movement by axis. Screw lane transport remains explicit and unchanged. test/t_corner_seal.js: 23 checks, including both staircase orientations for sand/water/steam, all four diagonal intakes, a flyer, world hashes/flag restoration, and an open pile that remains 57x17 with identical material layout and random state when sealing is disabled.- All 25 headless suites pass. Before/after outputs:
t_slopessquare-funnel outflow 379 -> 339 (more sand retained on steps);t_drillore outside the wall 56 -> 55 (solid corners hold ore);t_worldawake chunks after 900 gameplay ticks following reload 148 -> 149 (different settling after corner blocking). Disabling the checks restores drill/world baseline output exactly. Other existing output is identical except timings. Evidence: /tmp/t39-before, /tmp/t39-after. - Full mat/temp/flags hashes match original code for seeds 777/77/4242 at 512x384 and 2048x1024, plus 424242 at 1024x640 (/tmp/t39-world-hashes-{before,after}.txt).
- Factory benchmark at 1024x640: idle 0.69 -> 0.68 ms/tick; 1/4/8 modules 2.05/2.36/2.65 -> 2.13/2.44/2.70 ms/tick. Timing is indicative: settling changes, and this fixture reported zero machine-moved grains, so it does not prove loaded transport performance.
- Rebuilt dist and regenerated PROJECT_MAP. Inspected
shots/t39-corners.pngin pixel look: sand rests along the upper steps and pours through the central outlet. - Final
python3 test/run_all.py --browser: ALL PASSED (25 headless suites, 323 play checks, 15 smooth checks). Updated t39 subtasks/report for Review and recorded the design decision. No commit made; owner playtest remains.
2026-10-25: t40 art pass: sloped belts and filters drawn as one belt (Claude Code, Codex)
- Sloped runs draw as the flat belt (rollers, studs) or filter (tinted mesh) band turned to 45°,
continuous, with elbows into flat belts, filters and tubes and rounded free ends, in both looks
(
src/render/slope-belts.js, newslope-belt-shader.js,world.js,smooth-shaders.js); ghosts and icons match. Drawing only. test/t_slope_belt_art.js;run_all.py --browserALL PASSED.
2026-10-25: t40: sloped belts and filters (Claude Code, Codex)
- Block codes 19-22 (
BP_BELT_UP/DOWN,BP_FILTER_UP/DOWN,src/sim/pieces.js): slope-shaped belts and filters whose 45° face runs, laid by a diagonal Belt or Filter drag.conveyorStep,holdSlopeColumnand a shared front-first order for belts and filters insrc/sim/conveyors.js; bars initems.js; saves, blueprints, undo, hover, previews;src/render/slope-belts.js. - 90 grains/s flat, up and down for belts and filters; a sloped filter sorted 64/64 with no loss. Seeds 777, 77 and 4242 generate identically.
test/t_slope_belts.js(32 checks);run_all.py --browserALL PASSED. Art pass follows.
2026-10-25: t35 follow-up: diagonal tube elbows (Claude Code, Codex)
- Diagonal tubes are as thick as straight ones, and an end block joining a straight tube, belt or
filter draws an elbow at its centre, so the pipes meet centred and edge to edge; free ends get a
collar; ghosts match (
src/render/world.js,smooth-shaders.js,src/ui/previews.js,preview-selection.js). Scenetools/scenes/t35-joins.js. test/t_diag_tubes.js;run_all.py --browserALL PASSED.
2026-10-25: t35/t36 follow-ups: wall drags make slopes, diagonal tube art (Claude Code, Codex)
- Wall piece: a diagonal drag lays slopes (
pieceCode/turnSlopeinsrc/ui/selection.js), T flips a drag or cycles square and four corners for one block; the Sloped wall piece is gone from the menu, slope codes map to the wall piece (src/sim/pieces.js); old slope saves load (test/fixtures/slopes-v2.json.gz). - Diagonal tubes draw as one continuous 45° pipe in both looks, overhanging empty cells at joints
(
src/render/world.js,smooth-shaders.js). Drag ghosts lost the long side arrow; each ghost block keeps its own chevron (src/ui/previews.js). The owner found the diagonal still meets straight tubes off-centre; an elbow fix follows. test/t_slopes.js,t_diag_tubes.js,play.py;run_all.py --browserALL PASSED.
2026-10-25: t37 follow-up: grains on built things cast no shadows (Claude Code, Codex)
- The owner saw a flickering shadow under a grinder fed by a stream: grains resting on its
housing for a moment shaded.
looseForLight(src/render/light.js) now treats a loose stack that rests on anybuiltForLightcell, or ends in air or gas, as see-through. Stacks on natural ground or deeper than 12 cells still shade. test/t_light.js(a grinder stream checked for 180 ticks in both lighting modes);run_all.py --browserALL PASSED.
2026-10-25: t37 follow-up: falling grains cast no shadows (Claude Code, Codex)
- The owner saw a single falling grain darken a column of sky.
onBeltForLightbecamelooseForLight(src/render/light.js): a loose stack that ends in air or gas within 12 cells (falling) is see-through, as are belt and filter loads; stacks resting on any solid or a tube part still shade. test/t_light.js;run_all.py --browserALL PASSED.
2026-10-25: t37 follow-up: belt loads cast no shadows (Claude Code, Codex)
- The owner saw grains riding a belt overhead still shading the ground.
onBeltForLight(i)insrc/render/light.jswalks down through up to 12 loose cells to a belt or filter; such cells are skipped by the light roof and pass light like air.builtForLightis unchanged, so the t33 loose-grain shading still applies to belt loads. Heaps on rock, silos and wall floors still shade. test/t_light.js(56 checks);run_all.py --browserALL PASSED.
2026-10-25: t35: diagonal screw tubes (Claude Code, Codex)
- Parts
P_TUR..P_TUL(11-14),TUBE_PARTS, and anIS_TUBEtable replacing everyP_TU..P_TRrange check (src/sim/material-tables.jsand its users); tube variants 4-7; diagonal drags for tubes; grains insrc/sim/conveyors.js/cells.js, bars initems.js; both looks, previews, hover, copy/paste, undo, blueprints, saves. test/t_diag_tubes.js(85 checks): 120 grains/s straight, up and down over ten blocks; joins, no spills, water passes, bars both ways, controls, persistence. Existing suites unchanged and passing; seeds unchanged.run_all.py --browserALL PASSED. Screenshotsshots/t35-diagonal-*.png, scenetools/scenes/t35-diagonal.js.
2026-10-25: t36: sloped walls and diagonal wall lines (Claude Code, Codex)
BP_SLOPE_BL..BP_SLOPE_TR(15-18) insrc/sim/pieces.js(isSlope,isWall,slopeCell,slopeCode);src/sim/blocks.js: half cost and refund,slopeSealcorner cell andslopeSealOwner,diagonalDragandblockLine(.., diagonal). Build menu, T key (turnSlope), previews, copy/paste, undo, blueprints, hover, both looks.test/t_slopes.js: funnel 400/400 sand (square steps 379/400), water 400/400, no leaks in all four orientations from both sides, bars slide, every wall material; play.py menu and diagonal drag. Seeds unchanged.run_all.py --browserALL PASSED. Screenshotsshots/t36-funnel-*.png, scenetools/scenes/t36-funnel.js.
2026-10-25: t33: loose grains and solid ground look different (Claude Code, Codex)
- Smooth look (
src/render/smooth-shaders.js): natural ground gets a soft seam where it meets loose grains as well as air; packed grains keep the pebble shading and are slightly lighter. Pixel look and thermal lens (drawWorld()insrc/render/world.js): loose grains a touch lighter and speckled, solid cells touching loose grains darker. - Smooth frame 1.08 -> 1.10 ms.
t_smooth.pychecks the seam and the lighter grains;tools/scenes/t33-ground.jsfor screenshots (shots/t33/). - play.py: 'F on an automatic ingot mold stays quiet' counted toasts on screen, and failed
now and then when an older toast expired or a new one arrived in the same 150 ms; it now
records the toasts the key press makes (none).
run_all.py --browserALL PASSED apart from that check, which passes in the new form.
2026-10-25: t37: buildings cast no shadows (Claude Code, Codex)
computeLight()(src/render/light.js): a per-view-column light roof that skips player-built cells (builtForLight: unnatural solids, block, machine or part cells), and built cells pass light like air.drawWorld()stops marking built cells as shading terrain for the smooth look. The simulation'sroofYis untouched (built roofs still condense steam).test/t_light.js; screenshotsshots/t37-before-*.png,shots/t37-after-*.png.run_all.py --browserALL PASSED.
2026-10-25: t34: the nozzle blows no further than it sucks (Claude Code, Codex)
toolBlow()(src/sim/vacuum.js) aims at the capped point fromnozzle()when the cursor is out of reach (thrown bars already did);drawPreviews()draws the blow brush there, in the blow colour rather than the out-of-reach red.test/t_nozzle.js: aiming at 3x and 5x the reach lands sand within reach + 16 grains (brush and settling, measured 15); inside reach it still lands around the cursor. play.py: the preview sits at the capped point.run_all.py --browserALL PASSED. Screenshotshots/t34-blow-out-of-reach.png.
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:deserializeWorldno 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()(fromsimTick) 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 --browserALL 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.jsleft whole (one large function). - Every suite's output identical apart from timings; seeds generate the same worlds.
run_all.py --browserALL 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 bywakeXY/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.heatChunkskipsreact()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 forenter/passable. test/t_sim_optimizations.js: the dirty roof matches a full scan;heatChunkmatches the reference loop intest/fixtures/heat-chunk.jsbyte for byte, random state included (catches a reaction added toreact()but not toREACT_MIN).- Every suite's output identical before and after apart from timings; seeds 777 and 4242
generate the same worlds.
run_all.py --browserALL 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:SORTP0.003 -> 0.06,SORTP_FUEL0.003 for sinking through burning fuel (at 0.06 dirt sank through a buried burning wood pile andt_world's charcoal check failed);wakeBotPowder()every 8 ticks fromsimTickwakes areas with loose grains (powderAreahint, kept bysimMove) within 100 grains of the bot.worldStepssettles at 0.003: seeds unchanged.- Cost on a Medium dig site: 0.108 -> 0.134 ms/tick.
t_physicssorting checks: near the bot ore sorts in 432 ticks then sleeps; far away it stays; sand on dirt never sorts. - Tests:
run_all.py --browserALL 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/depositNearinsrc/sim/vacuum.jsnow 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 --browserALL 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()insrc/sim/cells.js: multi-cell steps (LSTEP, scaled byFLUID), 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).worldStepssettles 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_conveyorspit check counts steam on its way up (the water total was exact);t_moldsburning-bed check seedsRS(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 --browserALL PASSED. Screenshotsshots/t21-pour-120.png,shots/t21-pour-900.png.
2026-10-24: t1: bars stack neatly, Rack blocks store them (Claude Code, Codex)
232f5af:entTicksnap-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(rackStateper block, FIFO runs,rackIntakefromentTick,rackTickrelease,rackRefill),BP_RACK/RACKB, F toggle ininteractAt, removal returns bars withgiveOrDrop, saved in metaracksand validated on load, shelves overlay, hover, Machines tab, notebook.test/t_racks.js(29 checks), play.py menu check. - Tests:
run_all.py --browserALL PASSED. Screenshotshots/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,updateMapfrom the frame loop), minimap (#minimapin#hud .br,SET.minimap,SET.minimapZoom), full map modalm-map(openMap,zoomMap, drag pan).input.js: M map, Shift+M mute.#cardsmoved 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 --browserALL PASSED. Screenshotsshots/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:worldStepsgenerator yields[fraction, label];generateWorld(sync, tests) andgenerateWorldAsync(yields every 30 ms) run it. Small and Medium mat/temp/flags hashes match the old generator exactly.session.jsshowLoading(on, p, label, title)with#loadstep;GAME.loadingstops the frame loop's sim and drawing while worlds swap.loadSavedWorldfor Continue and the world list;worldNewest/unpackBuffers/startFromSavereport progress; travel and new claims too.#savingchip 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";
claimNameinsrc/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 (reusesrenameWorld). - 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 whereAMB[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)
26d17c2deeper is richer (worldgen.jsore 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.4a88254waste rock:REFINEgranite/basalt about 2x; granite and basalt bulk orders; prices.56c93aeHuge 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 recordcareer:<id>in IndexedDB, applied over each loaded claim;openNewClaim,travel, Claims tab),session.jssetupNewWorld,saves.js. I switched claims.js to the repo's single quotes. Screenshotshots/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.jsskips 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, nornd().08d2d65:PLANKitem; the Saw cutsLWOODtoo (SAW_CUTS, per-materialcutcounts). "Stone brick" is "Stone block" in player text. Saw text explains the pile-up under it.- t28:
BP_PRESS/PRESSB,GREENBandBARBRitems,FIRETO(items.js fires a hot green brick into a Brick). Machines tab, hover, notebook, milestone hint. - Tests:
run_all.py --browserpassed after each commit (t_smooth's flake rerun alone). Screenshotsshots/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, materialsGRINDER/SAWB, NMAT 97).procBlocksTick(src/sim/processors.js) works the row above each stack top and passes the result down the stack (procFeed), reusingREFINEandCUT_PER_BRICK; per-block state inprocState, rebuilt on place/remove/load. Refiner and Stone cutter arehidden; their ticks are unchanged. Texture inrender/world.js, hover text ininfo.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)
drawAirStreaksinsrc/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, nornd(); skipped with particles off or in fog.test/play.py: drawing both doesn't change the sim RNG. Also commits thedocs/PROJECT_MAP.mdrefresh missed in t27/t26.- Tests:
run_all.py --browserpassed (play.py 240; t_smooth's half-resolution flake at load 9 passed on rerun). Screenshotsshots/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 awidth Nchip, moved from the sub line), and player text insession.js,selection.js,notebook.jsandprogress.js. Ids, function names and comments still say vacuum.- Tests:
run_all.py --browserall passed (play.py 239). Screenshotsshots/t26-card{,-900}.png.
2026-10-24: t27: tool messages as toasts (Claude Code, Codex)
setStatusstill recordsGAME.statusand now also shows a toast (errorinfoclass), unless told to stay quiet (the "New drill bit/bag" F messages, which have their own card).toastrestarts 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;doInteractno longer toasts twice. Files:src/ui/{state,selection,actions}.js,style.css,test/play.py.- Tests:
run_all.py --browserall passed (play.py 237). Screenshotshots/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#bagfootunder 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. Screenshotsshots/t20c-ctrl{,-900}.png. Tests:run_all.py --browserall 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. Windowwheellistener: Ctrl + wheel never zooms the page and scrolls the scrollable panel under the mouse (the Ctrl bag list). Bag clicks add whenGAME.ctrlis set even if the click has noctrlKey(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 --browserall passed.
2026-10-24: t20 follow-up: one selection, full list while Ctrl is held (Claude Code, Codex)
GAME.blowis 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) makesrenderBagadd a "Not in bag" section of everyseenmaterial 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 --browserpassed (play.py 226) exceptt_smoothhalf-resolution at load 8.4, which passed on rerun. Screenshotsshots/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.blowis "the hand" (click/Q/E, Ctrl+click mix). Empty materials leave the hand; an empty hand blows nothing; a pickup fills an empty hand. RemovedGAME.collect, R marks, Ctrl+right-click,openVacuumMenu,ensureBlowand the.vpickstyles. - Files:
src/ui/{actions,selection,hud,state,previews,build-menu,info,notebook,session}.js,src/sim/{vacuum,progress}.js(toolVacuumreturns the first material picked up),src/styles/style.css,test/play.py,test/t_basics.js. - Tests:
run_all.py --browserall passed (play.py 214 checks). Screenshotsshots/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.mdupdated. Screenshotsshots/fog-dig-black-{pixel,smooth,nolight}.png.- Tests:
run_all.py --browserpassed exceptt_smoothonce 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,fogRevealAroundgets 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.mdupdated. Screenshotsshots/fog-edge-{before,after}{,-nolight}.pngcompared.- Tests: all pass except
t_smoothhalf-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;fogLookalso reveals every fog cell within 20 grains, through rock (full to 16, fading to 20). Claude rewrote Codex's per-grainMath.hypotloop to work per fog cell.fogLookcost: 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.mdrule to write tasks from chat in our own words (no owner labels, dates or quotes); t16/t19 descriptions rewritten that way;docs/kandown.mdnotes that web-board descriptions were lost.- Tests:
run_all.py --browserall pass (220play.pychecks).t_smoothhalf-resolution failed on two runs while the machine was loaded (load 6–10) and passed on the final run; timingfogLookshowed the change is not the cause. Screenshotshots/fog-sense.pngchecked.
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.jsblowingAll()(Shift held and not flying, or blowing everything);selection.jscard ("Everything · mix of N", no swatches so it stays one line) and bag.onrows;previews.jscursor 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 --browserall pass (220play.pychecks).
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)setsGAME.blowAll;hud.jsblowAllList()(loose bag materials, no bars);previews.jsshows it.selection.js: Ctrl+click a bag name toggles blow, Ctrl+right-click toggles pick-up; the vacuum menu is one.vrowper material (CSS instyle.css). Text in the card, bag hint andnotebook.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 --browserall pass (215play.pychecks). 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.vacLockedand 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 innotebook.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.jsname.- Tests:
run_all.py --browserall pass (208play.pychecks). 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) andGAME.blow(a set) replacedropMat,blowMix,matChosenand the Ticked mix. Pick-up marks stay inGAME.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 --browserall pass (214play.pychecks).
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.mdand 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 fromsimTick. 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". Settingfog(Game tab).- Browser tests and tools turn fog off (
play.py,t_smooth.py,shot.py,perf.py,playthrough.py) except the fog checks. Newtest/t_fog.js;docs/systems/fog.md. - Tests: all 12 headless suites,
play.py(185 checks),t_smoothpass. 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 --browserpass, apart from thet_smoothhalf-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 nextWorld Nand 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 withrenameWorld.test/play.py: a typed name reaches the Worlds list and the pause menu; the default and an all-spaces name both giveWorld N. Screenshotshots/play/new-world.pngchecked.- Tests:
run_all.py --browserall 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.jsgives 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.pypass;t_smoothhalf-resolution flake as before.
2026-10-24: t9 Script to run the local server for testing (Claude Code, Codex)
- New
tools/serve.py: builds, servesdist/on 127.0.0.1 (port 8000, or the next free one up to 8010), sendsCache-Control: no-store, and rebuilds whensrc/orbuild.pychanges; a failed build is printed and the old page keeps being served. README.md,AGENTS.md,build.pydocstring,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:enterGameclears 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 --browserpasses (153play.pychecks).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-openstarts no daemon and makes no sample tasks whentasks/exists;createprints only the id and names the file<id>_<slug>.md; a multi-linereport: |block is read (checked in kandown's parser) anddoctoraccepts it;list --jsondoesn't includereport; project instructions in.kandown/kandown_work.mdappear inkandown work(~1,100 tokens). - A mistake on the way:
kandown init --helpraninitin 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.mdwarns about it. kandown init --no-openin 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 taggedidea), each with the owner's note word for word under## Context(checked by script: 14 of 14).kandown doctoris clean. .kandown/kandown_work.mdholds the board rules (from AGENTS.md's old NOTES section);docs/kandown.mdexplains 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.mdremoved.kandown listandkandown doctorchecked 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>:autosaves, plus an index recordworlds(name, seed, size, created, saved, playtime). A save writes the world and its index entry in one transaction.GAME.worldIdis the open world.- Migration: an old single
main/autosave 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.pypassed in Codex's run and failed its known flaky check in mine.- Shots:
shots/worlds-{pixel,smooth}.pngand-900versions. - Not committed: a line in
AGENTS.mdabout runningkandown workappeared 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.jstube 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 andplay.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 throughtubeIntake. 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 andplay.pypass.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.pyand 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.jssfxPlace: 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.pyand 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.jsinteractAtpasses ingot molds by, so F reaches anything else nearby;src/ui/actions.jsshows 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) andplay.py(F over an ingot mold leaves no status and no toast). All 11 headless suites andplay.pypass. - 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:drawSilois the smooth (illustrated) version as before; in the pixel lookdrawSiloPixeldraws 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 afterclear-sky.js): three silos, empty, half sand, nearly full copper ore with the gate open. Shotsshots/silo-*.pngat 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.jsplayed every one at full volume. The sizzle (heat.jsboiling 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.jspasses 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.pyandt_smooth.pypass.- 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), gold100% · Ffor a set bit or tank, red when spoiled or when an ingot mold is blocked (mo.blocked, set inejectIngot, 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 (warnevent, colouredspawnFloater) instead of a toast.- Status lines shortened to one sentence across
src/sim(molds, actions, blocks, blueprints, drill, items, machines, tools, undo, vacuum) andsrc/ui(actions, selection). - Tests:
play.pyrecords 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 andplay.pypass. - 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.jsejectIngot: 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.collectMoldon 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:
troughStateflaws,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_molds60 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.jsadapted. All 11 headless suites andplay.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-statusrow is gone (its contents are on the card); the variant chips are shared throughbuildVariantChips(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.ctrlbecameGAME.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.jscontrols,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: abeforeunloadhandler makes the browser ask before leaving whileGAME.running. Thevisibilitychangeautosave 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.pypassed 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-popabove 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-tiprow 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:#selcardis 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.py118 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.jssets up the owner's case fortools/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.pyfailed 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:ejectIngotruns fromcheckMolds(every 15 ticks). Bars spawn directly under the mold, only whereentFreeAtsays there is room; the rest wait in the trough. Metal is taken from the top so the short stub stays on the floor.ingot_dropandingot_blockedevents (blocked is said once per spell viamo.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 emitcast_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.jsandplay.pysection 6 changed to the new behaviour. All 11 headless suites,play.py(109) andt_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) andcycleBuildCat(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.jscomment,docs/DESIGN.md,docs/PROJECT_MAP.md(regenerated,--checkpasses). 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.pypass; atools/shot.pyshot of the Moving tab shows the new hint.t_smooth.pypasses, 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.pyand called it before Playwright imports intools/shot.py,tools/perf.py,tools/playthrough.py,test/play.pyandtest/t_smooth.py. Thesys.prefixguard prevents an exec loop if the venv lacks Playwright. -
Updated
requirements.txt,.gitignore,README.md,AGENTS.md,docs/TESTING.mdanddocs/PROJECT_MAP.mdfor 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 --browserall passed (11 headless suites,play.py101 checks,t_smooth.py8 checks) on the newer Chromium;tools/shot.pyshots of the stone-uses scene look right in both looks. With.venvmoved away,play.pyexits 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) andrender.js(938) became 52 files insrc/sim/,src/render/andsrc/ui/, each with a header line. The build order lives insrc/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
simMoveinstead of two mirrored copies. - A
drawHazardRimshelper replaced 3 copies. - The belt and filter shader code shares
beltLineSD,bandRidgeandbandColor. - Removed unused
refundCost,wakeArea,brushFor, and thePARTS/PART_BY_IDaliases. - The suites share
suite()(check / done) from the harness, and failures now set the exit code.
- One scan loop in
- 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_v9were renamedt_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.hidePreviewsis 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 (
exportWorldinsrc/ui/saves.js). test/play.pychecks that an exported file downloads and imports back on the title screen as the same world.
- Build.
build.pywritesdist/index.html(a full page),dist/deepgrain-foundry.html(the same without the document wrapper, as the claude.ai artifact used) anddist/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) andtest/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.