Fog of war: unexplored is black, fades ease into it with no stepped rim (t17)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:59:00 -04:00
co-authored by Claude Opus 5.5
parent a18dcda8a4
commit 14194cb4be
7 changed files with 36 additions and 17 deletions
+1
View File
@@ -239,6 +239,7 @@ above, including:
the ground, so the owner couldn't see what was around them or where to dig. the ground, so the owner couldn't see what was around them or where to dig.
As tuned: clear to 12 grains, fading out by 24, with a grainy edge; the owner found the first As tuned: clear to 12 grains, fading out by 24, with a grainy edge; the owner found the first
version's narrow fade too sharp. version's narrow fade too sharp.
Unexplored ground is pure black, not dark grey (the owner: "should be black anyway").
## Ideas not built yet ## Ideas not built yet
+2 -2
View File
@@ -221,7 +221,7 @@ World state: the per-cell typed arrays, alloc(), ambient temperature by depth, w
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx - 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 - 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/fog.js (51 lines) ### src/sim/fog.js (52 lines)
Exploration only: never changes grains or the random stream. Exploration only: never changes grains or the random stream.
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH - variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
@@ -343,7 +343,7 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
Propagated coloured light from glowing and burning cells. Propagated coloured light from glowing and burning cells.
- functions: computeLight - functions: computeLight
### src/render/world.js (299 lines) ### src/render/world.js (304 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld - functions: fogColor, drawWorld
- variables: TUBE_CLASS - variables: TUBE_CLASS
+12
View File
@@ -4,6 +4,18 @@ 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-24: t17 follow-up: no jagged rim, unexplored is black (Claude Code)
- The owner still saw a jagged grey edge. Causes found by sampling pixels: the fade went to 0
(black) while unexplored was drawn (12, 10, 10), leaving a dark stepped rim; the ±40 noise lit
the faintest cells; rays that reached rock already dim dropped to 0 in 2–3 grains.
- `src/render/world.js`: unexplored and off-map are pure black (`OUT`, smooth-look buffers);
partial fog eases into black along a smoothstep, with noise scaled by s·(1−s). `src/sim/fog.js`:
sight into rock fades over 8 grains as a fraction of its brightness.
- `test/play.py`: the three unexplored-colour checks expect black (owner's decision).
`docs/systems/fog.md` updated. Screenshots `shots/fog-dig-black-{pixel,smooth,nolight}.png`.
- Tests: `run_all.py --browser` passed except `t_smooth` once at load 7 (passed on rerun).
## 2026-10-24: t17 follow-up: softer fog edges (Claude Code, Codex) ## 2026-10-24: t17 follow-up: softer fog edges (Claude Code, Codex)
- The owner found the edges sharp and stepped. `src/sim/fog.js`: sense ring 24 grains with a - The owner found the edges sharp and stepped. `src/sim/fog.js`: sense ring 24 grains with a
+4 -4
View File
@@ -28,10 +28,10 @@ immediately from the bot.
## Drawing and setting ## Drawing and setting
Both looks and the thermal lens sample fog bilinearly. Partial values get stable per-grain Both looks and the thermal lens sample fog bilinearly. Partly seen grains darken toward black
noise of about ±40, clamped to 1–255, to break up stepped edges. Unseen and fully seen along a smoothstep curve, with stable per-grain noise (up to about ±38 mid-fade, none at either
grains are unchanged. Seen grains use the usual drawing; end) to break up the half-resolution steps. Fully seen grains use the usual drawing; unseen
unseen grains use the out-of-world colour and smooth-look buffers, hiding their shapes. grains are pure black (as is everything off the map) in both looks, hiding their shapes.
Partial values dim foreground, background and trees. Bloom sources need at least 128; Partial values dim foreground, background and trees. Bloom sources need at least 128;
bars, flyers, rain and particles need at least 64. Hover info and the cursor label say bars, flyers, rain and particles need at least 64. Hover info and the cursor label say
only “Unexplored” below 64. only “Unexplored” below 64.
+11 -6
View File
@@ -23,7 +23,7 @@ function drawWorld() {
} }
} }
const useFog = SET.fog; const useFog = SET.fog;
const OUT = sm ? 0x040a0a0c : 0xff0a0a0c; const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
const lighting = SET.lighting !== 'off' && !R.lens; const lighting = SET.lighting !== 'off' && !R.lens;
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight(); if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
const grain = SET.grain, incand = SET.incandescence; const grain = SET.grain, incand = SET.incandescence;
@@ -35,22 +35,27 @@ function drawWorld() {
for (let by = 0; by < bh; by++) { for (let by = 0; by < bh; by++) {
const y = vy0 + by; const y = vy0 + by;
const row = by * bw; const row = by * bw;
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; } if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW; const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
const yw = y * W; const yw = y * W;
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y]; let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; } if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
for (let bx = 0; bx < bw; bx++) { for (let bx = 0; bx < bw; bx++) {
const x = vx0 + bx; const x = vx0 + bx;
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; } if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
let fv = 255; let fv = 255;
if (useFog) { if (useFog) {
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1); const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1]; const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft; if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; } if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
// Stable grain noise breaks up the half-resolution fog's stepped edges. // ease the fade into black (smoothstep) and break up the half-resolution steps with grain
if (fv < 255) fv = Math.max(1, Math.min(255, fv + (noiseB[yw + x] - 128) * (40 / 128))); // noise that is strongest mid-fade and nothing at either end, so the outer edge has no rim
if (fv < 255) {
let s = fv / 255;
s = Math.min(1, Math.max(0, s + (noiseB[yw + x] - 128) * (0.6 / 128) * s * (1 - s)));
fv = Math.max(1, 255 * s * s * (3 - 2 * s));
}
} }
const i = yw + x, m = mat[i], nz = noiseB[i] - 128; const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
const T = temp[i]; const T = temp[i];
+2 -1
View File
@@ -25,7 +25,8 @@ function fogLook(cx, cy) {
if (x < 0 || y < 0 || x >= W || y >= H) break; if (x < 0 || y < 0 || x >= W || y >= H) break;
const ph = PH[mat[y * W + x]]; const ph = PH[mat[y * W + x]];
if (hit < 0 && (ph === PH_SOLID || ph === PH_POWDER)) hit = d; if (hit < 0 && (ph === PH_SOLID || ph === PH_POWDER)) hit = d;
const v = Math.min(255, (48 - d) * 255 / 16, hit < 0 ? 255 : (8 - d + hit) * 255 / 8); // into rock, sight fades out over 8 grains from however bright it arrived
const v = Math.min(255, (48 - d) * 255 / 16) * (hit < 0 ? 1 : (8 - d + hit) / 8);
if (v <= 0) break; if (v <= 0) break;
fogMark(x, y, v); fogMark(x, y, v);
} }
+4 -4
View File
@@ -807,7 +807,7 @@ with sync_playwright() as p:
}""") }""")
fp.wait_for_timeout(300) fp.wait_for_timeout(300)
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()" sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
check('unexplored pixel is out-of-world black', fp.evaluate(sample) == 0x0a0a0c) check('unexplored pixel is black', fp.evaluate(sample) == 0)
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()")) check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()")) check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})")) check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
@@ -821,13 +821,13 @@ with sync_playwright() as p:
}""")) }"""))
fp.screenshot(path=str(out / 'fog-pixel.png')) fp.screenshot(path=str(out / 'fog-pixel.png'))
fp.evaluate('R.lens = true') fp.evaluate('R.lens = true')
check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0x0a0a0c) check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0)
fp.evaluate('R.lens = false; SET.fog = false') fp.evaluate('R.lens = false; SET.fog = false')
check('fog toggle shows the same rock', fp.evaluate(sample) != 0x0a0a0c) check('fog toggle shows the same rock', fp.evaluate(sample) != 0)
fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000") fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000")
fp.wait_for_timeout(500) fp.wait_for_timeout(500)
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok')) check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x040a0a0c && R.backPx[k]===0x000a0a0c && R.treePx[k]===0 && R.fieldPx[k]===255})()")) check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x04000000 && R.backPx[k]===0 && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
fp.screenshot(path=str(out / 'fog-smooth.png')) fp.screenshot(path=str(out / 'fog-smooth.png'))
fb.close() fb.close()