Every transport piece moves material at belt speed and capacity (t105)

This commit is contained in:
2026-10-30 03:34:39 -04:00
parent e605953661
commit 1740f598eb
20 changed files with 413 additions and 126 deletions
+61
View File
@@ -0,0 +1,61 @@
// Shared t105 measurement and screenshot layout. No generated terrain.
function transportFixture(kind, reverse = false) {
RS=7; nextPowerBuild=0; money=1e6; AMB.fill(20); temp.fill(20); hot.fill(0); weather.next=1e15;
const blocks=[];
const put=(c,v,x,y)=>blocks.push([c,v,x,y]);
const belt=(a,b,y)=>{for(let x=a;x<b;x++)put(BP_BELT,1,x,y);};
let inlet=24, outlet=24, start=[72,95], axis=0, sign=1, diagonal=false;
if(kind==='belt'||kind==='filter') {
belt(10,16,24);for(let x=16;x<26;x++)put(kind==='belt'?BP_BELT:BP_FILTER,kind==='belt'?1:128,x,24);belt(26,32,24);
} else if(kind==='uphill'||kind==='downhill'||kind==='screen') {
const g=kind==='uphill'?-1:1, by=g<0?34:20;
inlet=by+(g<0?1:0);outlet=by+9*g+(g>0?1:0);diagonal=true;
belt(10,16,inlet);for(let k=0;k<10;k++)put(kind==='screen'?BP_FILTER_DOWN:g<0?BP_BELT_UP:BP_BELT_DOWN,kind==='screen'?128:1,16+k,by+k*g);belt(26,32,outlet);
start=[72,beltFaceY(g<0?BP_BELT_UP:BP_BELT_DOWN,18,by+2*g,72)-1];
} else if(kind==='screw-right') {
inlet=25;outlet=25;belt(10,16,25);for(let x=16;x<26;x++)put(BP_SCREW,1,x,24);belt(26,32,25);start=[72,99];
} else if(kind==='screw-up'||kind==='screw-down') {
const up=kind==='screw-up';inlet=up?35:20;outlet=up?24:32;axis=1;sign=up?-1:1;
belt(10,16,inlet);for(let y=up?24:20;y<(up?35:31);y++)put(BP_SCREW,up?0:2,16,y);
for(let x=16;x<20;x++)put(BP_SCREW,1,x,up?23:31);belt(20,32,outlet);start=[65,up?128:88];
} else {
const up=kind==='screw-up-right';inlet=up?35:20;outlet=up?25:30;diagonal=true;
belt(10,16,inlet);for(let k=0;k<10;k++)put(BP_SCREW,up?4:5,16+k,(up?35:20)+k*(up?-1:1));belt(26,32,outlet);start=[72,up?133:89];
}
const shift=24-inlet;for(const b of blocks)b[3]+=shift;start[1]+=shift*4;outlet+=shift;inlet=24;
if(reverse)for(const b of blocks){b[2]=41-b[2];if(b[0]===BP_SCREW)b[1]=[0,3,2,1,7,6,5,4][b[1]];else {if(isSlopeBelt(b[0]))b[0]+=b[0]===BP_BELT_UP||b[0]===BP_FILTER_UP?1:-1;b[1]=isFilter(b[0])?0:0;}}
for(const [c,v,x,y] of blocks)placeBlock(c,v,x,y,true,{m:[CUORE],inv:0});
if(reverse)start[0]=167-start[0];
return {kind,reverse,inlet,outlet,start,axis,sign,diagonal,feedX:reverse?123:44,sink:reverse?40:128};
}
function transportFeed(s,depth) {
let added=0;
for(let l=1;l<=depth;l++){const i=(s.inlet*4-l)*W+s.feedX;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}
return added;
}
function transportCollect(s) {
let out=0;
for(let y=0;y<H;y++)for(let x=s.reverse?0:s.sink;x<(s.reverse?s.sink:W);x++){const i=y*W+x;if(mat[i]===SAND){out++;setCell(i,EMPTY,20,0);}}
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND&&(s.reverse?flyers[j].x<s.sink:flyers[j].x>=s.sink)){out++;flyers.splice(j,1);}
return out;
}
function transportChainFixture() {
RS=7; nextPowerBuild=0; money=1e6; AMB.fill(20); temp.fill(20); hot.fill(0); weather.next=1e15;
const belt=(a,b,y)=>{for(let x=a;x<b;x++)placeBlock(BP_BELT,1,x,y,true);};
belt(10,16,48);
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,16+k,47-k,true);
belt(22,30,42);
for(let y=32;y<42;y++)placeBlock(BP_SCREW,0,30,y,true);
for(let x=30;x<34;x++)placeBlock(BP_SCREW,1,x,31,true);
belt(34,40,32);
for(let x=40;x<46;x++)placeBlock(BP_FILTER,128,x,32,true,{m:[CUORE],inv:0});
belt(46,52,32);
for(let k=0;k<6;k++)placeBlock(BP_SCREW,4,52+k,32-k,true);
belt(58,66,26);
return {kind:'chain',reverse:false,inlet:48,feedX:44,sink:264,
joins:[['sidewall',64,192],['top belt',88,168],['lift',120,168],['lift outlet',136,128],['filter',160,128],['filter outlet',184,128],['diagonal screw',208,128],['final belt',232,104]]};
}
function transportPiles(s) {
return s.joins.map(([name,x,y])=>[name,(()=>{let n=0;for(let yy=y-20;yy<y;yy++)for(let xx=x-8;xx<x+4;xx++)if(mat[yy*W+xx]===SAND)n++;return n;})()]);
}
+8 -5
View File
@@ -5,11 +5,11 @@ const { check, done } = suite();
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
const screwLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_SCREW_U && part[i] <= P_SCREW_R && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
// grains that land in the sink region are counted and taken away, so outlets never back up
function rate(x0, x1, y0, y1, warm, meas) {
function rate(x0, x1, y0, y1, warm, meas, sample = '') {
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
for (let k = 0; k < warm; k++) run(`simTick(); ${sink}`);
for (let k = 0; k < warm; k++) run(`simTick(); ${sample} ${sink}`);
run('__got = 0');
for (let k = 0; k < meas; k++) run(`simTick(); ${sink}`);
for (let k = 0; k < meas; k++) run(`simTick(); ${sample} ${sink}`);
return run('__got') / (meas / 60);
}
@@ -116,8 +116,11 @@ fresh();
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
rect(run, 90, 130, 129, 195, run('SAND'));
run('for (let by = 20; by <= 37; by++) placeBlock(BP_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);');
const lr = rate(164, 319, 80, 119, 400, 300);
const liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
// Observe while the finite pit feeds the lift: faster turns can empty it before the final tick.
run('globalThis.__liftLanes = new Set();');
const lr = rate(164, 319, 80, 119, 400, 300,
'for(let y=76;y<80;y++)for(let x=120;x<160;x++)if(mat[y*W+x]===SAND)__liftLanes.add(y);');
const liftLanes = run('__liftLanes.size');
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
fresh();
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
+3 -2
View File
@@ -91,7 +91,7 @@ for (const v of [4,5,6,7]) for (const side of [-1,1]) for (const code of ['BP_BE
check(`${code} at diagonal ${v} end ${end} draws the shared join`,run(`(diagonalScrewJoins(20,20,${dv})>>${end*3}&7)===${kind}`));
check(`${code} at diagonal ${v} end ${end} receives every grain lane above its face`,run(`(()=>{for(let l=0;l<4;l++){const i=(80+l)*W+${side>0?83:80};if(diagonalScrewNext(i,SCREW_PARTS[${dv}])!==${ny*4-1}*W+${side>0?84:79})return false;}return true;})()`));
putBar(80,81);
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=8;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=12;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
}
// Flat carriers enter every diagonal at its same-row inlet; blocked mouths must wait safely.
for(const v of [4,5,6,7])for(const filter of [false,true]) {
@@ -137,7 +137,8 @@ for (const v of [0, 4, 5]) {
rates.push(result.out/10);
}
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
check('diagonals match straight throughput per block',rates.every(n=>n===120),rates);
// t105 slows diagonal path travel; four lanes now carry the belt's heavy rate.
check('straight screw retains its full lanes; diagonals match the belt heavy rate',rates[0]===120 && rates.slice(1).every(n=>n>=81&&n<=99),rates);
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
const belt=line(4);
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,32,true);');
+6 -6
View File
@@ -67,7 +67,7 @@ for(const grade of [-1,1]) {
const released=collect(s,800);
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
line(grade);
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=2;tickStamp++;machinesTick();`);
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=4;tickStamp++;machinesTick();`);
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
@@ -179,22 +179,22 @@ for (const grade of [-1,1]) {
run('rebuildPower();slopeCleatsTick(false);');
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
check(`grade ${grade} ${grade<0?'bare face slides':'carrying bed waits'} between steps without stamping the load`,run(`(()=>{
for(let x=72;x<96;x++){
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
if(cleatMask[i] || cleatMask[i+W+${grade}])continue;
setCell(i,SAND,20,0);tick=3;tickStamp++;machinesTick();
const unstamped=stamp[i]!==tickStamp;
for(let k=0;k<200 && mat[i]===SAND;k++)powder(i,x,y,SAND);
return unstamped && mat[i]===EMPTY;
return unstamped && ${grade<0?"mat[i]===EMPTY":"mat[i]===SAND"};
} return false;
})()`));
}
fresh();
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=SCREEN_NUDGE_TICKS-1;screenNudge(80*W+80,1,null);');
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=2;screenNudge(80*W+80,1,null);');
check('screen waits between its configured nudges',run('mat[79*W+80]===GRAVEL && stamp[79*W+80]!==tickStamp'));
run('tick++;screenNudge(80*W+80,1,null);');
check('screen nudges just its mesh contact downhill at the configured rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
run('tick+=2;screenNudge(80*W+80,1,null);');
check('screen carries its mesh contact downhill at the shared diagonal rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
fresh();run('placeBlock(BP_BELT_UP,1,20,32,true);');
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
// The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
+15
View File
@@ -67,6 +67,21 @@ function hash() {
run('const forwardChunks=runChunks;runChunks=(chunks,pass,work)=>forwardChunks(chunks.slice().reverse(),pass,work);for(let k=0;k<320;k++)simTick();runChunks=forwardChunks;');
assert.strictEqual(hash(),expected,'chunk order within a colour must not matter');
console.log('PASS reversing chunks within every pass preserves all bytes and effects');
run(require('fs').readFileSync('test/fixtures/t105-transport.js','utf8'));
let chainExpected;
for(const n of [0,1,2,8]) {
flatWorld(run,320,256,240);
run(`WS=456;resetWeather();seen.fill(0);ledger={};EV.length=0;
for(const k of Object.keys(stats))stats[k]=0;resetEconomy();
globalThis.ts=transportChainFixture();`);
const stop=n?await enableThreads(run,n):null;
try {
run('for(let k=0;k<1000;k++){transportFeed(ts,16);simTick();transportCollect(ts);}');
const actual=hash();if(!chainExpected)chainExpected=actual;
assert.strictEqual(actual,chainExpected,`mixed transport chain differs with ${n} workers`);
console.log(`PASS mixed transport chain is identical with ${n} workers (${actual.slice(0,16)})`);
} finally {if(stop)await stop();}
}
const {execFileSync}=require('child_process');
const fallback=execFileSync(process.execPath,['-e',`
global.SharedArrayBuffer=undefined;
+30
View File
@@ -0,0 +1,30 @@
// t105: compare every direction with the unchanged t45 belt and exercise the complete line.
const {kinds,speed,throughput,chain,fresh,run}=require('../tools/experiments/transport-balance');
const {suite}=require('./harness');
const {check,done}=suite();
for(const kind of kinds)for(const reverse of [false,true]){
const name=kind+(reverse?' mirrored':'');
if(kind==='screw-up-right'||kind==='screw-down-right') {
fresh(kind,reverse);
check(`${name}: both belt joins are mutual`,run(`(()=>{let n=0;for(let b=0;b<NB;b++)if(blockP[b]===BP_SCREW)for(let e=0;e<2;e++){const j=endJoin(b%BW,(b/BW)|0,e);if(j>=0&&blockP[j>>1]===BP_BELT)n++;}return n===2;})()`));
}
const v=speed(kind,reverse),bar=speed(kind,reverse,true);
const normal=throughput(kind,reverse,1),heavy=throughput(kind,reverse,16);
check(`${name}: grain path speed matches belt`,Math.abs(v-30)<=3,v);
check(`${name}: bar path speed matches belt`,Math.abs(bar-30)<=3,bar);
check(`${name}: normal feed matches belt`,Math.abs(normal-30)<=3,normal);
check(`${name}: heavy feed matches belt`,Math.abs(heavy-90)<=9,heavy);
}
fresh('screw-up-right',false);
run('placeBlock(BP_WALL,RSTONE,26,13,true);for(let k=0;k<1000;k++){transportFeed(ts,16);simTick();transportCollect(ts);}');
check('a blocked raised outlet holds its load and warns',run('transportStalled(15*BW+25)'));
run('removeBlock(26,13);');
const released=run('(()=>{let n=0;for(let k=0;k<600;k++){transportFeed(ts,16);simTick();n+=transportCollect(ts);}return n;})()');
check('clearing the raised outlet resumes delivery',released>0,released);
const line=chain();
check('mixed chain carries the full heavy belt feed',line.heavy>=89&&line.heavy<=91,JSON.stringify(line));
check('mixed chain has mutual diagonal joins',line.joined);
check('mixed chain conserves every supplied grain',line.added===line.accounted,`${line.added}/${line.accounted}`);
check('mixed chain spills no grains below its joins',line.stray===0,line.stray);
check('mixed chain has no growing piles at joins',line.late.every(([name,n],i)=>n<=line.early[i][1]+12),JSON.stringify(line.late));
done();