Rebuild lake generation: bowls in valleys filled to their spill level (t96)
Lakes no longer pour water to a guessed lip and repair it afterwards. Each lake lowers the land in the nearest valley with a smoothstep bowl, and water fills it to its spill level (the lower of the highest ground on each side, Priority-Flood in one dimension), so it is flat and contained by construction. Clay beds, no caves or loose pockets close under lakes, trees kept off the shore. Removes the levelling pass, the lower-rim lip and the bank-slope cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cj2RPXdPykKMEz5BXXczsj
This commit is contained in:
@@ -4,11 +4,11 @@ const { createHash } = require('node:crypto');
|
||||
const { run } = load();
|
||||
const { check, done } = suite();
|
||||
|
||||
// t96 re-baseline: only lake generation (rim height, spawn clearance, levelling) changes these material bytes.
|
||||
// t96 re-baseline: only lake generation (bowl, spill level, clay bed, no caves under lakes) changes these bytes.
|
||||
for (const [seed, expected] of [
|
||||
[777, 'dc0a19b076f341bcd944e1692651446198f57c7353db5fdea36d6bdd886ab6ff'],
|
||||
[77, 'aa979290d81a758e6ff68fa22b06513fb546aa73a80d09e69b35899a46758a0b'],
|
||||
[4242, '25737574612fe0395a2b0304a1ac698f817aa68bcd9e2b13a24ba794201ba6dc'],
|
||||
[777, '6c75cd15278a0d8c952bfb0b0f7f6185c597bd8f607d112d5970f8bdff4b1f2f'],
|
||||
[77, 'c1f3b0ac9b6d0a566e0051fa3f52388fa626ee714c3e18feed235721569ba056'],
|
||||
[4242, '63f6d90358f3e53be577a724cc5265a6e54380c431196cd3ed24cfd064285924'],
|
||||
]) {
|
||||
run(`generateWorld(${seed}, 512, 384)`);
|
||||
check(`seed ${seed} restores corner sealing`,run('CORNER_SEAL')===true);
|
||||
|
||||
+55
-53
@@ -1,80 +1,82 @@
|
||||
// Generated lakes start flat, conserve water, and have dry tree bases.
|
||||
const { load, suite, ticks, count, rect } = require('./harness');
|
||||
// Generated lakes sit in a smooth bowl filled to its spill level: flat, contained, clay-bedded,
|
||||
// stable, with natural banks, dry tree bases and the dock clear.
|
||||
const { load, suite, ticks } = require('./harness');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { run } = load();
|
||||
const { check, done } = suite();
|
||||
|
||||
// Surface pools with their depth, water count, and the ground top of the dry column on each side.
|
||||
function surfacePools() {
|
||||
return run(`(() => {
|
||||
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
|
||||
const pools = []; let pool = null;
|
||||
for (let x = 0; x < W; x++) {
|
||||
let y = 0; while (y < H && mat[y * W + x] === EMPTY) y++;
|
||||
if (mat[y * W + x] !== WATER) { pool = null; continue; }
|
||||
if (!pool) { pool = { x0: x, x1: x, width: 0, min: y, max: y }; pools.push(pool); }
|
||||
let b = y; while (b < H && mat[b * W + x] === WATER) b++;
|
||||
if (!pool) { pool = { x0: x, x1: x, width: 0, min: y, max: y, deep: 0, water: 0, clay: true }; pools.push(pool); }
|
||||
pool.x1 = x; pool.width++; pool.min = Math.min(pool.min, y); pool.max = Math.max(pool.max, y);
|
||||
pool.deep = Math.max(pool.deep, b - y); pool.water += b - y; pool.clay = pool.clay && mat[b * W + x] === CLAY;
|
||||
}
|
||||
return pools.filter(p => p.width > 20);
|
||||
return pools.filter(p => p.width > 20).map(p => Object.assign(p, { left: top(p.x0 - 1), right: top(p.x1 + 1) }));
|
||||
})()`);
|
||||
}
|
||||
// Largest ground step within 40 columns of each pool, away from the dock's foundation.
|
||||
function bankStep(pools) {
|
||||
return run(`(() => { const d = machines.find(m => m && m.type === 'dock');
|
||||
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
|
||||
let worst = 0;
|
||||
for (const p of ${JSON.stringify(pools)}) for (let x = Math.max(1, p.x0 - 40); x < Math.min(W - 1, p.x1 + 40); x++)
|
||||
if (x < d.x - 2 || x > d.x + 26) worst = Math.max(worst, Math.abs(top(x + 1) - top(x)));
|
||||
return worst; })()`);
|
||||
}
|
||||
function lakeChecks(seed, pools) {
|
||||
check(`seed ${seed} has a real lake`, pools.some(p => p.width >= 120) && pools.every(p => p.deep >= 12), JSON.stringify(pools));
|
||||
check(`seed ${seed} lakes are exactly flat`, pools.every(p => p.max === p.min), JSON.stringify(pools));
|
||||
// Contained: the dry column on each side reaches at least the water's surface, so nothing can spill.
|
||||
check(`seed ${seed} lakes are held by their banks`, pools.every(p => p.left <= p.min && p.right <= p.min), JSON.stringify(pools));
|
||||
check(`seed ${seed} lake beds are clay`, pools.every(p => p.clay));
|
||||
// Banks follow the land: round 3 left a 15-grain cliff, round 4 a ruler-straight ramp.
|
||||
check(`seed ${seed} lake banks have no cliff`, bankStep(pools) <= 3, bankStep(pools));
|
||||
check(`seed ${seed} keeps its lakes clear of the trade dock`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
|
||||
return ${JSON.stringify(pools)}.every(p => p.x1 < d.x - 8 || p.x0 > d.x + 24 + 8); })()`), JSON.stringify(pools));
|
||||
check(`seed ${seed} has no submerged tree base`, run('treeBases.every(i => !treeHeld(i) || mat[i] !== WATER)'));
|
||||
check(`seed ${seed} retains dry trees`, run('treeBases.filter(i => treeHeld(i)).length > 20'));
|
||||
}
|
||||
|
||||
// Spill levels on a hand-made surface: each column fills to the lower of its two highest sides.
|
||||
run('alloc(16, 64); surfY.set([20, 30, 34, 32, 26, 30, 36, 33, 24, 40, 40, 22, 25, 25, 25, 25]); globalThis.levels = new Int16Array(16);');
|
||||
// The highest ground right of x=1 is 22 (x=11), lower than the 20 on the left, so x=1..11 fill
|
||||
// to 22; past x=11 nothing on the right rises above 25. The deepest water is 18 (x=9, 10).
|
||||
check('spill levels fill each basin to its lower rim', run('spillLevels(0, 15, levels) === 18 && [20,22,22,22,22,22,22,22,22,22,22,22,25,25,25,25].every((v, x) => levels[x] === v)'), run('Array.from(levels)'));
|
||||
|
||||
for (const [seed, expected] of [
|
||||
[7, '5e230523dc4dda212e7c67ceb7f3b496ff201d826366e1d7c379efd1351d1956'],
|
||||
[42, 'cc3d150a91caac6ab88dfefbd45d85c5bc5c38fc77d7913b9de157647f4b9d7b'],
|
||||
[424242, '1339c90f4f7f1d245c2e21aa4b8d23b21c199b8251392256f6de5ed704e2b396'],
|
||||
[7, '3e97601ea08027e7f21fa3d52ec300fb7e13c630a1ddad0a7be6ca7ab4b07592'],
|
||||
[42, 'ecf71e4309c0b19654fbc17dbb0b5717023842d331f4f12c010d3312e9f9383f'],
|
||||
[424242, '235a33a7c85118d05cc9f0508acaa266b61d5f9f100703c0a095d760fe80066d'],
|
||||
[2, null],
|
||||
[1, null],
|
||||
// Before t96 round 3 these lakes ran over their lower rim up to the trade dock.
|
||||
// These lakes once ran over their lower rim up to the trade dock.
|
||||
[5, null],
|
||||
[6, null],
|
||||
]) {
|
||||
run(`generateWorld(${seed}, 2048, 1024, (f, label) => {
|
||||
if (f === 0.99 && label === 'Finishing') globalThis.waterBeforeLevel = mat.reduce((n, m) => n + (m === WATER), 0);
|
||||
}); weather.next = 1e15;`);
|
||||
run(`generateWorld(${seed}, 2048, 1024); weather.next = 1e15;`);
|
||||
const hash = createHash('sha256');
|
||||
for (const name of ['mat', 'temp']) hash.update(Buffer.from(run(name).buffer));
|
||||
const actual = hash.digest('hex');
|
||||
if (expected) check(`seed ${seed} matches levelled generation hash`, actual === expected, actual);
|
||||
check(`seed ${seed} conserves water through levelling`, run('mat.reduce((n, m) => n + (m === WATER), 0) === waterBeforeLevel'));
|
||||
const initial = surfacePools();
|
||||
check(`seed ${seed} starts with flat surface pools`, initial.length > 0 && initial.every(p => p.max - p.min <= 1), JSON.stringify(initial));
|
||||
// Lakes dug to their lower rim once left a cliff on the high side (seed 766733's 15-grain step).
|
||||
check(`seed ${seed} lake banks slope with no cliff`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
|
||||
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
|
||||
return ${JSON.stringify(initial)}.every(p => { for (let x = Math.max(1, p.x0 - 40); x < Math.min(W - 1, p.x1 + 40); x++)
|
||||
if (x < d.x - 2 || x > d.x + 26) { if (Math.abs(top(x + 1) - top(x)) > 3) return false; } return true; }); })()`), JSON.stringify(initial));
|
||||
check(`seed ${seed} keeps its lakes clear of the trade dock`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
|
||||
return ${JSON.stringify(initial)}.every(p => p.x1 < d.x - 8 || p.x0 > d.x + 24 + 8); })()`), JSON.stringify(initial));
|
||||
if (expected) check(`seed ${seed} matches its generation hash`, actual === expected, actual);
|
||||
const pools = surfacePools();
|
||||
lakeChecks(seed, pools);
|
||||
check(`seed ${seed} restores pinned generation switches`, run('LSTEP === 8 && LFILM === 4 && SORTP === 0.06 && CORNER_SEAL && POWDER_FAN'));
|
||||
check(`seed ${seed} starts every water area awake`, run(`mat.every((m, i) => m !== WATER || awake[((i / W) >> 5) * CW + ((i % W) >> 5)] > 0)`));
|
||||
check(`seed ${seed} has no submerged tree base`, run('treeBases.every(i => !treeHeld(i) || mat[i] !== WATER)'));
|
||||
check(`seed ${seed} retains dry trees`, run('treeBases.filter(i => treeHeld(i)).length > 20'));
|
||||
ticks(run, 600);
|
||||
const settled = surfacePools();
|
||||
check(`seed ${seed} stays flat after 600 ticks`, settled.length > 0 && settled.every(p => p.max - p.min <= 1), JSON.stringify(settled));
|
||||
for (const lake of initial) check(`seed ${seed} original lake retains dry roots`, run(`treeBases.every(i => i % W < ${lake.x0} || i % W > ${lake.x1} || !treeHeld(i) || mat[i] !== WATER)`));
|
||||
// Stable: no leak into a cave and no slump; the surface and the water in each lake stay put.
|
||||
ticks(run, 3000);
|
||||
const later = surfacePools();
|
||||
check(`seed ${seed} lakes keep their surface and water for 3000 ticks`, later.length === pools.length &&
|
||||
later.every((p, k) => p.x0 === pools[k].x0 && p.x1 === pools[k].x1 && p.min === pools[k].min && p.max === pools[k].max && p.water === pools[k].water),
|
||||
JSON.stringify({ pools, later }));
|
||||
}
|
||||
// A low rim with a much deeper empty basin beyond it must not steal the water.
|
||||
for (const wall of ['STONE', 'SAND']) {
|
||||
run('alloc(64, 64); mat.fill(STONE);');
|
||||
rect(run, 5, 10, 24, 29, run('EMPTY'));
|
||||
rect(run, 26, 10, 55, 45, run('EMPTY'));
|
||||
rect(run, 25, 22, 25, 45, run(wall));
|
||||
rect(run, 25, 10, 25, 21, run('EMPTY'));
|
||||
rect(run, 5, 20, 14, 29, run('WATER'));
|
||||
rect(run, 15, 27, 15, 29, run('WATER'));
|
||||
run('globalThis.beforeLake = mat.slice(); levelWorldWater();');
|
||||
check(`${wall} rim holds all 103 grains`, count(run, run('WATER')) === 103 && count(run, run('WATER'), 26, 0) === 0);
|
||||
check(`${wall} basin fills from bottom upward`, count(run, run('WATER'), 5, 25, 24, 29) === 100);
|
||||
check(`${wall} partial top row fills from the middle outward`, run('[13,14,15].every(x => mat[24 * W + x] === WATER)') && count(run, run('WATER'), 0, 0, 63, 24) === 3);
|
||||
check(`${wall} levelling changes only water and air`, run('mat.every((m,i) => m === beforeLake[i] || ((m === EMPTY || m === WATER) && (beforeLake[i] === EMPTY || beforeLake[i] === WATER)))'));
|
||||
}
|
||||
// Diagonally touching small bodies remain separate; exactly 50 grains is eligible.
|
||||
run('alloc(64,64); mat.fill(STONE);');
|
||||
rect(run, 5, 20, 11, 26, run('WATER'));
|
||||
rect(run, 12, 27, 18, 33, run('WATER'));
|
||||
rect(run, 5, 10, 11, 19, run('EMPTY'));
|
||||
rect(run, 30, 10, 49, 29, run('EMPTY'));
|
||||
rect(run, 30, 20, 34, 29, run('WATER'));
|
||||
run('globalThis.smallBefore = mat.slice(); levelWorldWater();');
|
||||
check('diagonally touching 49-grain pools are left intact', run('mat.every((m,i) => i % W >= 30 || m === smallBefore[i])'));
|
||||
check('50-grain body levels and conserves volume', count(run, run('WATER'), 30, 28, 49, 29) === 40 && count(run, run('WATER'), 30, 0) === 50);
|
||||
// One of the owner's Huge seeds, whose round-3/4 banks showed the cliff and the ramp.
|
||||
run('generateWorld(766733, 6144, 2048); weather.next = 1e15;');
|
||||
lakeChecks(766733, surfacePools());
|
||||
done();
|
||||
|
||||
+3
-2
@@ -185,8 +185,9 @@ oreNames.forEach((name, j) => {
|
||||
});
|
||||
const totalOre = oreBalance.counts.reduce((a, b) => a + b, 0);
|
||||
check('total ore is about 4 to 4.5 times the original', totalOre >= 162519 * 4 && totalOre <= 162519 * 4.5, totalOre);
|
||||
// t96: lake basins moved and their high banks now slope, so ore there changed (only at the two lakes).
|
||||
check('shallow ore matches the original generator', oreBalance.shallowHash === 1640715415, oreBalance.shallowHash);
|
||||
// t96: lakes are bowls in the nearest valley with no caves or loose pockets close under them, so ore
|
||||
// changed there (724 cells, all within the old and new lake spans).
|
||||
check('shallow ore matches the original generator', oreBalance.shallowHash === 3875519651, oreBalance.shallowHash);
|
||||
run('generateWorld(99, 4096, 1536, null)');
|
||||
const marks = run('(()=>{let lakes=0,peaks=0,inLake=false;for(let x=1;x<W-1;x++){const l=mat[(surfY[x]-1)*W+x]===WATER;if(l&&!inLake)lakes++;inLake=l;if(surfY[x]<surfBase-100&&surfY[x]<=surfY[x-1]&&surfY[x]<surfY[x+1])peaks++;}return [lakes,peaks]})()');
|
||||
check('a large world has several lakes and mountains across it', marks[0] >= 2 && marks[1] >= 2, marks);
|
||||
|
||||
Reference in New Issue
Block a user