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:
+20
-21
@@ -590,27 +590,26 @@ above, including:
|
||||
ordinary repose rules take over. Keep stamped moves, barriers, burning-bed protection and
|
||||
worker independence. The existing worldgen POWDER_FAN switch disables new face releases.
|
||||
|
||||
- **Level lakes during world generation (t96, 2026-10-29).** The owner wants new worlds
|
||||
to start with flat lakes instead of waiting 80–125 seconds for them to flow level. Changed
|
||||
generated worlds are accepted: seeds no longer reproduce the old water layout. After the
|
||||
existing settling loop, level four-connected water bodies of at least 50 grains, preserving
|
||||
their exact volume. Fill reachable air/water from the bottom upward, respecting solid/powder
|
||||
walls and dry rims; place a partial top row from the middle outward. Establish the dock's
|
||||
existing foundation first so it cannot erase newly levelled water. Keep the pinned settling
|
||||
values, startup water wakes, and round-1 wet/water-connected-bank tree filtering. Existing
|
||||
load wakes and later snowmelt/flooding rules stay as they are.
|
||||
- **Lakes fill only to their lower rim and keep clear of spawn (t96 round 3, 2026-10-29).**
|
||||
The generator took the basin's `lip` as `Math.min(surfY)`, the highest ground (y grows
|
||||
downward), so water was poured above the lower rim and ran out toward low ground; once
|
||||
levelled it reached the trade dock on 4 of 12 seeds. The owner chose option 2: the lip is
|
||||
now the lower of the two rim edges, minus one, so a lake is filled to just under that rim,
|
||||
and each lake's half-width is capped so its edge stays 80 grains from spawn (and the dock).
|
||||
Lakes are smaller and lower: about 180-250 grains wide instead of 250-330, 5-15 grains
|
||||
lower. The random draws keep their order.
|
||||
Dug to the lower rim, the high side ended in a vertical cliff as tall as the gap between the
|
||||
rims (15-18 grains on seeds 766733, 7, 42; owner screenshots). From each edge inward the
|
||||
ground may now fall at most 0.7 grains a column and is never raised above the natural
|
||||
surface, so the high side is a sloped bank down to the water.
|
||||
- **Lakes are bowls filled to their spill level (t96, 2026-10-29).** Three patches the same
|
||||
day (a levelling pass, a lower-rim lip, a 0.7-grain bank cap) each fixed one symptom and
|
||||
exposed the next: lakes started tilted, then flooded to the dock, then had a cliff, then a
|
||||
straight ramp. The real cause was pouring water to a guessed `lip` (which was the highest
|
||||
ground, since y grows downward) so it ran out over the lower rim during settling. Banks and
|
||||
beds are solid and never moved. Owner decisions: fix it properly, planned lakes only (no
|
||||
extra ponds), clay lake beds. The method, from depression filling (Priority-Flood, Barnes et
|
||||
al.): every basin fills to its spill level, the lowest point of its rim, so water is flat and
|
||||
contained by construction. The generated surface is one height per column with no overhangs
|
||||
(caves start 10+ rows down), so this is the 1D form: each column's water top is the lower of
|
||||
the highest ground to its left and to its right (`spillLevels` in worldgen-helpers.js).
|
||||
Generation: each lake goes to the lowest smoothed ground within 60 grains of its planned spot,
|
||||
keeping 80 grains from spawn and the dock; the land is lowered by a smoothstep bowl (flat floor
|
||||
in the middle 40%, banks easing in and out, a little noise) on top of its own shape, so banks
|
||||
keep the land's contours and meet it with no step; on sloping ground the bowl deepens (up to
|
||||
3x) until the water is at least 12 deep. Water fills only to the spill level within the bowl.
|
||||
The bed (top 3 rows under water) is clay and grass stops at the waterline. No caves or loose
|
||||
sand/gravel pockets within 40 rows under a lake or 12 columns of its edge, so nothing can open
|
||||
a hole in its bed. Trees keep 6 columns from the water. The levelling pass, the lip, the bank
|
||||
cap and the post-settle tree re-check are gone. Generation time is unchanged (within 5%).
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
|
||||
+5
-6
@@ -378,12 +378,12 @@ The same chunk schedule runs inline or in workers. Workers only own a pass at a
|
||||
simTick: one simulation step, in order.
|
||||
- functions: simTick
|
||||
|
||||
### src/sim/worldgen-helpers.js (131 lines)
|
||||
World generation helpers: noise and removal of tiny hard rock specks.
|
||||
- functions: makeNoise, fbm, clearHardSpecks, levelWorldWater
|
||||
### src/sim/worldgen-helpers.js (74 lines)
|
||||
World generation helpers: noise, hard rock specks and lake spill levels.
|
||||
- functions: makeNoise, fbm, clearHardSpecks, spillLevels
|
||||
- variables: SPECK_MIN
|
||||
|
||||
### src/sim/worldgen.js (305 lines)
|
||||
### src/sim/worldgen.js (309 lines)
|
||||
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||
- functions: generateWorld, generateWorldAsync
|
||||
|
||||
@@ -664,8 +664,7 @@ The frame loop and boot.
|
||||
- `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid.
|
||||
- `test/t_import_save.js`: Import must preserve the running world before reading or replacing it.
|
||||
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||
- `test/t_lakes.py`: Pixel startup and fresh/reloaded flat lakes after 600 ticks; shots/t96-level.png.
|
||||
- `test/t_lakes.js`: Generated lakes start flat, conserve water, and have dry tree bases.
|
||||
- `test/t_lakes.js`: 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.
|
||||
- `test/t_lakes.py`: t96: flat generated lakes, fresh and reloaded, in the real pixel frame loop.
|
||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||
|
||||
+8
-7
@@ -76,14 +76,15 @@ The conveyor fixture uses a wider feed pile, the screen fixture warms longer, an
|
||||
checks waking within eight ticks separately from the probabilistic consumption of fuel.
|
||||
|
||||
`test/t_lakes.js` pins SHA-256 over raw `mat` followed by Float32 `temp` bytes for seeds
|
||||
7, 42 and 424242 at 2048x1024. t96 round 2 re-baselines only for the lake-levelling pass;
|
||||
`t_corner_seal.js` also pins three smaller worlds with the same intentional change. The lake
|
||||
suite checks all five seeds (also 2 and 1): surface pools wider than 20 grains start within
|
||||
one grain of level and stay so after 600 ticks, exact WATER count from the pre-pass 0.99
|
||||
progress callback, dry tree bases and startup wakes. Small fixtures cover solid/powder rims,
|
||||
volume, centered partial rows, the 50-grain cutoff and diagonal separation.
|
||||
7, 42 and 424242 at 2048x1024; `t_corner_seal.js` pins three smaller worlds and `t_world.js`
|
||||
the shallow-ore hash, all re-baselined for the t96 lake rebuild (changes only inside lake spans).
|
||||
For seeds 7, 42, 424242, 2, 1, 5 and 6 (and Huge seed 766733 without ticks) every lake is at
|
||||
least 12 deep with one at least 120 wide, exactly flat, held by a dry bank at or above its
|
||||
surface on both sides, clay-bedded, with no ground step over 3 grains within 40 columns, clear
|
||||
of the dock, with no submerged tree base; after 3000 ticks each lake keeps its exact surface and
|
||||
water count (no leaks). A hand-made surface checks `spillLevels`.
|
||||
`python3 test/t_lakes.py` checks pixel startup and fresh/reloaded lakes after 600 ticks with
|
||||
two browser helpers and no page errors; `shots/t96-level.png` shows the untouched new lake.
|
||||
two browser helpers and no page errors; `shots/t96-level.png` shows the new lake.
|
||||
|
||||
`test/t_bot_rescue.js` covers deep basalt burial, nearest/upward and diagonal nudges,
|
||||
unexplored-pocket rejection, newest open trail points, ring wrap/load reset, bounded searches,
|
||||
|
||||
@@ -4,6 +4,28 @@ 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
|
||||
open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
## 2026-10-29: t96 rebuilt: lakes are bowls filled to their spill level (Claude)
|
||||
|
||||
- Owner: the round-4 bank was a flat, unnatural ramp; fix lake generation properly instead of
|
||||
patching. Mapped worldgen first: banks and beds are solid (only sand, gravel, snow and liquids
|
||||
move while settling), so the original tilt was water poured above the lower rim spilling out,
|
||||
not slumping banks (Claude's earlier explanation was wrong). Researched depression filling
|
||||
(Priority-Flood) and its 1D form. Owner chose planned lakes only and clay beds.
|
||||
- src/sim/worldgen.js: lake goes to the lowest smoothed ground within 60 grains of its plan (80
|
||||
from spawn); smoothstep bowl added to the land's own shape, deepened up to 3x until 12+ deep;
|
||||
water fills to `spillLevels` (new, worldgen-helpers.js) within the bowl; clay bed (3 rows),
|
||||
grass stops at the waterline; no caves or sand/gravel pockets within 40 rows under a lake or 12
|
||||
columns of it; noTree 6 columns from water. Removed levelWorldWater, the lip, the 0.7 bank
|
||||
cap, the dock-first ordering and the post-settle tree re-check.
|
||||
- Measured on 10 seeds (incl. owner's 766733/12446/23565 at 6144x2048): lakes 120-231 wide,
|
||||
14-47 deep, exactly flat, banks no steeper than 3 per column, no wet trees, each lake's
|
||||
surface and water identical after 3000 ticks (underground aquifers still drain, as before).
|
||||
Screenshots of all four seeds checked in the smooth/pixel looks. Generation time within 5%.
|
||||
- Tests: t_lakes.js rewritten (flat, held by banks, clay bed, no cliff, real lake, dock clear,
|
||||
stable 3000 ticks, spillLevels fixture; Huge seed 766733); hashes re-baselined in t_lakes,
|
||||
t_corner_seal and t_world (shallow ore: 724 of 27,985 cells, all within old/new lake spans).
|
||||
Full `run_all.py --browser`: ALL PASSED.
|
||||
|
||||
## 2026-10-29: t96 round 3, lakes fill to their lower rim (Claude)
|
||||
|
||||
- Owner chose option 2. src/sim/worldgen.js: lake `lip` is the lower rim edge minus one (was
|
||||
|
||||
+11
-68
@@ -1,4 +1,4 @@
|
||||
// World generation helpers: noise, hard rock specks and lake levelling.
|
||||
// World generation helpers: noise, hard rock specks and lake spill levels.
|
||||
// ---------- Perlin noise ----------
|
||||
function makeNoise(rng) {
|
||||
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
|
||||
@@ -60,72 +60,15 @@ function clearHardSpecks(live) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Grow each basin upward from its bottom: dry walls/rims are crossed only if filled to them.
|
||||
function levelWorldWater() {
|
||||
const seen = new Uint8Array(N), region = new Uint32Array(N), cells = new Int32Array(N), stack = new Int32Array(N);
|
||||
let tag = 0;
|
||||
for (let start = 0; start < N; start++) {
|
||||
if (mat[start] !== WATER || seen[start]) continue;
|
||||
let size = 0, sp = 1, bottom = start;
|
||||
stack[0] = start; seen[start] = 1;
|
||||
// Measure the original four-connected body before moving any of its water.
|
||||
while (sp) {
|
||||
const i = stack[--sp], x = i % W, y = (i / W) | 0;
|
||||
cells[size++] = i;
|
||||
if (i > bottom) bottom = i;
|
||||
for (let d = 0; d < 4; d++) {
|
||||
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && !y) || (d === 3 && y === H - 1)) continue;
|
||||
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
|
||||
if (mat[j] === WATER && !seen[j]) { seen[j] = 1; stack[sp++] = j; }
|
||||
}
|
||||
}
|
||||
if (size < 50) continue;
|
||||
let volume = size;
|
||||
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; }
|
||||
tag++; size = sp = 1; cells[0] = stack[0] = bottom; region[bottom] = tag;
|
||||
for (let level = (bottom / W) | 0; level >= 0; level--) {
|
||||
if (!sp && level < H - 1) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
const i = level * W + x;
|
||||
if (region[i + W] === tag && (mat[i] === EMPTY || mat[i] === WATER)) {
|
||||
region[i] = tag; cells[size++] = i; stack[sp++] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this height, fill every connected low space before rising another row.
|
||||
while (sp) {
|
||||
const i = stack[--sp], x = i % W, y = (i / W) | 0;
|
||||
if (mat[i] === WATER) { volume++; seen[i] = 1; }
|
||||
for (let d = 0; d < 4; d++) {
|
||||
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && y <= level) || (d === 3 && y === H - 1)) continue;
|
||||
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
|
||||
if (region[j] === tag || (mat[j] !== EMPTY && mat[j] !== WATER)) continue;
|
||||
region[j] = tag; cells[size++] = j; stack[sp++] = j;
|
||||
}
|
||||
}
|
||||
if (size >= volume) break;
|
||||
}
|
||||
const body = cells.subarray(0, size);
|
||||
body.sort(); // row order, with no objects allocated per grain
|
||||
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; seen[i] = 1; }
|
||||
for (let end = size; end > 0 && volume > 0;) {
|
||||
const y = (cells[end - 1] / W) | 0;
|
||||
let begin = end - 1;
|
||||
while (begin > 0 && ((cells[begin - 1] / W) | 0) === y) begin--;
|
||||
const width = end - begin;
|
||||
if (volume >= width) {
|
||||
for (let k = begin; k < end; k++) { mat[cells[k]] = WATER; flags[cells[k]] = F_NAT; }
|
||||
volume -= width;
|
||||
} else {
|
||||
const left = cells[begin] % W, right = cells[end - 1] % W, mid = (left + right) >> 1;
|
||||
for (let d = 0; volume > 0; d++) {
|
||||
const x = mid + (d & 1 ? (d + 1) >> 1 : -(d >> 1));
|
||||
if (x < left || x > right) continue;
|
||||
const i = y * W + x;
|
||||
if (region[i] === tag) { mat[i] = WATER; flags[i] = F_NAT; volume--; }
|
||||
}
|
||||
}
|
||||
end = begin;
|
||||
}
|
||||
// Water top for each column of [x0, x1] on the 1D side-view surface: the lower of the highest
|
||||
// ground to its left and to its right (Priority-Flood reduced to one dimension; y grows
|
||||
// downward). Writes out[x0..x1] and returns the deepest water.
|
||||
function spillLevels(x0, x1, out) {
|
||||
for (let x = x0, top = surfY[x0]; x <= x1; x++) { top = Math.min(top, surfY[x]); out[x] = top; }
|
||||
let deepest = 0;
|
||||
for (let x = x1, top = surfY[x1]; x >= x0; x--) {
|
||||
top = Math.min(top, surfY[x]); out[x] = Math.max(out[x], top);
|
||||
deepest = Math.max(deepest, surfY[x] - out[x]);
|
||||
}
|
||||
return deepest;
|
||||
}
|
||||
|
||||
+37
-45
@@ -67,31 +67,38 @@ function* worldSteps(seedIn, w, h) {
|
||||
else dirtY[x] = Math.min(dirtY[x], surfY[x] + 20);
|
||||
}
|
||||
}
|
||||
// lakes: a basin dug below the lower of its two rims, filled to just under that rim (y grows
|
||||
// downward, so the lower rim is the larger surfY); kept 80 grains clear of spawn and the dock
|
||||
const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), desAt = new Uint8Array(w);
|
||||
// Lakes: lower the land in the nearest valley with a smooth bump (no slope at its edges, so the
|
||||
// banks keep the land's own shape), then fill the basin to its spill level. Water is flat and
|
||||
// contained by construction; lakes keep 80 grains clear of spawn and the dock.
|
||||
const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), lakeBed = new Uint8Array(w), desAt = new Uint8Array(w);
|
||||
for (const f of feats) {
|
||||
if (f.k !== 'lake') continue;
|
||||
const lw = Math.min(Math.round(90 + rng() * 40), Math.abs(f.x - spawnX) - 80), deep = 22 + rng() * 8;
|
||||
const lip = Math.max(surfY[Math.max(0, f.x - lw)], surfY[Math.min(w - 1, f.x + lw)]) - 1;
|
||||
const x0 = Math.max(0, f.x - lw), x1 = Math.min(w - 1, f.x + lw), nat = surfY.slice(x0, x1 + 1);
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const u = (x - f.x) / lw, depth = Math.max(0, 1 - u * u) * deep;
|
||||
if (depth > 0) surfY[x] = Math.max(surfY[x], Math.round(lip + depth));
|
||||
lakeTop[x] = lip + 2;
|
||||
const lwDraw = Math.round(90 + rng() * 40), deep = 22 + rng() * 8;
|
||||
// water gathers in valleys: take the lowest smoothed ground within 60 grains of the plan
|
||||
let c = f.x, low = -1;
|
||||
for (let x = f.x - 60; x <= f.x + 60; x++) {
|
||||
if (x - lwDraw < 0 || x + lwDraw >= w || Math.abs(x - spawnX) - 80 < lwDraw * 0.85) continue;
|
||||
let s = 0; for (let k = -15; k <= 15; k++) s += surfY[x + k];
|
||||
if (s > low) { low = s; c = x; }
|
||||
}
|
||||
// Dug to the lower rim, the high side would end in a cliff as tall as the gap between the
|
||||
// rims: from each edge inward the ground may fall at most 0.7 grains a column, never above nature.
|
||||
for (const d of [1, -1]) {
|
||||
let prev = surfY[d > 0 ? Math.max(0, x0 - 1) : Math.min(w - 1, x1 + 1)];
|
||||
for (let x = d > 0 ? x0 : x1; x >= x0 && x <= x1; x += d) {
|
||||
prev = Math.min(prev + 0.7, surfY[x]);
|
||||
surfY[x] = Math.max(nat[x - x0], Math.round(prev));
|
||||
prev = Math.max(prev, surfY[x]);
|
||||
const lw = Math.min(lwDraw, Math.abs(c - spawnX) - 80), x0 = Math.max(0, c - lw), x1 = Math.min(w - 1, c + lw);
|
||||
const nat = surfY.slice(x0, x1 + 1);
|
||||
// on sloping ground the low rim spills early, so deepen the bump until the lake holds water
|
||||
for (let scale = 1; scale <= 3; scale += 0.25) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
// a bowl: flat floor in the middle 40%, banks easing in and out along a smoothstep
|
||||
const t = Math.min(1, Math.max(0, (1 - Math.abs(x - c) / lw) / 0.6)), b = t * t * (3 - 2 * t);
|
||||
surfY[x] = nat[x - x0] + Math.round(deep * scale * b * (1 + 0.15 * nDetail(x / 25, 6.6)));
|
||||
}
|
||||
if (spillLevels(x0, x1, lakeTop) >= 12) break;
|
||||
}
|
||||
for (let x = x0; x <= x1; x++) dirtY[x] = Math.max(dirtY[x], surfY[x] + 8);
|
||||
for (let x = Math.max(0, f.x - lw - 6); x <= Math.min(w - 1, f.x + lw + 6); x++) noTree[x] = 1;
|
||||
let wet0 = w, wet1 = -1;
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
dirtY[x] = Math.max(dirtY[x], surfY[x] + 8);
|
||||
if (lakeTop[x] < surfY[x]) { wet0 = Math.min(wet0, x); wet1 = Math.max(wet1, x); } else lakeTop[x] = h;
|
||||
}
|
||||
for (let x = Math.max(0, wet0 - 6); x <= Math.min(w - 1, wet1 + 6); x++) noTree[x] = 1;
|
||||
for (let x = Math.max(0, wet0 - 12); x <= Math.min(w - 1, wet1 + 12); x++) lakeBed[x] = 1;
|
||||
}
|
||||
for (const f of feats) {
|
||||
if (f.k !== 'desert') continue;
|
||||
@@ -110,8 +117,9 @@ function* worldSteps(seedIn, w, h) {
|
||||
if (y >= lakeTop[x]) m = WATER;
|
||||
} else {
|
||||
const d = y - sy;
|
||||
const inDes = desAt[x] === 1;
|
||||
if (y < dirtY[x]) m = d === 0 && !inDes ? GRASS : DIRT;
|
||||
const inDes = desAt[x] === 1, bed = lakeBed[x] === 1;
|
||||
// a lake bottom is clay; the grass stops at the waterline
|
||||
if (y < dirtY[x]) m = lakeTop[x] < sy && d < 3 ? CLAY : d === 0 && !inDes ? GRASS : DIRT;
|
||||
else if (y < graniteY[x] + nWarp(x / 70, y / 45) * 26 + nWarp(x / 22 + 40, y / 15) * 8 + nPocket(x / 9, y / 9) * 3) m = STONE;
|
||||
else if (y < basaltY[x] + nWarp(x / 64 + 50, y / 40) * 22 + nWarp(x / 20 + 90, y / 14) * 7 + nPocket(x / 7, y / 7) * 3) m = GRANITE;
|
||||
else m = BASALT;
|
||||
@@ -126,8 +134,9 @@ function* worldSteps(seedIn, w, h) {
|
||||
else if (m === STONE) {
|
||||
if (d < 150 && pk > 0.52) m = DIRT;
|
||||
else if (d < 110 && pk2 > 0.5) m = CLAY;
|
||||
else if (pk < -0.56) m = GRAVEL;
|
||||
else if (pk2 < -0.6 && d < 220) m = SAND;
|
||||
// no loose pockets close under a lake: one falling into a cave would open a hole in its bed
|
||||
else if (pk < -0.56 && (!bed || d > 40)) m = GRAVEL;
|
||||
else if (pk2 < -0.6 && d < 220 && (!bed || d > 40)) m = SAND;
|
||||
}
|
||||
// ores
|
||||
if (m === STONE || m === DIRT || m === GRANITE || m === BASALT || m === CLAY) {
|
||||
@@ -160,8 +169,8 @@ function* worldSteps(seedIn, w, h) {
|
||||
// no ore where the rock is naturally hot enough to light or melt it: it would burn or run forever
|
||||
if (m !== host && (IGN[m] ? AMB[y] > IGN[m] - 120 : MELT[m] && AMB[y] > MELT[m] - 200)) m = host;
|
||||
}
|
||||
// caves
|
||||
if (d > 9) {
|
||||
// caves (kept deeper under lakes so none opens into one)
|
||||
if (d > (bed ? 40 : 9)) {
|
||||
const t1 = Math.abs(fbm(nCave, x / 130, y / 80, 2));
|
||||
const big = fy > 0.2 ? fbm(nCave2, x / 95, y / 64, 3) : -1;
|
||||
const small = nCave3(x / 26, y / 20);
|
||||
@@ -202,14 +211,12 @@ function* worldSteps(seedIn, w, h) {
|
||||
}
|
||||
yield [0.72, 'Snow and trees'];
|
||||
// trees
|
||||
const trees = [];
|
||||
for (let x = 12; x < w - 12;) {
|
||||
const sy = surfY[x];
|
||||
const ok = mat[sy * w + x] === GRASS && mat[sy * w + x + 1] === GRASS && !noTree[x] && Math.abs(x - spawnX) > 6 && !(x > spawnX + 14 && x < spawnX + 72); // none where the dock goes
|
||||
if (ok && rng() < 0.7) {
|
||||
const tree = { x, y: sy - 1, cells: [] }; trees.push(tree);
|
||||
const th = 9 + Math.floor(rng() * 8);
|
||||
const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) { bgm[i] = WOOD; tree.cells.push(i, WOOD); } };
|
||||
const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) bgm[i] = WOOD; };
|
||||
for (let k = 1; k <= th; k++) for (let dx = 0; dx < 2; dx++) setW(x + dx, sy - k);
|
||||
// two branches
|
||||
for (const side of [-1, 1]) { const by = sy - th + 3 + Math.floor(rng() * 3); for (let k = 1; k <= 3; k++) setW(x + (side < 0 ? -k : 1 + k), by - (k >> 1)); }
|
||||
@@ -220,7 +227,7 @@ function* worldSteps(seedIn, w, h) {
|
||||
for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) {
|
||||
if (xx < 0 || yy < 1 || xx >= w) continue;
|
||||
const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85);
|
||||
if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) { bgm[i] = LEAVES; tree.cells.push(i, LEAVES); } }
|
||||
if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) bgm[i] = LEAVES; }
|
||||
}
|
||||
}
|
||||
x += 16 + Math.floor(rng() * 20);
|
||||
@@ -268,26 +275,11 @@ function* worldSteps(seedIn, w, h) {
|
||||
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||
}
|
||||
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal; POWDER_FAN = powderFan;
|
||||
// Establish the dock's ground before levelling, so its foundation cannot erase water.
|
||||
placeDock(spawnX + 34);
|
||||
yield [0.99, 'Finishing'];
|
||||
levelWorldWater();
|
||||
// re-ambient after settling, then start clean
|
||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
||||
tick = 0;
|
||||
// Settling can carry lakes past noTree; leave their connected low banks treeless too.
|
||||
bgm.fill(0);
|
||||
for (const tree of trees) {
|
||||
let wet = false;
|
||||
for (let root = tree.x; root <= tree.x + 1 && !wet; root++) for (const d of [-1, 1]) {
|
||||
for (let x = root; x >= 0 && x < w; x += d) {
|
||||
const m = mat[tree.y * w + x];
|
||||
if (m === WATER) { wet = true; break; }
|
||||
if (m !== EMPTY && PH[m] !== PH_GAS) break;
|
||||
}
|
||||
}
|
||||
if (!wet) for (let k = 0; k < tree.cells.length; k += 2) bgm[tree.cells[k]] = tree.cells[k + 1];
|
||||
}
|
||||
findTreeBases();
|
||||
hot.fill(0); awake.fill(0); updateRoof();
|
||||
// Wake generated water so it responds from the first tick.
|
||||
|
||||
@@ -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