Deepgrain Foundry: move from a claude.ai artifact to a local repository

The game as of artifact version 24, ready for local development with
coding agents.

- Source split from three large files into 52 small ones under src/sim,
  src/render and src/ui, joined in the order in src/manifest.json by
  build.py. The joined code is unchanged apart from comments; every
  headless suite gives the same output, and screenshots in both looks
  are pixel-identical to before.
- Light tidying with no change in behaviour: one movement scan loop,
  shared hazard-stripe and belt shader helpers, unused code removed, a
  shared check helper for the test suites.
- World files can be exported as an ordinary download outside claude.ai.
- Tests: broken old scripts removed, experiments moved to tools/,
  test/run_all.py added. Tools: shot.py (repeatable screenshots),
  where.py, map.py.
- Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox),
  docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the
  owner's requests and an agent log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
This commit is contained in:
2026-10-23 16:05:31 -04:00
co-authored by Claude Opus 5.5
commit 3f971a94c2
100 changed files with 11928 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
# Design
What Deepgrain Foundry is meant to be, how its systems fit together, and the decisions made so
far with their reasons. Read the relevant part before changing how the game plays. When the owner
decides something new, add it under "Decisions" with the date.
## What the owner wants (stated by the owner)
- A **Sandustry-style factory and automation game**. Every cell is a fine falling-sand grain
(one or two screen pixels at normal zoom), not a Terraria-size tile. The world is a
Terraria-like side view with dark caves.
- **Strictly a factory game.** No RPG elements, no enemies, and no character items besides the
dig and build tools.
- **Real physics, and player-built machines** instead of magic converter buildings. Furnaces are
built from blocks, metal is cast in molds, and heat, air and gravity do the work. Physics
hazards (heat, molten metal) damage builds only.
- **Loose material falls; solids hold.**
- **Copy and paste of builds.** Pasting places ghosts that the bot builds from the bag.
- **Tool upgrades are earned in a satisfying, not tedious way**, not bought with arbitrary
credits. So far that means casting them: a drill bit in a Bit mold, a bigger bag in a Tank mold.
The owner wants the whole upgrade system redesigned (see NOTES.md).
- **A hovering bot that flies freely.** A limited bag that can be upgraded.
- **An economy instead of a rocket or other final goal.**
- Sell materials at the trade dock; refined goods and alloys pay more.
- Spend the money on buildings.
- Pay off the $20,000 land claim, then keep growing.
- A milestone ledger marks progress.
- **Machines run without power**, for simplicity. The power code (water wheel, shaft, steam
engine) is still there but switched off (`POWER_FREE` in `src/sim/pieces.js`). Those pieces are
hidden from the menu, and old saves keep them.
- **Look.** Not pixel art. The default is the "smooth look": a smooth, illustrated style with
soft, clay-like lighting, drawn by WebGL2. The pixel look stays as an option and is what tests
use.
- **A water cycle, not springs.** Steam rises into clouds and comes back as rain, or as snow
where it is freezing.
- **Sound.** Generated ambient music that changes with depth, and retro chiptune effects.
- **No external assets.** Everything is drawn and synthesized in code.
- **Controls and settings.** Modern keyboard and mouse, plus gamepad support, and plenty of
graphics and display settings.
- **Platform.** A web game for now, built and tested by agents locally. A Rust version may come
much later, for performance; keeping `src/sim/` free of the DOM keeps that open.
## The core loop
1. Dig with the resonance drill. Rock shatters into loose grains: rubble, ore, coal, clay, sand.
2. Vacuum up what you want (tick materials to take only those), and blow it out where you need it.
3. Build a furnace from wall blocks, with a grate floor over a mold. Charge it with ore and coal,
light it, and let the molten metal run into the mold.
4. Knock out bars and sell them at the trade dock, or fill buyers' orders, which pay more.
5. Better bits (copper, then iron, then steel) open harder rock: granite, then basalt. Deeper
ores give tin, zinc, silver and gold, and alloys (bronze, brass) pay more.
6. Automate with belts, screw tubes, filters, fans, pipes and machines. Pay off the claim.
## Systems in brief
Details and numbers live in the code and in `docs/systems/`.
- **World:**
- sizes 2048×1024, 3072×1280 or 4096×1536 grains;
- layers: soil, stone, granite and basalt over a magma sea;
- ores by depth:
- coal, copper and clay near the top;
- tin in the upper stone;
- iron from about a quarter of the way down;
- zinc in the middle (stone and granite);
- silver deep in the granite;
- gold deepest (granite and basalt);
- ambient temperature falls with height and rises with depth.
- **Materials:** 95 material ids, counting air and a few used for built parts. Each is in a
phase: solid, powder, liquid, gas, or item (bars).
- Heat flows between cells. Things melt, freeze, boil, burn and react.
- Fire needs air: a buried fire chokes, and a fan blowing in makes it hotter.
- Coal burns at about 1350°C in still air and about 2000°C with a fan.
- **Drill:** see `docs/systems/dig-tools.md`. Pulse widths, tuning to one material, hardness by
bit tier.
- **Building:** a 4×4-grain block grid.
- Blocks: walls of any of several materials, belts, screw tubes, filters, grates, gates,
pipes, pumps, outlets.
- Machines: fan, Refiner, Stone cutter, Silo.
- Molds: ingot, bit, tank.
- Build actions: drag for lines, Shift-drag for rectangles, Ctrl to replace, hold C / X and
drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse (or G) to pick.
- **Economy:**
- The trade dock pays for anything dropped through its grate, at the market price.
- Prices dip when you sell a lot of one thing and recover over minutes.
- Up to 3 orders at a time pay well over market. Builders' orders want bulk rubble, gravel,
sand or stone bricks.
- You start with $250 and $20,000 of claim debt.
- **Weather:** steam in the open sky becomes clouds. They drift and rain, or snow when cold.
The world's water is counted, so rain only returns what evaporated.
- **Waste rock and liquids:** see `docs/systems/stone-and-liquids.md`. It covers the Refiner,
pipes, the silo, glass bricks, the stone cutter and builders' orders.
## Decisions
The newest are at the bottom. The dates are when the decision was made or built.
**Up to 2026-10-19** (early design, stated by the owner): everything in "What the owner wants"
above, including:
- the economy replacing a rocket goal;
- machines without power;
- the smooth illustrated look;
- the water cycle instead of springs;
- testing in the pixel look unless graphics are being checked.
**2026-10-19 / 20, dig tools.** The resonance drill replaced the old drill. See
`docs/systems/dig-tools.md`.
**2026-10-22:**
- **Rock too hard for your bit never crumbles, however small.** Crumbling it to ore would give ore
without the right bit and confuse players. Crumbling it to stone would lose ore. Instead, world
generation makes no tiny hard specks: pieces of 30 grains or fewer become the rock around them,
and older saves are cleaned once on load. Iron and zinc veins are fewer and chunkier, with the
same total ore.
- Belts and filters next to each other look like one belt. Belt ends are square with rounded
corners.
- Dragging belts or filters sets their direction from the drag. R sets it when placing without
dragging. An arrow shows the direction while dragging, and the selection card names it.
- **Copy and cut** are held keys, not a tool. Hold C or X and drag a box; it acts on release.
Copy and cut look different. Cut is destructive, but it can be pasted back or undone.
- **Ctrl+Z undoes building** (up to 80 steps). The bot building ghosts is never a step of its own.
- **Holding Ctrl** while placing replaces what is built there.
- **Fans melt like copper (1085°C).** Put the fan outside the furnace, blowing in through a grate.
The game warns when a fan gets close to melting.
- **The vacuum takes only ticked materials** when anything is ticked, even if the drag starts on
something else.
- **The Refiner** grinds stone, granite and basalt rubble, gravel, sand and dirt for trace ore,
roughly halving the waste. Ore and anything else it cannot grind drop straight through.
- **Pipes, pumps and outlets** move liquids up to 500°C, one liquid per line. A line is stored as
a count, not simulated grains. Water in pipes counts toward the world's water, so the weather
does not replace it.
**2026-10-23:**
- **Screw tubes carry no more than belts:** they move every other tick. For more throughput,
build more lines.
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner
reports this still misbehaves (see NOTES.md).
- **Pick** (middle mouse, also G for now) selects the piece under the cursor with its direction,
wall material and filter settings. The owner wants G dropped (see NOTES.md).
- **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving,
Liquids, Machines, Molds. The owner wants more changes (see NOTES.md).
- **No length limit when dragging.** Blocks out of the bot's reach become ghosts that it builds
when it gets there.
- **Pipes are thin:** sand, water and anything else loose passes them. Pumps and outlets are
solid.
- **Uses for waste rock and sand:**
- glass bricks (sand melted with a fan furnace, cast in an Ingot mold);
- a Silo for storing one loose material, which should look like a real silo, not a magic
black hole;
- builders' orders for bulk rubble, gravel, sand and stone bricks;
- a Stone cutter that makes stone bricks, which build cut-stone walls.
- **Rubble sells for a pittance** ($0.02 a grain, gravel $0.03). Dirt still earns nothing.
- **Re-refining sand stays allowed.** Refiner sand can be refined again, even in stacked
refiners until none is left. The owner's reason: it is less profitable than making glass, so
players will do it only when they need more ore rather than more money.
- The project moved from a claude.ai artifact to local development with coding agents. It is
built from small files, with `NOTES.md` as the owner's to-do inbox.
## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble.
- **Upgrade ideas for the drill**: an amplifier, a tuning fork, harmonics. Also a Grav pick
upgrade tool and mining charges. See `docs/systems/dig-tools.md`.
- The owner's ideas for later are in the "Future features and ideas" section of `NOTES.md`.
+469
View File
@@ -0,0 +1,469 @@
# Project map
Where everything is and how the parts connect. The index at the end lists every source file in
build order, with its top-level functions and variables. It is generated: run
`python3 tools/map.py` after adding, moving or renaming files or top-level functions. Edit this
top part by hand when the structure changes.
## Layout
```
AGENTS.md, CLAUDE.md instructions for coding agents
NOTES.md the owner's to-do inbox
build.py joins src/ into dist/index.html (one self-contained page)
src/manifest.json the build order: simulation files, then browser files
src/shell.html page markup (HUD, menus, dialogs); CSS and JS are put in at the markers
src/styles/style.css all styles
src/sim/ the simulation. No DOM. The headless tests load only these files.
src/render/ drawing: the pixel look (CPU) and the smooth look (WebGL2), overlays
src/ui/ the browser shell: input, HUD, build menu, notebook, settings, saves,
audio, the frame loop
test/ headless suites (t_*.js) and their harness; browser tests (play.py,
t_smooth.py); run_all.py
tools/ shot.py (screenshots), where.py, map.py, perf.py, playthrough.py,
scenes/ (screenshot setups), experiments/ (measurements and reports)
docs/ this map, DESIGN.md, TESTING.md, systems/ (notes per system),
history/ (the owner's requests), agent-log.md
dist/, shots/ build output and screenshots (not committed)
```
## How it runs
- **One script.** All files share one global scope; there is no import/export.
- A function may call functions in any file.
- Top-level `const`/`let` must be declared before top-level code that uses it at load time.
That is why the manifest order matters: sim, then render, then ui.
- `'use strict'` at the top of `src/sim/base.js` covers everything.
- **The frame loop** (`frame` in `src/ui/loop.js`):
1. read input;
2. run up to `SET.maxSteps` simulation ticks, at 60 ticks a second of game time (none while
paused);
3. draw the world (`drawWorld`) and the overlays;
4. update the HUD.
- **One tick** (`simTick` in `src/sim/tick.js`), in this order:
- update the sky roof line (every 20 ticks);
- weather, machines (including belts and tubes), pipes, drill pulses;
- bars (`entTick`) and grains in flight (`flyTick`);
- falling sand (`simMove`), then heat (`heatPass`);
- every so often: molds, milestones, plants, cold, tree support;
- the dock, the market and orders, saplings;
- the bot building ghosts.
- **Events.** The simulation reports what happened with `emit({ t: ... })`. `drainEvents`
(`src/ui/events.js`) turns each event into sounds, toasts and cards.
## The world (src/sim/world.js)
- **The grid** is `W × H` grains; cell `i = y * W + x`. Worlds are 2048×1024, 3072×1280 or
4096×1536.
- **Per-cell typed arrays:**
- `mat`: material id;
- `temp`: °C;
- `flags`: `F_NAT` natural rock, `F_CAST`, `F_BURN` burning;
- `stamp`: moved this tick;
- `part`: which built part a cell belongs to (`P_*`: tube directions, grate, open gate, filter,
pipe...);
- `mach`: machine id;
- `digp`: drill damage;
- `occ`: covered by a bar;
- `bgm`: the background layer (trees);
- also `life`, `netId` and `noiseB`.
- **Blocks:** the 4×4-grain build grid (`BS = 4`). `blockP` holds the piece code (`BP_*`) and
`blockV` the variant (direction, wall material, flags) of each block, over `BW × BH`.
- **Chunks** of 32×32 grains. `awake` marks chunks where things move and `hot` where heat flows;
only those are simulated. `setCell` wakes what it touches; after writing arrays directly, call
`wakeXY` / `wakeHeatXY`.
- **Other state:**
- `machines`: index = machine id; removed ones are `null`. Each is
`{ type, x, y, w, h, dir, on, ... }`. Its footprint is hull cells (solid) and void cells
(open inside).
- `molds`;
- `ents` (bars, 4×2 grains, with their own physics);
- `flyers` (grains in the air: blown, off belt ends, out of tube mouths, rain);
- `pipeNets`;
- `ghostBlocks`, `ghostMachines`, `ghostMolds`.
- **Player:** `bot` (position), `bag` (count per material), `player` (bit and tank tier, lens,
flask), `ledger` (milestones reached), `stats`, `money`, `debt`, `orders`.
## Materials (src/sim/materials.js)
- **Ids** are constants (`EMPTY = 0` to `BARSB = 94`, `NMAT = 95`). They are stored in saves:
never renumber them, only add new ones at the end.
- **`def(id, {...})`** sets each material's properties:
- `ph` (phase), `col`, `dens`;
- `k` (heat conduction), `cap`;
- `hard` (which bit can break it), `yld` (what it breaks into);
- `melt` / `meltTo`, `freeze` / `freezeTo`, `boil` / `boilTo`;
- `ign`, `flame`, `fuel`, `burnTo`;
- `bag` (can be carried), `desc` (the text players read).
These are copied into per-material typed arrays (`PH`, `DENS`, `KC`...).
- **Phases:** `PH_SOLID` holds, `PH_POWDER` falls and piles, `PH_LIQUID`, `PH_GAS`, and
`PH_ITEM` (bars, which are ents, not cells).
- **Naming:**
- `...V`: ore vein (solid);
- `...ORE`: loose ore;
- `M...`: molten;
- `SCR...`: scrap;
- `BAR...`: bar;
- `R...`: rubble (`RSTONE`);
- `L...`: loose (`LDIRT`, `LCLAY`, `LWOOD`).
## Building
- `PIECES` (`src/sim/pieces.js`) lists everything buildable:
- kind `block` (a code `BP_*`);
- kind `machine` (`bw × bh` blocks);
- kind `mold`.
`hidden` pieces are not in the menu. The menu's tabs and order are `BUILD_CATS` in
`src/ui/build-menu.js`.
- **Placing and removing:**
- `placeBlock` / `removeBlock` (`blocks.js`);
- `placeMachineAt` / `addMachine` / `removeMachine` (`actions.js`, `machines.js`);
- `addMold` / `removeMold` (`molds.js`);
- `removePieceAt` (`actions.js`) takes back whatever is at a cell;
- `interactAt` (`actions.js`) is the F key.
- **Ghosts** are what a paste or an out-of-reach drag leaves. The bot builds them from the bag
(`ghostTick` in `undo.js`).
- **Undo:** `undoRecord(label, fn)` wraps one player action; the place and remove functions
record their own steps with `undoNote(op)`.
- **Blueprints** (`blueprints.js`): `copyRegion`, `cutRegion`, `flipBlueprint`, `pasteGhost`.
## Drawing
- **The pixel look:**
- `drawWorld` (`src/render/world.js`) fills one pixel per grain;
- `computeLight` (`light.js`) adds coloured light;
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
(`machines.js`, `overlays.js`).
- **The smooth look** (`src/render/smooth.js`) takes the buffers `drawWorld` fills (colour and
a class per cell, the colour behind, trees, fields, light, glow) and shades them with WebGL2.
- The cell classes are set in `world.js` and read in the shader: 4 is a crisp built piece,
6 a wall tile, 7/8 a belt, 9–12 a screw tube, 13/14 a filter.
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
tools set it).
## The browser shell
- `GAME` (`src/ui/state.js`) holds the UI state: tool, selected piece, build tab, drag,
clipboard, paste mode, overlays. `SET` holds the settings, kept in `localStorage`.
- **Saves:**
- worlds are kept in IndexedDB (`src/ui/saves.js`);
- world files can be exported and imported;
- `serializeWorld` / `deserializeWorld` (`src/sim/save.js`) do the conversion and migrate
older saves.
## Where to find...
| What | Where |
|---|---|
| Material properties and descriptions | `src/sim/materials.js` (`def`) |
| Prices, orders, the dock | `src/sim/economy.js` |
| Piece costs and descriptions | `src/sim/pieces.js` (`PIECES`) |
| Build menu tabs, tiles and icons | `src/ui/build-menu.js` |
| Key bindings | `src/ui/input.js` (keydown); what they do: `src/ui/actions.js` |
| Mouse building and dragging | `src/ui/actions.js` (`primaryPress`, `primaryRelease`, `secondaryPress`, `toolTick`) |
| Cursor previews (ghosts, drill aim, selection box) | `src/ui/previews.js` |
| Controls help text | `src/ui/notebook.js` (`controlsHtml`) |
| Hover info and discovery cards | `src/ui/info.js` |
| Milestones | `src/sim/progress.js` (`MILESTONES`), checked in `src/sim/ledger.js` |
| Starter furnace designs | `src/sim/progress.js` (`STARTER_DESIGNS`, `smelterBlueprint`) |
| Falling sand, liquids, gases | `src/sim/cells.js` |
| Heat, melting, fire, reactions, alloys, steel | `src/sim/heat.js` |
| Belts, filters, screw tubes | `src/sim/conveyors.js`, `src/sim/machine-tick.js` |
| Fans | `src/sim/machine-tick.js` (`fanBlow`) |
| Refiner, Stone cutter, Silo | `src/sim/processors.js` |
| Pipes, pumps, outlets | `src/sim/pipes.js` |
| Molds and casting | `src/sim/molds.js` |
| The drill | `src/sim/drill.js`; reach in `src/sim/tools.js` |
| Vacuum and blowing | `src/sim/vacuum.js` |
| Trees and saplings | `src/sim/nature.js` |
| Weather | `src/sim/weather.js` |
| World generation | `src/sim/worldgen.js` |
| Save format and migrations | `src/sim/save.js` |
| Machine drawing (silo, refiner...) | `src/render/machines.js` |
| Smooth-look shader | `src/render/smooth.js` |
| Settings screen | `src/ui/settings.js` (settings and defaults: `SET` in `src/ui/state.js`) |
| Sound and music | `src/ui/audio.js` |
<!-- INDEX: generated by tools/map.py, do not edit below this line -->
## Index of source files (build order)
### src/sim/base.js (24 lines)
Deepgrain Foundry: simulation core (src/sim/). No DOM anywhere in src/sim/: plain data and functions, so the headless tests can run it and the layout can be ported to Rust later. This file: the random number generators and the phase ids every material has.
- functions: rnd, mulberry32, hash2
- variables: RS, PH_EMPTY, PH_SOLID, PH_POWDER, PH_LIQUID, PH_GAS, PH_ITEM
### src/sim/materials.js (159 lines)
Material ids, the material table (def), per-material property arrays, cell flags and part ids, and drill/tank tiers.
- functions: def, metalFamily
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
### src/sim/pieces.js (140 lines)
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
- functions: isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
### src/sim/economy.js (152 lines)
Money, prices, market saturation, the trade dock, buyer and builder orders.
- functions: saleAs, priceFactor, priceNow, sellUnit, marketTick, resetEconomy, payClaim, dock, inDock, dockTick, sellToDock, standing, makeBuildersOrder, makeOrder, ordersTick, completeOrder, placeDock
- variables: money, debt, earned, CLAIM_DEBT, PRICE, SAT_K, SAT_FLOOR, SAT_HALF, mkRecent, shipLog, saleAcc, floatAcc, orders, ordersDone, nextOrderT, orderSeq, ORDER_SLOTS, STANDING, BUILD_GOODS
### src/sim/progress.js (48 lines)
Starter furnace designs and the milestone list shown in the notebook.
- functions: smelterBlueprint
- variables: STARTER_DESIGNS, MILESTONES
### src/sim/world.js (96 lines)
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
### src/sim/cells.js (176 lines)
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove
- variables: SORTP
### src/sim/heat.js (246 lines)
Heat: conduction, melting and freezing, reactions, fire and air, steel.
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
- variables: POROUS, SOIL, HOTLIQ, ALLOY
### src/sim/machines.js (154 lines)
Machines: the power network, footprints, placing and removing machines.
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
- variables: nextPowerBuild
### src/sim/processors.js (99 lines)
Processing machines: the Refiner, the Stone cutter and the Silo.
- functions: refinerSlot, refinerTick, cutterTick, siloTick, siloFull
- variables: REFINE, REFINE_SAND, REFINE_RATE, CUT_PER_BRICK, CUT_RATE, SILO_CAP, SILO_RATE
### src/sim/pipes.js (114 lines)
Pipes, pumps and outlets: each joined line holds one liquid as a count, not as grains.
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
### src/sim/machine-tick.js (158 lines)
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
- functions: engineIntake, fanBlow, machinesTick
### src/sim/conveyors.js (109 lines)
How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear), the way a real belt carries a heap, not just its bottom layer. A filter's own material drops through it instead, unless the filter below is already full.
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeIntake
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/blocks.js (132 lines)
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (284 lines)
Molds: cavities that fill with molten metal (or glass) and are knocked out as bars.
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, checkMolds, moldAt, knockNote, tally, collectMold, removeMold
- variables: DROSS, grains
### src/sim/items.js (134 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
- functions: entSolidCell, entFreeAt, entMark, spawnBar, removeEnt, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast
### src/sim/tools.js (52 lines)
Tool helpers: reach, brushes, and plain digging.
- functions: inReach, forBrush, drillReach, inDrillReach, toolDig
### src/sim/drill.js (161 lines)
The resonance drill: tuned pulses that shatter one material.
- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF
### src/sim/nature.js (168 lines)
Trees (background layer), acorns, saplings, and waking freezing water.
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
- variables: treeBases, saplings, treeDraw
### src/sim/vacuum.js (165 lines)
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
- variables: blowRR, FLY_G, FLY_Q
### src/sim/actions.js (86 lines)
Player actions on built things: take back, place a machine or mold, gates, F to interact. Take back whatever piece is at (x,y): machine, mold or block. Returns a message.
- functions: removePieceAt, removePieceAt0, placeMachineAt, placeMoldAt, toggleGateAt, interactAt
### src/sim/blueprints.js (105 lines)
Blueprints: copy, cut, flip, cost and paste (as ghosts) a region of built pieces.
- functions: copyRegion, flipBlueprint, bpCost, pasteGhost, cutRegion, footprintBlocker, clearFootprint
### src/sim/undo.js (87 lines)
Undo (Ctrl+Z) for building, and the bot building ghosts.
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
### src/sim/bot.js (55 lines)
The bot: movement and collision with the world.
- functions: botBlocked, botOverlapSolid, botSlide, botStep
- variables: SLIDE_MAX, SLIDE_STEP
### src/sim/ledger.js (43 lines)
Milestone checks (the ledger) and the sky roof line used for cooling.
- functions: checkLedger, updateRoof
### src/sim/weather.js (85 lines)
Weather: clouds from steam, rain and snow, and the world water count.
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
- variables: WBAND, WS, weather
### src/sim/tick.js (25 lines)
simTick: one simulation step, in order.
- functions: simTick
### src/sim/worldgen.js (315 lines)
World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
- functions: makeNoise, fbm, clearHardSpecks, generateWorld
- variables: SPECK_MIN
### src/sim/save.js (112 lines)
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
- functions: serializeWorld, deserializeWorld
- variables: SAVE_VERSION, TOFF
### src/render/setup.js (196 lines)
Drawing (src/render/): browser only. This file: the renderer state R, colour tables (glow, thermal palettes, pipes, trees, sky), canvas setup and the camera.
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
### src/render/light.js (79 lines)
Propagated coloured light from glowing and burning cells.
- functions: computeLight
### src/render/world.js (277 lines)
drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
- functions: drawWorld
- variables: TUBE_CLASS
### src/render/machines.js (152 lines)
Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
- functions: cellRect, drawHazardRims, drawMachines, drawSilo
### src/render/overlays.js (230 lines)
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
### src/render/smooth.js (430 lines)
Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld fills (colour + class, colour behind, trees, occupancy fields, light, glow) into rounded, outlined, softly lit shapes at screen resolution. The 2D canvas then draws this image as its world layer, and every overlay (machines, bot, previews, labels) goes on top as before.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, SM_VS, SM_FS, smBytes
### src/ui/audio.js (209 lines)
Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples.
- functions: auInit, auVolumes, rateOk, tone, noiseBurst, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler
- variables: AU, SFX, ZONES
### src/ui/state.js (67 lines)
Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line.
- functions: loadSettings, saveSettings, loadCollect, el, esc, rgbOf, fmtT, toast, setStatus
- variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $
### src/ui/title.js (46 lines)
The title screen art.
- functions: buildTitleCells, drawTitle
- variables: FONT5, TITLE
### src/ui/saves.js (106 lines)
Saves in IndexedDB, world files (export / import) and the blueprint library.
- functions: saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
- variables: DB
### src/ui/session.js (87 lines)
Starting, loading and quitting a game; modal dialogs; the pause menu.
- functions: showLoading, enterGame, startNewWorld, startFromSave, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause
### src/ui/settings.js (116 lines)
The settings screen.
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
- variables: SET_TABS, setTab
### src/ui/notebook.js (201 lines)
The notebook (N): milestones, market, wallet, blueprints, controls.
- functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml
- variables: nbTab, MARKET_GROUPS
### src/ui/hud.js (40 lines)
The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, mixList, ensureDropMat, piece
### src/ui/build-menu.js (72 lines)
The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey
### src/ui/selection.js (246 lines)
The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
- variables: lastBagKey
### src/ui/info.js (167 lines)
Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
### src/ui/input.js (76 lines)
Keyboard and mouse listeners.
- functions: isTyping
- variables: canvas
### src/ui/actions.js (244 lines)
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown
### src/ui/gamepad.js (60 lines)
Gamepad input.
- functions: padPressed, pollPad
- variables: PAD
### src/ui/previews.js (334 lines)
Previews drawn under the cursor: build ghosts, drill aim and pulses, selections, paste.
- functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawPreviews, drawTag, drawSelection, drawPastePreview
- variables: CRACK_N, CRACK_LV, SEL_COL
### src/ui/events.js (44 lines)
Events from the simulation (emit) turned into sounds, toasts and cards.
- functions: drainEvents
### src/ui/loop.js (117 lines)
The frame loop and boot.
- functions: scanVisible, frame, boot
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
## Tests and tools
- `test/furnace_play.js`: Fires a starter furnace design repeatedly the way a player would, without cleaning it out between firings, to see whether it clogs. Usage: node test/furnace_play.js [design] [ore] [coal] [firings] [roll] [mode] mode: 'mix' (even ore/coal mix blown in), 'layers' (ore first, coal on top), 'vent' (blown in from above through the vent)
- `test/harness.js`: Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one script, the same way build.py joins them) so scenarios can drive it headlessly in Node. Errors point at the real file and line (sim/pipes.js:42), not at the joined script.
- `test/play.py`: Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue. Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the second argument is 'smooth'.
- `test/run_all.py`: Run the test suites and report.
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
- `test/t_molds.js`: Molds and furnaces stay usable: knock-outs leave the mold clean, plugs over the mold are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Refiner grinds waste rock for trace ore.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the stone cutter, the silo and builders' orders.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
- `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
- `tools/perf.py`: Times the renderer and the simulation in a headless browser: light, drawWorld (with and without light and glow), the pixel upload and blit, and one simulation tick, in ms. Rebuild first.
- `tools/playthrough.py`: Headless browser run: load the page, start a world, play a little, take screenshots.
- `tools/shot.py`: Screenshot the game in a headless browser, after setting up a scene with JavaScript.
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
- `tools/experiments/factory_cost.js`: What does a running factory cost when the player is somewhere else? Builds K self-running modules in a generated world, far from the bot, and measures simulation time per tick. Each module: a coal + copper-ore furnace burning in a stone-block box, a spring feeding a water wheel, a shaft carrying its power to a 10-block belt that hauls sand off a hopper.
- `tools/experiments/flow.js`: Throughput of belts and screw tubes: grains delivered per second. Whatever lands past the outlet is taken away, so the output pile never backs up into the line.
- `tools/experiments/furnace.js`: Furnace scenarios on a flat test world: builds a chamber, charges it with ore and coal, lights it and prints temperatures, burning cells and what has melted or set at intervals.
- `tools/experiments/section.js`: ASCII cross-section of a stone furnace firing, with a temperature digit per cell (0-9), printed at intervals. Usage: node tools/experiments/section.js [WALL_MATERIAL] (default STONE).
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
- `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area).
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
+133
View File
@@ -0,0 +1,133 @@
# Testing
There are three layers. Use all three.
1. **Headless suites** (`test/t_*.js`) run the simulation in Node with no browser. They are fast
and deterministic, and they are where most checks live.
2. **Browser tests** (`test/play.py`, `test/t_smooth.py`) drive the real page in headless
Chromium with real key, mouse and gamepad input. They check the UI, saving, the smooth look,
and that the page throws no errors.
3. **Screenshots** (`tools/shot.py`) show what something looks like. Open the PNG and look at it.
Run everything with `python3 test/run_all.py --browser`. It exits non-zero if anything failed and
prints the failing checks.
## Headless suites
Each suite is a plain Node script:
```js
// What this suite covers, in a sentence or two.
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
const { run } = load(); // loads src/sim/ (the files in src/manifest.json)
const { check, done } = suite();
flatWorld(run, 240, 160, 100); // 240x160 grains, sky above row 100, stone below
run('money = 5000; bot.x = 60; bot.y = 40;');
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_BELT, 1, bx, 20, true);'); // true: free, no reach check
rect(run, 84, 60, 95, 79, run('SAND'));
ticks(run, 300);
check('sand rides the belt off its end', count(run, run('SAND'), 120, 80, 239, 99) > 100, count(run, run('SAND')));
done(); // prints ALL PASS or N FAILED, sets the exit code
```
- `run(code)` evaluates JavaScript inside the simulation and returns the result. Every top-level
name in `src/sim/` is available. Return plain values (numbers, strings, `JSON.stringify(...)`).
- Harness helpers:
- `flatWorld(run, w, h, ground)`;
- `put(run, x, y, m)`;
- `rect(run, x0, y0, x1, y1, m)`;
- `count(run, m, x0, y0, x1, y1)`;
- `maxTemp(run, ...)`;
- `ticks(run, n)`;
- `dump(run, x0, y0, x1, y1, withTemp)` for an ASCII picture of an area.
- `check(name, ok, detail)`: name the behaviour the player would notice ("a pump moves water
over a wall"). Put the measured numbers in `detail`, so a failure shows how far off it was.
- Coordinates are in grains. Block coordinates (for `placeBlock`, `placeMachineAt`) are grains
divided by 4.
- Errors name the real file and line (`src/sim/pipes.js:42`).
- A new suite named `test/t_<topic>.js` is picked up by `run_all.py` automatically.
**Determinism.** The simulation's randomness comes from seeded generators. The same code and the
same test give the same output every run, apart from lines with timings in ms. To prove a
refactor changed nothing, save every suite's output before and after, and diff them:
```sh
for t in test/t_*.js; do node $t > /tmp/before/$(basename $t).txt; done # before the change
for t in test/t_*.js; do node $t > /tmp/after/$(basename $t).txt; done # after
diff -r /tmp/before /tmp/after
```
## Browser tests
- `test/play.py` (about 2.5 minutes) builds a world through the title screen and plays it with real
input. It checks the tools, building, copy/cut/paste, undo, the build menu, filters, pipes,
the notebook and market, saving and continuing, exporting and importing a world file, the
gamepad and world sizes. Screenshots go to
`shots/play/`. Pixel look by default; `python3 test/play.py shots/play smooth` uses the smooth
look.
- `test/t_smooth.py` checks that the WebGL2 smooth look compiles and draws, that it falls back
to pixels cleanly, and that the thermal lens works.
- Run them one at a time. Two headless browsers at once can starve each other, and
`t_smooth.py`'s half-resolution check then fails. `run_all.py` runs them one after the other.
- Rebuild first (`python3 build.py`). They load `dist/index.html`. `run_all.py --browser`
rebuilds for you.
## Screenshots: tools/shot.py
```sh
python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
--js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 --no-light
```
- It starts a new world, pauses it at once, runs your setup code in the page, runs `--ticks`
simulation steps, then saves the picture. Because the world is paused otherwise, the same
options always give the same picture.
- Setup code can use every game global, plus `SX`, `SY` (the bot's starting position).
`tools/scenes/clear-sky.js` clears a flat work area and sets `FLOOR`, `BX`, `BY` and `CAM`. Set
`CAM = [x, y]` to frame the shot.
- The HUD and the cursor previews are hidden unless you pass `--hud` or `--previews`.
- `--look smooth` for the smooth look. Test in the pixel look unless you are checking graphics.
- `--freeze` draws the last frame at a fixed time and frame count, so animations are caught at
the same moment. Two shots of the same scene then match pixel for pixel, and a before / after
pair differs only where something really changed. A quick comparison with Pillow:
```python
from PIL import Image, ImageChops
a, b = (Image.open(p).convert('RGB') for p in ('before.png', 'after.png'))
print(ImageChops.difference(a, b).getbbox()) # None: identical
```
To compare against an older version, build it somewhere else (for example from a git
worktree) and pass that page with `--page`.
- Page errors are printed with their source file and line, and the exit status is 1.
- Look at the picture. A shot that "ran without errors" can still show the wrong thing.
## Pitfalls
- **WebGL in headless Chromium.** There is no GPU, so the smooth look needs SwiftShader:
launch with `--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader`. Also set
`window.__allowSoftwareGL = true` before the page loads (`add_init_script`); otherwise the game
sees software rendering and switches to the pixel look. `tools/shot.py` does both.
- **Top-level `const` and `let` are not `window` properties.** In the page, `window.SM` is
`undefined` even though `SM` exists. Write `typeof SM !== 'undefined'`, or use the bare name.
Functions declared with `function` are on `window`.
- **Playwright calls a string that evaluates to a function.** `page.evaluate("x = function () {}")`
assigns the function and then calls it. Wrap statements in an arrow function:
`page.evaluate("() => { ... }")`.
- **Starting a world takes a moment.** `startNewWorld(seed, w, h)` generates on a timer. Wait
for `GAME.running === true`. Then remove the welcome card if it is in the way:
`document.querySelectorAll('.card, .toast').forEach(e => e.remove())`.
- **Real-time play is not deterministic.** In the browser the simulation runs as fast as frames
arrive. For repeatable results, pause (`GAME.paused = true`) and call `simTick()` yourself, as
`tools/shot.py` does.
- **World sizes** offered by the game: 2048×1024, 3072×1280, 4096×1536 grains.
- **Rebuild after every source change** before any browser test or screenshot. They all load
`dist/index.html`.
## Measuring
- `tools/perf.py`: renderer and tick times in the browser.
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
- `tools/experiments/flow.js`: belt and tube throughput in grains a second.
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
+53
View File
@@ -0,0 +1,53 @@
# 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-23: export from claude.ai to a local repository (Claude, in claude.ai)
Request: split the source into small files, deduplicate and tidy a little, test fully, and
write the notes, docs and agent instructions for local development.
- **Source split.**
- `core.js` (3,688 lines), `main.js` (2,012) and `render.js` (938) became 52 files in
`src/sim/`, `src/render/` and `src/ui/`, each with a header line. The build order lives in
`src/manifest.json`.
- Checked that the joined code was identical to before, apart from comments and blank lines.
- Every headless suite gave the same output, line for line, before and after.
- Screenshots of two scenes, in both looks, were pixel-identical before and after
(`tools/shot.py --freeze`).
- **Tidying, behaviour unchanged.**
- One scan loop in `simMove` instead of two mirrored copies.
- A `drawHazardRims` helper replaced 3 copies.
- The belt and filter shader code shares `beltLineSD`, `bandRidge` and `bandColor`.
- Removed unused `refundCost`, `wakeArea`, `brushFor`, and the `PARTS` / `PART_BY_ID` aliases.
- The suites share `suite()` (check / done) from the harness, and failures now set the exit
code.
- **Tests.**
- Six broken old scripts were deleted; they used functions removed long ago.
- Experiments and reports moved to `tools/experiments/`.
- `t_v2` / `t_v4` / `t_v9` were renamed `t_basics` / `t_world` / `t_conveyors`.
- Added `test/run_all.py`.
- **Tools.**
- `tools/shot.py`: repeatable screenshots of a scene set up in JavaScript.
- `tools/where.py`: maps built-page line numbers back to source files.
- `tools/map.py`: the project map's index.
- `tools/scenes/`: example scenes.
- The game skips cursor previews when `GAME.hidePreviews` is set, for clean screenshots.
- **World files outside claude.ai.**
- Pause → Export world file only worked inside claude.ai, through its downloads hook.
- It now falls back to an ordinary browser download (`exportWorld` in `src/ui/saves.js`).
- `test/play.py` checks that an exported file downloads and imports back on the title screen
as the same world.
- **Build.** `build.py` writes `dist/index.html` (a full page), `dist/deepgrain-foundry.html`
(the same without the document wrapper, as the claude.ai artifact used) and
`dist/bundle-lines.json`.
- **Docs.** `AGENTS.md`, `CLAUDE.md`, `README.md`, `NOTES.md`, `docs/PROJECT_MAP.md`,
`docs/DESIGN.md`, `docs/TESTING.md`, `docs/systems/*`, `docs/history/requests.md`.
- **Tests run.** All 11 headless suites pass, as do `test/play.py` (101 checks) and
`test/t_smooth.py` (8 checks).
- **Open.**
- The owner reports that big piles on belts still only move at the top (NOTES.md); not yet
reproduced.
- The game's last claude.ai artifact was version 24.
+82
View File
@@ -0,0 +1,82 @@
# The owner's requests, in their own words
What the owner asked for while the game was built in claude.ai, kept word for word so the intent
behind a feature can be checked. The newest are at the bottom. For what was decided and built,
see `docs/DESIGN.md`.
Messages from before 2026-10-20 are not available word for word. The early design
(2026-10-17 to 2026-10-19) is summarised in the "What the owner wants" section of
`docs/DESIGN.md`.
## 2026-10-20 (sent again 2026-10-22)
> - A lot of times im leaving behind a few bits of materials unmined and thats leaving obstackles in my way. how can we handle that. its a little annoying.
> - belts and filter belts should mesh together as one belt.
## 2026-10-22
> seems like sometimes when ticking items with R with he vacuum, then sucking items not starting with one that was ticked, will just suck everything. like if you tick coal and copper then click and drag from stone, you also pickup stone and other stuff.
> dont let the too hard specs crumble at all. world gen shouldnt generate viens that small anyway. leaving them creates friction for flying, crumbling them into what they are leads to player confusion about how they obtained them without the right bit, and crumbling to stone or something means lost ore.
> when drawing belts and filters, set their direction in the direction of drawing them. allow manual setting with R when they are placed without dragging. maybe show an arrow when dragging to place to indicate it and update the filter box on the top left where it says the direction and R.
> instead of rounded ends on the belts and filters, lets make them rectangles but somewhat rounded corners.
> instead of a copy tool. use modifyer keys like X for cut, c for copy. you hold the modifier while dragging a box around. when release it executesd the action. make it clear what function will happen. make them istinct. cut is destructive but it can be pasted back so its not too bad.
> add ctrl+z undo for building.
> when placing building objects, hold ctrl to go to replace mode to for it to replace.
> please explain how to use fans in a furnace. they seem to get destroyed when in contact with the heat from smelting.
> Lets allow refining of stone to get out trace amounts of common ores. maybe refine sand, dirt and gravel too?
> what can we do with all the stone in the world? itsa building material but other than that not much else use. the player may want to store it somewhere for use later. but what about the rest? it just sits somewhere clogging up the place. refining can help, refining can reduce it. what else can we do with it?
> need a way to move water. maybe pumps, pipes, and outlets.
## 2026-10-23
> it seems like the screw tubes can carry more materials than the belts can, that makes a buildup on that belt. they should be the same. if the player wants more input they can build additional screw tuibes and another level of belts or something.
> sand can be re-refined. that is nice but is it intended? im not sure if its good or not. in one hand the pl;ayer ends up with a lot of sand and not much to do with it all. but on the other hand re refining it means they could get more ores while reducing their sand stockpile. This also means its possible to stack refiners (one block clearance) and re-refine the sand multiple times until none is left. lets talk about this.
> add a dropper/ pick item key so you can select an item already built to build more.
> we need a better designed for ux, build menu system.
> there seems to be some sort of length limit on building belts with drag. lets remove that.
> in real life if a stack of items is on a belt, the belt does not move the items under them while leaving them stationary, thats not how physics works. lets make it more realistic.
> pipes should not block water.
> I also like all your ideas worth building next: Glass chain… Storage silo… Builders' orders… Cut stone or concrete… One thing about the silo though, lets make it look less like a magic black hole you can place and more like a real silo.
The four ideas were:
- the glass chain (sand to glass);
- a storage silo;
- builders' orders for bulk rubble;
- cut stone or concrete.
Cut stone was built; concrete was not.
### Moving to local development
> i have more notes for you but ive been thinking about exporting this to code it locally with claude code and codex. I would also need all the notes and other supporting documents and information about it and its development, and probably also an agents/claude.md files with good instructions for building this. Before i do this though i want to discuss how this would work and if the cli coding agents would be able to play test it and take screenshots as you are. i think for a while at least i might leave it as a web game like it is now but i might have the agent split the artifact into multiple modules for better coding practice and host it with python http server i think. i imagine you are playtesting the game with playwrite? i want the agent's to be able to playtest it the same way as you are. much later after the game is at a certain stage i may consider converting to a rust game for performance. but for now its fine as is. lets discuss this before moving forward with any export.
> I do have more notes, some that i was planning on giving you now and some for the future, but for now you could just not work on any of them and include them in a notes file i can add to locally after the export. the agent could read and modify the notes file removing items that have been completed. the completed items could be recorded in another log the agent uses internally for its self. ill be adding more notes to the notes file which it could read and complete when asked. for now a zip export is fine, ill have the agent add it to a gitea instance i have, here i think claude probably only supports github. make sure the agent instructions include things like to not hallucinate, assume or be too confident. confirm with different sources if not confident enough and use the web when needed. as for splitting it to modules i think keeping the build step is fine for now. but i do want to split the larger source files more. the agents will work better with smaller more concise files. the agents should also create a project map for their use so they can quickly find the information they need when its not in context. before the export, are there any more questions or concerns you have?
> q1: yes please split for me first that will make the agents get to work faster. maybe also deduplicate and refactor a little too while you are at it? ofcourse full testing first to confirm the game still works properly.
> q2: the done please check section is actually a better idea.
> q3: i usually run one or the other and if its claude code, i have instructions for it to locally delegate to codex some things to save tokens. so one at a time i guess. but codex and claude working side by side and some way for them to communicate sounds like a good idea, but for later, ive never done it that way and will need to figure out and plan how that will work first.
>
> concerns:
> note 3 was not fixed, all of those notes i wrote after version 24.
> i have exported the only save i care about.
> claude code and codex both have web access.
The notes written after version 24 are in `NOTES.md`. The note "keep sand as is, allow
re-refining..." was a decision, so it is recorded in `docs/DESIGN.md` instead.
+86
View File
@@ -0,0 +1,86 @@
# Dig tools: decisions
Decided 2026-10-19. The resonance drill went in on 2026-10-20, and the same day its widths were
reworked so that each one has a use. Handling of stray bits was added on 2026-10-22 and revised
the same day: rock too hard for the bit never crumbles, and world generation makes no tiny hard
specks instead. The code is in `src/sim/drill.js` and `src/sim/worldgen.js` (`clearHardSpecks`).
## Main tool: Resonance drill
It replaced the old "darken and delete" drill cursor.
- **How it works.**
- Holding the button sends pulses from the bot toward the cursor.
- Each pulse is a fan of rays leaving the whole face of the bot, so rock right beside the bot
is hit too.
- Rock a pulse passes through shakes: it pales, shivers, and shows cracks that grow finer as
it nears breaking. Then it shatters into its loose grains.
- The pulse loses energy as it goes deeper.
- **Pulse width** is changed on the fly with Q/E while the drill is selected. Each width has its
own reach.
| Width | Shape | Reach | Good for |
|---|---|---|---|
| Narrow | A beam a little wider than the bot | ×1.45 the bit's reach | The fastest and longest tunnels, big enough to fly down |
| Medium (default) | A cone | ×1 | General digging |
| Wide | A broad fan | ×0.7 | Opening the most rock per second, but only close to the bot |
Measured with a stone bit, moving with the face: narrow tunnels about 13 grains a second,
medium about 9, and wide about 7. Wide opens the most rock per second.
- **Tuning** (R over a material, R again to untune): the pulse passes through everything else
and shatters only that material.
- **Loose grains** a pulse passes are jostled back toward the bot, so a shattered vein pours out
of the wall and a bore clears itself.
- **Hardness and base reach** still come from the cast bit tiers: stone, copper, iron, steel.
- **Built blocks, machines and molds** soak up pulses unharmed and shield what is behind them.
- **Trees** in the path come apart into wood chunks (not with a tuned pulse).
- **The bot** never moves loose material. Sand, rubble and water pass straight through it.
### Stray bits
Most leftover obstacles were specks of rock too hard for the current bit, such as iron ore or
granite inside stone when you have a stone bit.
The rule: **rock too hard for your bit never breaks, crumbles or comes loose, however small.**
Crumbling it into ore would let players get ore without the right bit, and confuse them.
Crumbling it into stone would lose ore.
These keep specks out of the way instead:
- **World generation makes no tiny hard specks.**
- Iron and zinc come in fewer, chunkier veins, with the same total ore.
- Any piece of 30 grains or fewer that is harder than the rock around it becomes that rock
(`SPECK_MIN`).
- Older saves get this cleanup once, when loaded.
- Basalt dikes in the granite were removed, because they walled off parts of it.
- **Cut-off pieces fall apart.** When a grain breaks and leaves a piece of natural rock of 16
grains or fewer touching nothing else, the piece falls into its loose grains. This only happens
if the bit could break every grain of it. A piece with too-hard rock in it stays whole.
- **Thin rock breaks faster.** Corners, nubs and slivers with open space on two or more sides
shake apart quicker than a flat face.
- **The bot slips around lips.** Flying straight into a ledge a grain or two deep, it slides past
instead of stopping dead. A real wall still stops it.
- **Tuned pulses** do none of this; they only touch the tuned material.
Upgrade ideas, not built yet:
- Amplifier: reaches deeper per pulse.
- Tuning fork: tuning could become an upgrade instead of being there from the start.
- Harmonics: breaks granite, then basalt (could replace the bit tiers).
The owner wants the whole upgrade system redesigned. The bit mold does not really fit a
resonance drill (see NOTES.md).
## Later: Grav pick (upgrade tool)
A gravity field locks onto a lump of rock, pries it out whole and carries it. It then crushes it
into grains or throws it, into a hopper or a furnace.
It is too slow for normal digging, but useful for selective digging and for moving material.
Upgrades:
- field coil: bigger lumps;
- stabilizer: range;
- compressor: crushes faster;
- sling: throws.
## Late game: Mining charges
Placed charges that blast a radius and throw debris, for digging out big areas.
## Concept art
Animated concepts for both tools are on the "Dig Tool Concepts" design canvas in claude.ai (not
in this repository).
+101
View File
@@ -0,0 +1,101 @@
# Waste rock and moving liquids
Built 2026-10-22; the uses for stone and sand were added 2026-10-23.
- Code:
- `src/sim/processors.js` (Refiner, Stone cutter, Silo);
- `src/sim/pipes.js`;
- `src/sim/conveyors.js` and `src/sim/machine-tick.js` (belts and tubes);
- `src/sim/economy.js` (prices, builders' orders).
- Tests: `test/t_pipes.js`, `test/t_stone.js`, `test/t_conveyors.js`.
## Refiner (machine, 3×3 blocks, $120)
An open hopper over a grinding floor. Grains that reach the floor are ground at about 30 a
second and come out of a chute under the machine. If the space under the chute is blocked, it
stops and says so.
For each grain ground, it tries for one grain of ore, in the order below. If none turns up, half
the time a grain of fine sand comes out; otherwise nothing does. So a refiner roughly halves a
pile of waste.
| Input | Chances of ore per grain |
|---|---|
| Stone rubble | copper ore 2.5%, coal 2%, iron ore 1.5%, tin ore 0.6% |
| Granite rubble | iron ore 2%, zinc ore 1.2%, copper ore 1%, silver ore 0.2% |
| Basalt rubble | iron ore 3%, zinc ore 1%, gold ore 0.3% |
| Gravel | iron ore 3%, copper ore 2%, tin ore 1%, zinc ore 0.3% |
| Sand | tin ore 1%, iron ore 0.8%, gold ore 0.15% |
| Dirt | clay lumps 8%, coal 1%, copper ore 0.8%, acorns 0.3% |
Measured: 400 stone rubble gave 28 grains of ore and 190 of sand. Anything it cannot grind,
such as ore, coal or liquids, drops straight through.
**Re-refining sand is allowed** (decided 2026-10-23). The sand a refiner puts out can go
through another refiner.
- Each pass gives about 2% ore and returns about 49% as sand.
- Refining it again and again, as with stacked refiners, eventually turns all the sand into
about 3.8% more ore per grain of sand. That is roughly 27% more ore from rubble overall.
- The owner kept it because it pays less than making glass. Players will do it when they need
ore rather than money.
## Pipes, pumps and outlets (blocks)
- **Pipe ($8).**
- Pipes are thin. Sand, water and anything else loose fall past them, so a run of pipe never
dams anything.
- Pipes, pumps and outlets that touch join into one line, which holds 16 grains per block.
Its contents are kept as a count, not simulated grain by grain.
- Taking a pipe back spills its share where it stood.
- **Pump ($70).**
- Draws liquid in at the tail of its arrow, from up to 3 grains away.
- Pushes it into the line through its other sides, at about 60 grains a second while there is
room.
- Takes only liquids cooler than 500°C (water, molten tin, zinc, brass and bronze), and one
liquid per line.
- F turns it on and off. Pumps and outlets are solid blocks.
- **Outlet ($15).** Pours the line's liquid out of the side its arrow points to, up to 2 grains a
tick, wherever there is room. F opens and closes it.
- **Water accounting.** Water in pipes counts in the world's water total, so the weather never
sends rain to replace it. Pipe contents are saved.
## Belts and screw tubes
- A heap on a belt moves as one: the whole stack, up to 96 grains deep, steps along together
instead of only its bottom layer. The owner reports that big piles still only move at the top
(see NOTES.md).
- Screw tubes run at belt pace, moving every other tick.
- A horizontal tube fed from a full hopper moves about 100–110 grains a second.
- A belt under its mouth keeps up, carrying a stream 3–5 grains deep at about the same rate.
- A vertical lift brings up about 40–60 grains a second.
- `tools/experiments/flow.js` measures these.
## Uses for stone and sand
- **Glass bricks.**
- Sand melts to molten glass in a fan furnace (peak about 2000°C measured).
- Cast in an Ingot mold, it knocks out as glass bricks, worth $14 each.
- Glass bricks build glass wall blocks. Glass is too brittle for a bit mold.
- **Stone cutter** (machine, 2×2, $90).
- A saw under a hopper. Every 8 grains of stone rubble become one stone brick, which drops out
underneath; put a belt under it.
- Anything else falls straight through.
- **Cut stone walls.**
- 2 stone bricks per block. They look like coursed masonry and stand more heat than rough
stone (they melt at 1450°C).
- Taking one back returns the bricks.
- **Silo** (machine, 3×6, $180). It was made to look like a real silo, not a magic black hole.
- A steel bin on legs, with a roof hatch, a sight-glass gauge, a ladder, and a cone with a
slide gate.
- Holds up to 4,000 grains of one loose material; drop grains in through the hatch.
- F opens the gate, letting about 60 grains a second out underneath; F again shuts it.
- It cannot be taken down or cut while it holds anything. Its contents are saved.
- **Builders' orders.**
- About 30% of new orders come from Builders. They want 400–1,500 rubble, 300–1,000 gravel,
300–900 sand or 20–80 stone bricks.
- They pay 4–6 times the market price for loose material, and 3–4 times for bricks.
- Ordinary buyers never ask for rubble or gravel.
- **Market prices.** Rubble sells for $0.02 a grain and gravel for $0.03 at the dock; a stone
brick sells for $0.60. Dirt earns nothing.
## Other ways to deal with stone
- The trade dock takes anything that drops through it, so it also works as a dump.
- Wall blocks store rubble compactly (16 grains each); take them back to get the rubble.
- The vacuum can blow rubble into old shafts as backfill.
- Not built yet: concrete.