From d936f655536f9cadd56cc40ed799495673abd849 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Thu, 29 Oct 2026 18:12:47 -0400 Subject: [PATCH] Nozzle and rain count only their own grains in flight; factory launches uncapped (t95) --- docs/DESIGN.md | 6 ++++++ docs/agent-log.md | 14 ++++++++++++++ src/sim/vacuum.js | 7 +++++-- src/sim/weather.js | 5 ++++- test/t_nozzle.js | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 9c99ceb..706f3a1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -610,3 +610,9 @@ above, including: cells. Exclude dedicated construction materials from give, discovery and blowing. On load, clear loose construction grains and their bag/nozzle inventory; preserve owned blocks, slope seals and machines. Ordinary wall ingredients still become rubble, scrap or bars. + +- **Separate airborne allowances (t95, 2026-10-28).** The owner accepts a nozzle cap but + factory production must never stall because of a shared flyer limit. Only bag-blown grains + count toward the raised nozzle allowance (4,000); only rain counts toward rain's 2,500. + Factory launches remain uncapped. A full nozzle allowance says “Too much in the air.” + The 4,000 tuning preserves the existing eight-second blown-pile collapse check; 6,000 did not. diff --git a/docs/agent-log.md b/docs/agent-log.md index 9bbfef8..7c9ffb9 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,20 @@ What agents have done, newest first. Add an entry for every piece of work: the d 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-29: t95 separate nozzle and rain flight allowances (Codex) + +- Nozzle counts bag flyers only, up to 4,000, with “Too much in the air.” feedback; + rain counts only rain, up to 2,500. Factory launches remain uncapped. No worldgen/save changes. +- Files: src/sim/{vacuum,weather}.js, test/t_nozzle.js, docs/DESIGN.md, tasks/t95* and this log. +- Eight new checks failed first; all 37 headless suites passed (19 nozzle checks). + Initial 6,000 allowance failed existing collapse checks; 4,000 passes without physics/test changes. + Only browser test: /tmp/playwright-test-t95.py (held right click, limit toast/resume, busy factory, + no page errors); inspected shots/t95-nozzle-limit.png (pixel). Sandbox startup stalled; + outside-sandbox GPU run passed after correcting test setup. No full browser suite. +- CPU flyTick median/p95 at 20,000 flyers: before 0.40/0.45 ms, after 0.25/0.33 ms; + timings are noisy and update code is unchanged. Combined 100 blow/rain calls: 7.6 ms after. + Logs: /tmp/t95-{headless-final,browser-final,perf-final}.log. Owner playtest pending. No commit. + ## 2026-10-29: t97 keep construction grains out of the bag (Codex) - PARTMAT now blocks give/discovery, inventory/refunds and single/mixed blowing. Load clears diff --git a/src/sim/vacuum.js b/src/sim/vacuum.js index 66fe526..ba20196 100644 --- a/src/sim/vacuum.js +++ b/src/sim/vacuum.js @@ -90,10 +90,13 @@ function toolBlow(cx, cy, r, m, mix) { cx = nz.ox + nz.dx * nz.dist - 0.5; cy = nz.oy + nz.dy * nz.dist - 0.5; } - if (flyers.length > 3000) return { placed: 0, status: '' }; + // Factory output and rain do not use the nozzle allowance. + let airborne = 0; + for (const f of flyers) if (f.bag) airborne++; + if (airborne >= 4000) return { placed: 0, status: 'Too much in the air.' }; // nozzle 0 is a trickle for careful work, the widest a torrent const rate = 1 + r + ((r * r) >> 1); - const n = pickM ? rate : Math.min(bag[m], rate); + const n = Math.min(4000 - airborne, pickM ? rate : Math.min(bag[m], rate)); let placed = 0; for (let k = 0; k < n; k++) { const gm = pickM ? pickM() : m; diff --git a/src/sim/weather.js b/src/sim/weather.js index 9f7475d..6402ed1 100644 --- a/src/sim/weather.js +++ b/src/sim/weather.js @@ -28,8 +28,11 @@ function worldWater() { function absorbSteam(x) { if (chunkWork) { chunkEvent('moist', x); return; } const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)); weather.moist[b] += 1; } // Send about k drops (snowflakes where it is freezing) down from the clouds over x0..x1; returns how many fell function spawnRain(x0, x1, k) { - if (flyers.length > 2500) return 0; + let airborne = 0; + for (const f of flyers) if (f.rain) airborne++; + if (airborne >= 2500) return 0; let n = Math.floor(k) + (wrnd() < k - Math.floor(k) ? 1 : 0), made = 0; + n = Math.min(n, 2500 - airborne); for (; n > 0; n--) { const x = Math.floor(x0 + wrnd() * (x1 - x0)); if (x < 0 || x >= W) continue; diff --git a/test/t_nozzle.js b/test/t_nozzle.js index 0c11f72..5d99f48 100644 --- a/test/t_nozzle.js +++ b/test/t_nozzle.js @@ -29,4 +29,37 @@ for (const distance of [run('3 * REACH'), run('5 * REACH')]) { const nearby = blowSand(30, 0); check('aiming inside reach still lands sand around the cursor', nearby.cursorDistance <= BRUSH_AND_SETTLING_MARGIN, nearby.cursorDistance.toFixed(2)); +// Factory launches must not consume the nozzle or weather allowances. +flatWorld(run, 640, 512, 480); +run(`bag[SAND] = 30000; bagTotal = 30000; + placeBlock(BP_BELT, 1, 20, 20, true); + for (let k = 0; k < 10000; k++) { + const i = 79 * W + 83; setCell(i, SAND, 20, 0); + tickStamp++; carryColumn(i, 1, null); + }`); +check('a belt chute launches ten thousand grains without a shared cap', run('flyers.length === 10000 && mat[79*W+83] === EMPTY')); +check('busy chutes do not pause the nozzle', run('toolBlow(330, 400, 6, SAND).placed === 25')); +check('busy chutes and nozzle do not pause rain', run('spawnRain(0, W, 10) === 10')); +run(`flyers.length = 0; for (let k = 0; k < 3001; k++) toolBlow(330, 400, 0, SAND);`); +check('a long hold continues past the old shared limit', run('toolBlow(330, 400, 0, SAND).placed === 1')); +run(`flyers.length = 0; for (let k = 0; k < 3999; k++) toolBlow(330, 400, 0, SAND);`); +check('wide nozzle stops exactly at its own raised allowance', run('toolBlow(330, 400, 6, SAND).placed === 1 && flyers.length === 4000')); +check('nozzle limit explains the pause and keeps the bag intact', run(`(() => { + const before = bag[SAND], r = toolBlow(330, 400, 6, SAND); + return r.placed === 0 && r.status === 'Too much in the air.' && bag[SAND] === before; +})()`)); +check('nozzle allowance does not stop rain', run('spawnRain(0, W, 2501) === 2500')); +check('rain stops at its own allowance', run('spawnRain(0, W, 1) === 0')); +run('flyers.splice(0, 1)'); +check('nozzle resumes as soon as one own grain leaves flight', run('toolBlow(330, 400, 6, SAND).placed === 1')); +run('setCell(100*W+100, SAND, 20, 0); launchGrain(100*W+100, 100, 40, 0, 0)'); +check('production continues with both allowances full', run('flyers.length === 6501 && !flyers[6500].bag && !flyers[6500].rain')); +run(`flyers.length=0; spawnRain(0, W, 2500);`); +check('full rain allowance does not block the nozzle', run('toolBlow(330, 400, 6, SAND).placed === 25')); +run('flyers.length=0; spawnRain(0,W,2500); for(const f of flyers) f.age=900; flyTick();'); +check('rain can resume after its grains land', run('spawnRain(0,W,1) === 1')); +run('flyers.length=0; bag[SAND]=10000; bagTotal=10000;'); +run(`for(let k=0;k<3999;k++) toolBlow(330,400,0,SAND); toolVacuum(330,450,100,()=>true);`); +check('vacuum removal also frees nozzle allowance', run('toolBlow(330,400,6,SAND).placed > 1')); + done();