Nozzle and rain count only their own grains in flight; factory launches uncapped (t95)

This commit is contained in:
2026-10-29 18:12:47 -04:00
parent 3b427aeecc
commit d936f65553
5 changed files with 62 additions and 3 deletions
+6
View File
@@ -610,3 +610,9 @@ above, including:
cells. Exclude dedicated construction materials from give, discovery and blowing. On load, cells. Exclude dedicated construction materials from give, discovery and blowing. On load,
clear loose construction grains and their bag/nozzle inventory; preserve owned blocks, 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. 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.
+14
View File
@@ -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 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. 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) ## 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 - PARTMAT now blocks give/discovery, inventory/refunds and single/mixed blowing. Load clears
+5 -2
View File
@@ -90,10 +90,13 @@ function toolBlow(cx, cy, r, m, mix) {
cx = nz.ox + nz.dx * nz.dist - 0.5; cx = nz.ox + nz.dx * nz.dist - 0.5;
cy = nz.oy + nz.dy * 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 // nozzle 0 is a trickle for careful work, the widest a torrent
const rate = 1 + r + ((r * r) >> 1); 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; let placed = 0;
for (let k = 0; k < n; k++) { for (let k = 0; k < n; k++) {
const gm = pickM ? pickM() : m; const gm = pickM ? pickM() : m;
+4 -1
View File
@@ -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; } 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 // 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) { 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; let n = Math.floor(k) + (wrnd() < k - Math.floor(k) ? 1 : 0), made = 0;
n = Math.min(n, 2500 - airborne);
for (; n > 0; n--) { for (; n > 0; n--) {
const x = Math.floor(x0 + wrnd() * (x1 - x0)); const x = Math.floor(x0 + wrnd() * (x1 - x0));
if (x < 0 || x >= W) continue; if (x < 0 || x >= W) continue;
+33
View File
@@ -29,4 +29,37 @@ for (const distance of [run('3 * REACH'), run('5 * REACH')]) {
const nearby = blowSand(30, 0); 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)); 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(); done();