Falling grains cast no shadow (t37)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 20:11:57 -04:00
co-authored by Claude Opus 5.5
parent f744d786f1
commit 7d1be16094
4 changed files with 30 additions and 13 deletions
+2 -2
View File
@@ -352,9 +352,9 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont - functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
- 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 - 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 (103 lines) ### src/render/light.js (104 lines)
Propagated coloured light from glowing and burning cells. Propagated coloured light from glowing and burning cells.
- functions: builtForLight, onBeltForLight, computeLight - functions: builtForLight, looseForLight, computeLight
### src/render/world.js (332 lines) ### src/render/world.js (332 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
+8
View File
@@ -4,6 +4,14 @@ 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-25: t37 follow-up: falling grains cast no shadows (Claude Code, Codex)
- The owner saw a single falling grain darken a column of sky. `onBeltForLight` became
`looseForLight` (`src/render/light.js`): a loose stack that ends in air or gas within 12 cells
(falling) is see-through, as are belt and filter loads; stacks resting on any solid or a tube
part still shade.
- `test/t_light.js`; `run_all.py --browser` ALL PASSED.
## 2026-10-25: t37 follow-up: belt loads cast no shadows (Claude Code, Codex) ## 2026-10-25: t37 follow-up: belt loads cast no shadows (Claude Code, Codex)
- The owner saw grains riding a belt overhead still shading the ground. `onBeltForLight(i)` in - The owner saw grains riding a belt overhead still shading the ground. `onBeltForLight(i)` in
+5 -4
View File
@@ -5,11 +5,12 @@
function builtForLight(i, x, y) { function builtForLight(i, x, y) {
return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i]; return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i];
} }
function onBeltForLight(i) { function looseForLight(i) {
for (let depth = 0; depth <= 12 && i < N; depth++, i += W) { for (let depth = 0; depth <= 12 && i < N; depth++, i += W) {
const m = mat[i], ph = PH[m]; const m = mat[i], ph = PH[m];
if (m === BELTL || m === BELTR || part[i] === P_FILTER) return true; if (m === BELTL || m === BELTR || part[i] === P_FILTER) return true;
if (ph !== PH_POWDER && ph !== PH_LIQUID) return false; if (part[i]) return false;
if (ph !== PH_POWDER && ph !== PH_LIQUID) return m === EMPTY || ph === PH_GAS;
} }
return false; return false;
} }
@@ -40,7 +41,7 @@ function computeLight() {
let y = roofY[x]; let y = roofY[x];
for (; y < roofEnd; y++) { for (; y < roofEnd; y++) {
const i = y * W + x, ph = PH[mat[i]]; const i = y * W + x, ph = PH[mat[i]];
if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y) && !((ph === PH_POWDER || ph === PH_LIQUID) && onBeltForLight(i))) break; if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y) && !((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i))) break;
} }
lightRoof[lx] = y; lightRoof[lx] = y;
} }
@@ -50,7 +51,7 @@ function computeLight() {
const wx = (lx0 + lx) * res, k = ly * lw + lx; const wx = (lx0 + lx) * res, k = ly * lw + lx;
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; } if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
const i = wy * W + wx, m = mat[i], ph = PH[m]; const i = wy * W + wx, m = mat[i], ph = PH[m];
la[k] = builtForLight(i, wx, wy) || ((ph === PH_POWDER || ph === PH_LIQUID) && onBeltForLight(i)) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol; la[k] = builtForLight(i, wx, wy) || ((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i)) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
let r = 0, g = 0, b = 0; let r = 0, g = 0, b = 0;
if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; } if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; } else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
+15 -7
View File
@@ -44,9 +44,9 @@ for (const mode of ['full', 'fast']) {
} }
check('lighting leaves the simulation roof unchanged', run('simRoof.every((y, x) => roofY[x] === y) && roofY[80] === 80')); check('lighting leaves the simulation roof unchanged', run('simRoof.every((y, x) => roofY[x] === y) && roofY[80] === 80'));
for (const material of ['STONE', 'SAND', 'WATER']) { for (const material of ['STONE', 'SAND', 'WATER']) {
run(`setCell(96 * W + 80, ${material}, AMB[96], F_NAT); updateRoof(); computeLight();`); run(`setCell(96 * W + 80, ${material}, AMB[96], F_NAT); setCell(97 * W + 80, STONE, AMB[97], F_NAT); updateRoof(); computeLight();`);
check(`${material} above the view still blocks daylight`, run('lightAt(R.lr, 80, 144) < 0.95')); check(`${material} above the view still blocks daylight`, run('lightAt(R.lr, 80, 144) < 0.95'));
run('setCell(96 * W + 80, EMPTY, AMB[96], 0);'); run('setCell(96 * W + 80, EMPTY, AMB[96], 0); setCell(97 * W + 80, EMPTY, AMB[97], 0);');
} }
run('updateRoof(); computeLight();'); run('updateRoof(); computeLight();');
check('removing an obstruction restores daylight', run('lightAt(R.lr, 80, 144) === 1')); check('removing an obstruction restores daylight', run('lightAt(R.lr, 80, 144) === 1'));
@@ -70,14 +70,17 @@ run(`
setCell(80 * W + x, STONE, AMB[80], F_NAT); setCell(80 * W + x, STONE, AMB[80], F_NAT);
for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0); for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0);
} }
for (let y = 40; y <= 76; y += 2) setCell(y * W + 152, SAND, AMB[y], 0);
updateRoof(); updateRoof();
`); `);
for (const mode of ['full', 'fast']) { for (const mode of ['full', 'fast']) {
run(`SET.lighting = '${mode}'; computeLight();`); run(`SET.lighting = '${mode}'; computeLight();`);
for (const channel of ['lr', 'lg', 'lb']) { for (const channel of ['lr', 'lg', 'lb']) {
const [belt, open, stone] = run(`[lightAt(R.${channel}, 80, 84), lightAt(R.${channel}, 152, 84), lightAt(R.${channel}, 232, 84)]`); const [belt, open, stone] = run(`[lightAt(R.${channel}, 80, 84), lightAt(R.${channel}, 160, 84), lightAt(R.${channel}, 232, 84)]`);
check(`${mode} ${channel}: a loaded belt leaves daylight below it`, Math.abs(belt - open) < 1e-6, `${belt} / ${open}`); check(`${mode} ${channel}: a loaded belt leaves daylight below it`, Math.abs(belt - open) < 1e-6, `${belt} / ${open}`);
check(`${mode} ${channel}: sand on natural stone still shades below`, stone < open * 0.95, `${stone} / ${open}`); check(`${mode} ${channel}: sand on natural stone still shades below`, stone < open * 0.95, `${stone} / ${open}`);
const [streamGround, openGround] = run(`[lightAt(R.${channel}, 152, 160), lightAt(R.${channel}, 160, 160)]`);
check(`${mode} ${channel}: a falling stream leaves ground as bright as open sky`, Math.abs(streamGround - openGround) < 1e-6, `${streamGround} / ${openGround}`);
} }
run('setCell(76 * W + 80, WATER, AMB[76], 0); updateRoof(); computeLight();'); run('setCell(76 * W + 80, WATER, AMB[76], 0); updateRoof(); computeLight();');
const a = run('lightAt(R.latt, 80, 76)'); const a = run('lightAt(R.latt, 80, 76)');
@@ -89,19 +92,24 @@ for (const [name, support, depth, gap, expected] of [
['right belt', 'mat[i] = BELTR', 12, '', true], ['right belt', 'mat[i] = BELTR', 12, '', true],
['filter', 'part[i] = P_FILTER', 12, '', true], ['filter', 'part[i] = P_FILTER', 12, '', true],
['deep heap', 'mat[i] = BELTR', 13, '', false], ['deep heap', 'mat[i] = BELTR', 13, '', false],
['air gap', 'mat[i] = BELTR', 3, 'EMPTY', false], ['air gap', 'mat[i] = BELTR', 3, 'EMPTY', true],
['gas gap', 'mat[i] = BELTR', 3, 'STEAM', false], ['gas gap', 'mat[i] = BELTR', 3, 'STEAM', true],
['12 falling grains', '', 12, '', true],
['13 falling grains', '', 13, '', false],
['natural ledge', 'mat[i] = STONE; flags[i] = F_NAT', 3, '', false], ['natural ledge', 'mat[i] = STONE; flags[i] = F_NAT', 3, '', false],
['wall floor', 'mat[i] = STONE', 3, '', false], ['wall floor', 'mat[i] = STONE', 3, '', false],
['tube', 'part[i] = P_TR', 3, '', false], ['tube', 'part[i] = P_TR', 3, '', false],
['loaded tube', 'mat[i] = SAND; part[i] = P_TR', 3, '', false],
]) { ]) {
run(` run(`
for (let y = 40; y <= 53; y++) { const i = y * W + 300; mat[i] = SAND; part[i] = 0; flags[i] = 0; } for (let y = 40; y <= 53; y++) { const i = y * W + 300; mat[i] = SAND; part[i] = 0; flags[i] = 0; }
{ const i = (40 + ${depth}) * W + 300; mat[i] = EMPTY; mat[i + W] = BELTR; ${support}; } { const i = (40 + ${depth}) * W + 300; mat[i] = EMPTY; mat[i + W] = BELTR; ${support}; }
${gap ? `mat[41 * W + 300] = ${gap};` : ''} ${gap ? `mat[41 * W + 300] = ${gap};` : ''}
`); `);
check(`${name}: belt load detection`, run('onBeltForLight(40 * W + 300)') === expected); check(`${name}: loose cell detection`, run('looseForLight(40 * W + 300)') === expected);
} }
run('mat[40 * W + 300] = SAND; mat[41 * W + 300] = EMPTY;');
check('a single grain in mid-air is see-through', run('looseForLight(40 * W + 300)'));
run('mat[(H - 1) * W + 300] = SAND;'); run('mat[(H - 1) * W + 300] = SAND;');
check('belt load detection stops at the world bottom', run('!onBeltForLight((H - 1) * W + 300)')); check('loose cell detection stops at the world bottom', run('!looseForLight((H - 1) * W + 300)'));
done(); done();