Faster roof scan and heat pass with identical results (t21)
- The roof is rescanned only in area columns that changed since the last scan: a quiet Huge world goes from 4.60 to 1.88 ms per scan (every 20 ticks). - The heat pass skips react() for solids and powders below their first reaction threshold: Huge world digging 2.54 -> 2.34 ms/tick, quiet 1.39 -> 1.18. - Every suite's output is identical apart from timing lines; seeds make the same worlds. - test/t_sim_optimizations.js checks the dirty roof against a full scan and the heat loop against the reference loop byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+8
-7
@@ -216,25 +216,25 @@ Starter furnace designs and the milestone list shown in the notebook.
|
|||||||
- functions: smelterBlueprint
|
- functions: smelterBlueprint
|
||||||
- variables: STARTER_DESIGNS, MILESTONES
|
- variables: STARTER_DESIGNS, MILESTONES
|
||||||
|
|
||||||
### src/sim/world.js (98 lines)
|
### src/sim/world.js (101 lines)
|
||||||
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
||||||
- 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, powderArea, 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, powderArea, roofDirty, 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 (52 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
|
||||||
|
|
||||||
### src/sim/cells.js (206 lines)
|
### src/sim/cells.js (207 lines)
|
||||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
||||||
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, wakeBotPowder, simMove
|
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, wakeBotPowder, simMove
|
||||||
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM
|
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM
|
||||||
|
|
||||||
### src/sim/heat.js (246 lines)
|
### src/sim/heat.js (265 lines)
|
||||||
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
||||||
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
||||||
- variables: POROUS, SOIL, HOTLIQ, ALLOY
|
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
||||||
|
|
||||||
### src/sim/machines.js (148 lines)
|
### src/sim/machines.js (148 lines)
|
||||||
Machines: the power network, footprints, placing and removing machines.
|
Machines: the power network, footprints, placing and removing machines.
|
||||||
@@ -316,9 +316,9 @@ The bot: movement and collision with the world.
|
|||||||
- functions: botBlocked, botOverlapSolid, botSlide, botStep
|
- functions: botBlocked, botOverlapSolid, botSlide, botStep
|
||||||
- variables: SLIDE_MAX, SLIDE_STEP
|
- variables: SLIDE_MAX, SLIDE_STEP
|
||||||
|
|
||||||
### src/sim/ledger.js (43 lines)
|
### src/sim/ledger.js (54 lines)
|
||||||
Milestone checks (the ledger) and the sky roof line used for cooling.
|
Milestone checks (the ledger) and the sky roof line used for cooling.
|
||||||
- functions: checkLedger, updateRoof
|
- functions: checkLedger, updateRoof, updateDirtyRoof, scanRoof
|
||||||
|
|
||||||
### src/sim/weather.js (87 lines)
|
### src/sim/weather.js (87 lines)
|
||||||
Weather: clouds from steam, rain and snow, and the world water count.
|
Weather: clouds from steam, rain and snow, and the world water count.
|
||||||
@@ -475,6 +475,7 @@ The frame loop and boot.
|
|||||||
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
|
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
|
||||||
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
|
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
|
||||||
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
|
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
|
||||||
|
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
|
||||||
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
|
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
|
||||||
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
||||||
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ 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: t21 phase 3: speed-ups that change nothing (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Profiled a Huge world (6144x2048) headless and in the browser. After a load every area is
|
||||||
|
awake: 8-15 ms/tick for the first 600 ticks, settling to about 0.7 ms/tick after 3 minutes. In
|
||||||
|
the browser, drawing is cheap; the full roof scan every 20 ticks was the largest single item.
|
||||||
|
- Kept: `updateDirtyRoof()` (`src/sim/ledger.js`) rescans only area columns marked by
|
||||||
|
`wakeXY`/`wakeHeatXY`/`simMove`/`heatChunk` (`roofDirty`); `updateRoof()` is still the full scan
|
||||||
|
for setup code. Quiet Huge world 4.60 -> 1.88 ms per scan, digging 4.67 -> 3.48.
|
||||||
|
`heatChunk` skips `react()` for solids and powders below their first reaction threshold
|
||||||
|
(`REACT_MIN`). Huge, ms/tick: digging 2.54 -> 2.34, quiet 1.39 -> 1.18.
|
||||||
|
- Reverted (no consistent gain): inlining `condPair`, one shared part rule for `enter`/`passable`.
|
||||||
|
- `test/t_sim_optimizations.js`: the dirty roof matches a full scan; `heatChunk` matches the
|
||||||
|
reference loop in `test/fixtures/heat-chunk.js` byte for byte, random state included (catches a
|
||||||
|
reaction added to `react()` but not to `REACT_MIN`).
|
||||||
|
- Every suite's output identical before and after apart from timings; seeds 777 and 4242
|
||||||
|
generate the same worlds. `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
## 2026-10-24: t21: piles near the bot sort quickly (Claude Code, Codex)
|
## 2026-10-24: t21: piles near the bot sort quickly (Claude Code, Codex)
|
||||||
|
|
||||||
- Owner: "a pile should sort when the player is nearby. the sorting shouldnt take long anyway."
|
- Owner: "a pile should sort when the player is nearby. the sorting shouldnt take long anyway."
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ function simMove() {
|
|||||||
for (let k = 0; k < CW; k++) {
|
for (let k = 0; k < CW; k++) {
|
||||||
const cx = ltr ? k : CW - 1 - k;
|
const cx = ltr ? k : CW - 1 - k;
|
||||||
if (!awake[rowBase + cx]) continue;
|
if (!awake[rowBase + cx]) continue;
|
||||||
|
roofDirty[cx] = 1;
|
||||||
const x0 = cx << 5, x1 = Math.min(W, x0 + 32);
|
const x0 = cx << 5, x1 = Math.min(W, x0 + 32);
|
||||||
// alternate rows scan left to right and right to left, so nothing drifts one way
|
// alternate rows scan left to right and right to left, so nothing drifts one way
|
||||||
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
|
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
|
||||||
|
|||||||
+20
-1
@@ -7,6 +7,20 @@ function airflowAt(i) { const a = airS[i]; return a !== 0 && ((curAir() - a) & 0
|
|||||||
const POROUS = new Uint8Array(NMAT); POROUS[ASH] = 1; POROUS[SLAG] = 1; POROUS[COAL] = 1; POROUS[CHARCOAL] = 1;
|
const POROUS = new Uint8Array(NMAT); POROUS[ASH] = 1; POROUS[SLAG] = 1; POROUS[COAL] = 1; POROUS[CHARCOAL] = 1;
|
||||||
const SOIL = new Uint8Array(NMAT); SOIL[DIRT] = 1; SOIL[GRASS] = 1; SOIL[LDIRT] = 1;
|
const SOIL = new Uint8Array(NMAT); SOIL[DIRT] = 1; SOIL[GRASS] = 1; SOIL[LDIRT] = 1;
|
||||||
const HOTLIQ = new Uint8Array(NMAT); for (const m of [MAGMA, MCU, MFE, MST, MGL, MAG, MAU, MBZ, MBS, MSLAG]) HOTLIQ[m] = 1;
|
const HOTLIQ = new Uint8Array(NMAT); for (const m of [MAGMA, MCU, MFE, MST, MGL, MAG, MAU, MBZ, MBS, MSLAG]) HOTLIQ[m] = 1;
|
||||||
|
// Cold solids and powders cannot react below their first applicable threshold.
|
||||||
|
const REACT_MIN = new Float64Array(NMAT).fill(Infinity);
|
||||||
|
for (let m = 0; m < NMAT; m++) {
|
||||||
|
if (PH[m] === PH_LIQUID || PH[m] === PH_GAS) REACT_MIN[m] = -Infinity;
|
||||||
|
else {
|
||||||
|
if (IGN[m] !== 0) REACT_MIN[m] = Math.min(REACT_MIN[m], IGN[m]);
|
||||||
|
if (MELT[m] !== 0) REACT_MIN[m] = Math.min(REACT_MIN[m], MELT[m]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REACT_MIN[CLAY] = Math.min(REACT_MIN[CLAY], 880);
|
||||||
|
REACT_MIN[LCLAY] = Math.min(REACT_MIN[LCLAY], 880);
|
||||||
|
REACT_MIN[WOOD] = Math.min(REACT_MIN[WOOD], 380);
|
||||||
|
REACT_MIN[LWOOD] = Math.min(REACT_MIN[LWOOD], 380);
|
||||||
|
REACT_MIN[GRASS] = Math.min(REACT_MIN[GRASS], 260);
|
||||||
// Alloys: a molten additive grain turns neighbouring molten copper into the alloy,
|
// Alloys: a molten additive grain turns neighbouring molten copper into the alloy,
|
||||||
// and is itself used up after about this many (the ratio: bronze ~1:7 tin, brass ~1:2 zinc).
|
// and is itself used up after about this many (the ratio: bronze ~1:7 tin, brass ~1:2 zinc).
|
||||||
const ALLOY = { [MSN]: { into: MBZ, per: 7 }, [MZN]: { into: MBS, per: 2 } };
|
const ALLOY = { [MSN]: { into: MBZ, per: 7 }, [MZN]: { into: MBS, per: 2 } };
|
||||||
@@ -51,6 +65,7 @@ function heatPass() {
|
|||||||
function heatChunk(c, sc) {
|
function heatChunk(c, sc) {
|
||||||
const cx = c % CW, cy = (c / CW) | 0;
|
const cx = c % CW, cy = (c / CW) | 0;
|
||||||
const x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
|
const x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
|
||||||
|
roofDirty[cx] = 1;
|
||||||
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
|
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
|
||||||
let maxDev = 0, burning = false;
|
let maxDev = 0, burning = false;
|
||||||
for (let y = y0; y < y1; y++) {
|
for (let y = y0; y < y1; y++) {
|
||||||
@@ -80,7 +95,11 @@ function heatChunk(c, sc) {
|
|||||||
} else rel = RELAX[m];
|
} else rel = RELAX[m];
|
||||||
t += (amb - t) * rel * sc;
|
t += (amb - t) * rel * sc;
|
||||||
temp[i] = t;
|
temp[i] = t;
|
||||||
if (m !== EMPTY) { react(i, x, y, m); m = mat[i]; t = temp[i]; }
|
if (m !== EMPTY) {
|
||||||
|
// Test the stored Float32 temperature, just as react() reads it.
|
||||||
|
if ((flags[i] & F_BURN) || !(temp[i] < REACT_MIN[m])) react(i, x, y, m);
|
||||||
|
m = mat[i]; t = temp[i];
|
||||||
|
}
|
||||||
const dv = t > amb ? t - amb : amb - t;
|
const dv = t > amb ? t - amb : amb - t;
|
||||||
if (dv > maxDev) maxDev = dv;
|
if (dv > maxDev) maxDev = dv;
|
||||||
if (flags[i] & F_BURN) burning = true;
|
if (flags[i] & F_BURN) burning = true;
|
||||||
|
|||||||
+12
-1
@@ -35,7 +35,18 @@ function checkLedger() {
|
|||||||
|
|
||||||
// Topmost non-air cell of each column: air above it can shed heat to the sky.
|
// Topmost non-air cell of each column: air above it can shed heat to the sky.
|
||||||
function updateRoof() {
|
function updateRoof() {
|
||||||
for (let x = 0; x < W; x++) {
|
scanRoof(0, W);
|
||||||
|
roofDirty.fill(0);
|
||||||
|
}
|
||||||
|
function updateDirtyRoof() {
|
||||||
|
for (let cx = 0; cx < CW; cx++) {
|
||||||
|
if (!roofDirty[cx]) continue;
|
||||||
|
scanRoof(cx << 5, Math.min(W, (cx + 1) << 5));
|
||||||
|
roofDirty[cx] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function scanRoof(x0, x1) {
|
||||||
|
for (let x = x0; x < x1; x++) {
|
||||||
let y = 0;
|
let y = 0;
|
||||||
for (; y < H; y++) { const ph = PH[mat[y * W + x]]; if (ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) break; }
|
for (; y < H; y++) { const ph = PH[mat[y * W + x]]; if (ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) break; }
|
||||||
roofY[x] = y;
|
roofY[x] = y;
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
function simTick() {
|
function simTick() {
|
||||||
tick++;
|
tick++;
|
||||||
tickStamp = (tickStamp % 255) + 1;
|
tickStamp = (tickStamp % 255) + 1;
|
||||||
if (tick % 20 === 1) updateRoof();
|
if (tick % 20 === 1) updateDirtyRoof();
|
||||||
weatherTick();
|
weatherTick();
|
||||||
machinesTick();
|
machinesTick();
|
||||||
pipesTick();
|
pipesTick();
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,7 @@ let ents = []; // loose objects (bars)
|
|||||||
let bgm; // background layer: trees. Liquids, powders and gases pass in front of it.
|
let bgm; // background layer: trees. Liquids, powders and gases pass in front of it.
|
||||||
let flyers = []; // grains in flight, blown out of the bot
|
let flyers = []; // grains in flight, blown out of the bot
|
||||||
let ghostBlocks = new Map(), ghostMolds = [];
|
let ghostBlocks = new Map(), ghostMolds = [];
|
||||||
let awake, hot, hotLvl, powderArea;
|
let awake, hot, hotLvl, powderArea, roofDirty;
|
||||||
let tick = 0, tickStamp = 1;
|
let tick = 0, tickStamp = 1;
|
||||||
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
|
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
|
||||||
let beltL = [], beltR = [], tubeLists = [[], [], [], [], []];
|
let beltL = [], beltR = [], tubeLists = [[], [], [], [], []];
|
||||||
@@ -50,6 +50,7 @@ function alloc(w, h) {
|
|||||||
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
|
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
|
||||||
AMB = new Float32Array(h);
|
AMB = new Float32Array(h);
|
||||||
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
|
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
|
||||||
|
roofDirty = new Uint8Array(CW).fill(1);
|
||||||
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
||||||
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
||||||
beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = [];
|
beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = [];
|
||||||
@@ -85,6 +86,7 @@ function fillNoise() {
|
|||||||
// ---------- Waking ----------
|
// ---------- Waking ----------
|
||||||
function wakeXY(x, y) {
|
function wakeXY(x, y) {
|
||||||
const cx = x >> 5, cy = y >> 5, c = cy * CW + cx;
|
const cx = x >> 5, cy = y >> 5, c = cy * CW + cx;
|
||||||
|
roofDirty[cx] = 1;
|
||||||
awake[c] = 3;
|
awake[c] = 3;
|
||||||
const lx = x & 31, ly = y & 31;
|
const lx = x & 31, ly = y & 31;
|
||||||
if (lx === 0) { if (cx > 0) awake[c - 1] = 3; } else if (lx === 31 && cx < CW - 1) awake[c + 1] = 3;
|
if (lx === 0) { if (cx > 0) awake[c - 1] = 3; } else if (lx === 31 && cx < CW - 1) awake[c + 1] = 3;
|
||||||
@@ -92,6 +94,7 @@ function wakeXY(x, y) {
|
|||||||
}
|
}
|
||||||
function wakeHeatXY(x, y) {
|
function wakeHeatXY(x, y) {
|
||||||
const c = (y >> 5) * CW + (x >> 5);
|
const c = (y >> 5) * CW + (x >> 5);
|
||||||
|
roofDirty[x >> 5] = 1;
|
||||||
hot[c] = 6; hotLvl[c] = 2;
|
hot[c] = 6; hotLvl[c] = 2;
|
||||||
}
|
}
|
||||||
function wakeIdx(i) { wakeXY(i % W, (i / W) | 0); }
|
function wakeIdx(i) { wakeXY(i % W, (i / W) | 0); }
|
||||||
|
|||||||
Vendored
+42
@@ -0,0 +1,42 @@
|
|||||||
|
// Reference heat loop before t21 phase 3; preserves conduction and Float32 store order.
|
||||||
|
function referenceHeatChunk(c, sc) {
|
||||||
|
const cx = c % CW, cy = (c / CW) | 0;
|
||||||
|
const x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
|
||||||
|
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
|
||||||
|
let maxDev = 0, burning = false;
|
||||||
|
for (let y = y0; y < y1; y++) {
|
||||||
|
const amb = AMB[y], yw = y * W;
|
||||||
|
const dDown = y + 1 < H ? amb - AMB[y + 1] : 0, dUp = y > 0 ? AMB[y - 1] - amb : 0;
|
||||||
|
for (let x = x0; x < x1; x++) {
|
||||||
|
const i = yw + x;
|
||||||
|
if (x + 1 < W) condPair(i, i + 1, sc, x + 1 === x1 ? c + 1 : -1, 0);
|
||||||
|
if (y + 1 < H) condPair(i, i + W, sc, y + 1 === y1 ? c + CW : -1, dDown);
|
||||||
|
if (x === x0 && leftCold) condPair(i - 1, i, sc, c - 1, 0);
|
||||||
|
if (y === y0 && upCold) condPair(i - W, i, sc, c - CW, dUp);
|
||||||
|
let m = mat[i];
|
||||||
|
const ph = PH[m];
|
||||||
|
let t = temp[i];
|
||||||
|
let rel;
|
||||||
|
if (ph === PH_EMPTY || ph === PH_GAS) {
|
||||||
|
if (y > 0) {
|
||||||
|
const u = i - W, pu = PH[mat[u]];
|
||||||
|
const ex = (t - amb) - (temp[u] - AMB[y - 1]);
|
||||||
|
if ((pu === PH_EMPTY || pu === PH_GAS) && ex > 2) {
|
||||||
|
const a = ex * (sc > 1 ? 0.45 : 0.25);
|
||||||
|
t -= a; temp[u] += a;
|
||||||
|
if (y === y0 && upCold && a > 0.5) { hot[c - CW] = 6; hotLvl[c - CW] = 2; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rel = y < roofY[x] ? 0.02 : 0.002;
|
||||||
|
} else rel = RELAX[m];
|
||||||
|
t += (amb - t) * rel * sc;
|
||||||
|
temp[i] = t;
|
||||||
|
if (m !== EMPTY) { react(i, x, y, m); m = mat[i]; t = temp[i]; }
|
||||||
|
const dv = t > amb ? t - amb : amb - t;
|
||||||
|
if (dv > maxDev) maxDev = dv;
|
||||||
|
if (flags[i] & F_BURN) burning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (burning || maxDev > 1.5) { hot[c] = 6; hotLvl[c] = (burning || maxDev > 60) ? 2 : 1; }
|
||||||
|
else hot[c]--;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Optimized paths must match the full roof scan and the original heat loop exactly.
|
||||||
|
const fs = require('fs');
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
run(fs.readFileSync(__dirname + '/fixtures/heat-chunk.js', 'utf8'));
|
||||||
|
|
||||||
|
// Partial areas and changes at area boundaries, including material appearing in empty sky.
|
||||||
|
flatWorld(run, 99, 97, 80);
|
||||||
|
run('RS = 123; bot.x = 45; bot.y = 30;');
|
||||||
|
run(`
|
||||||
|
const originalWeatherTick = weatherTick;
|
||||||
|
weatherTick = function () {
|
||||||
|
if (tick % 20 === 1) {
|
||||||
|
const roof = roofY.slice(); updateRoof();
|
||||||
|
if (!roof.every((v, x) => v === roofY[x])) globalThis.__roofMiss = (globalThis.__roofMiss || 0) + 1;
|
||||||
|
}
|
||||||
|
originalWeatherTick();
|
||||||
|
};
|
||||||
|
`);
|
||||||
|
for (let k = 0; k < 200; k++) run(`
|
||||||
|
setCell((12 + ${k} % 60) * W + (${k} * 31 % W), ${k} % 3 ? WATER : EMPTY, 18, 0);
|
||||||
|
simTick();
|
||||||
|
`);
|
||||||
|
check('scheduled roof matches a full scan at area edges', !run('globalThis.__roofMiss'), run('globalThis.__roofMiss'));
|
||||||
|
run('weatherTick = originalWeatherTick;');
|
||||||
|
|
||||||
|
// Every material, threshold neighbours, fractional temperatures, burning, cold neighbours,
|
||||||
|
// alternating heat scales, and partial areas exercise both random calls and Float32 stores.
|
||||||
|
const snapshot = () => run(`JSON.stringify({
|
||||||
|
arrays: [mat, temp, life, flags, hot, hotLvl, part, digp, seen].map(a => Array.from(new Uint8Array(a.buffer))),
|
||||||
|
RS, stats, EV, brokenMachines
|
||||||
|
})`);
|
||||||
|
const setup = () => {
|
||||||
|
flatWorld(run, 67, 65, 60);
|
||||||
|
run(`{
|
||||||
|
RS = 456; seen.fill(0); EV.length = 0; stats.ignitions = 0; brokenMachines = [];
|
||||||
|
const ts = [-30, -0.5, 0, 0.5, 18, 97, 100, 260, 380, 600, 880, 1085, 1300, 1530, 1900];
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const m = (i * 37) % NMAT;
|
||||||
|
mat[i] = m; temp[i] = ts[i % ts.length] + ((i % 3) - 1) * 0.001;
|
||||||
|
life[i] = i % 256; flags[i] = i % 23 === 0 && FUEL[m] ? F_BURN : 0;
|
||||||
|
}
|
||||||
|
hot.fill(6); hot[0] = 0; hotLvl.fill(2); updateRoof();
|
||||||
|
}`);
|
||||||
|
};
|
||||||
|
for (const scale of [1, 4]) {
|
||||||
|
setup();
|
||||||
|
run(`for (let c = 1; c < NCH; c++) referenceHeatChunk(c, ${scale});`);
|
||||||
|
const expected = snapshot();
|
||||||
|
setup();
|
||||||
|
run(`for (let c = 1; c < NCH; c++) heatChunk(c, ${scale});`);
|
||||||
|
check('heat matches original bytes and random state at scale ' + scale, snapshot() === expected);
|
||||||
|
}
|
||||||
|
done();
|
||||||
Reference in New Issue
Block a user