clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

M12: island verticality - terraced runs (slope windows, per-cell terrace claims/mass) + corridor-blindness and claimsOf-duplication fixes

Dami committed 78184df
commit
78184df9ecfeacd4892686f3cbbb2b7474f84926
parent
7d7591dae3c0f954b2813055fb1538abdd533e90
author
Dami <dami.etoile@gmail.com>
7 files changed, +321 -44
Mprogress.md29++++-
Msrc/gen/endless/islands.ts42+++++-
Msrc/gen/endless/occupancy.ts7+-
Msrc/gen/endless/path.ts191++++++++++++++++++++++++++++--
Mtests/endless_test.ts19++-
Mtests/physics_test.ts8+-
Mtests/scenario_test.ts69++++++++++-
M progress.md +24 -5
@@ -777,11 +777,30 @@ per-cell worst-case SPEED ENVELOPE tracked by the planner so vocabulary is gated
777777 (no hairpin after a long chute unless a brake/spreader sits between). Every new778778 spec ships with named scenarios at envelope extremes.779779 780-**M12 — Island verticality.** Islands stop being flat: runs climb/descend ACROSS781-a mass (entry and exit at different elevations), terraced columns following the782-run's profile, switchbacks, x/y/z flow. Anchorage rule: every run cell supported783-at ITS height (the invariant already exists — it generalizes). Terrace step784-limits + scenarios.780+**M12 — Island verticality. [DONE this round.]** Island runs ≥7 cells may781+embed ONE slope window (0.5 or 1.0, climb or descent, shared vocab filtered782+to ≤0.5/cell parts, no drop-stacks; ≥2 flat shoreline cells each side).783+IslandCrossing gained `profile` (terrace top per run cell): flat cells784+anchor at their OWN base, each slope PART's cells share that part's low785+base (the ramp grounds on the low terrace and bridges — finite-mode rule).786+The envelope + canopy claims and the island builder both use the same787+nearest-run-cell terrace rule (Chebyshev, ties to the LOWER top — a rule788+mismatch would orphan claims, so it's mirrored verbatim). Island descents789+≥1.0 get boost zones like any chute; diamonds require flat, unreserved790+windows with same-level neighbors. TWO REAL BUGS found by the gates:791+(1) the envelope loop skipped ALL same-segment bands — the island's own792+APPROACH CORRIDOR (planned earlier in the same segment) was invisible and793+terraced mass plowed through it, jamming the marble (det-seed s=159); own794+bands are now transparent ONLY on run/branch cells. (2) Occupancy.claimsOf795+double-counted cells claimed twice under one ref (mass + canopy) — every796+island block had been placed TWICE since M5 (invisible: superimposed).797+Tests generalized (per-cell terrace anchorage, ≤1.0 terrace steps, mass798+never above the highest terrace) + new named scenario "island verticality"799+(emergence pinned). Gates: 71 tests green, 120×40 fuzz clean, 7-seed deep800+drives to the horizon, 6-sim-min browser soak (1 death, recovered, 0801+console errors); terraces read on screen. Gravity rotation / bouncers /802+icy: deferred to M13 backlog (icy needs a visible surface treatment first803+— zones must READ, per the set-piece rule).785804 786805 **M13 — Seed freeze + release polish (the old M6, moved to the end because the787806 slalom rescale and physics change everything before it).** Goldens permanent
M src/gen/endless/islands.ts +34 -8
@@ -126,6 +126,24 @@ export function buildIsland(
126126 const { id, topY, margin } = crossing;127127 const worldTop = topY * TRACK_SCALE;128128 const ref = `isle/${id}`;129+ // Terrace top per TRACK cell — the EXACT rule the planner claimed with130+ // (nearest run cell's floor base, Chebyshev, ties pick the lower top).131+ // Mass claims are recognized by their y1 matching this; recomputing132+ // with any other rule would orphan claims at tie cells.133+ const profile = crossing.profile;134+ const terraceTop = (cx: number, cz: number): number => {135+ let best = Infinity;136+ let top = topY;137+ for (let j = 0; j < runCells.length; j++) {138+ const [rx, rz] = runCells[j];139+ const d = Math.max(Math.abs(cx - rx), Math.abs(cz - rz));140+ if (d < best || (d === best && profile[j] < top)) {141+ best = d;142+ top = profile[j];143+ }144+ }145+ return top;146+ };129147 const biome = rng.weighted(BIOMES.map((b) => [b, b.weight] as const));130148 const placements: Placement[] = [];131149
@@ -158,8 +176,10 @@ export function buildIsland(
158176 const topCells: string[] = [];159177 for (const claim of occ.claimsOf(ref)) {160178 const { x, z, y0, y1 } = claim;161- if (Math.abs(y1 - topY) > 1e-9) continue; // not a mass claim (paranoia)162- const availWorld = (topY - y0) * TRACK_SCALE;179+ const cellTop = terraceTop(x, z);180+ if (Math.abs(y1 - cellTop) > 1e-9) continue; // canopy claim, not mass181+ const cellTopWorld = cellTop * TRACK_SCALE;182+ const availWorld = (cellTop - y0) * TRACK_SCALE;163183 if (availWorld < 0.999) continue;164184 for (const [wx, wz] of worldCellsOf(x, z)) {165185 const cellRng = rng.child(`cell/${wx}/${wz}`);
@@ -177,7 +197,7 @@ export function buildIsland(
177197 // Stack blocks top-down: full blocks, then a half/quarter shim so178198 // the underside lands exactly on the claimed grid.179199 let remaining = depth;180- let yTop = worldTop;200+ let yTop = cellTopWorld;181201 while (remaining >= 1 - 1e-9) {182202 placements.push(183203 block(`cliff_block_${biome.material}`, wx, yTop - 1, wz, cellRng),
@@ -211,11 +231,12 @@ export function buildIsland(
211231 }212232 topCells.push(key(wx, wz));213233 214- // Caps: biome surface tile on non-track tops.234+ // Caps: biome surface tile on non-track tops (at the cell's own235+ // terrace level).215236 if (!onRun && biome.capId && cellRng.chance(biome.capChance)) {216237 placements.push({217238 pieceId: `${NATURE}:${biome.capId}`,218- pos: [wx, worldTop, wz],239+ pos: [wx, cellTopWorld, wz],219240 rotation: cellRng.int(0, 4) as Placement["rotation"],220241 role: "decor",221242 });
@@ -244,14 +265,19 @@ export function buildIsland(
244265 if (!topSet.has(key(cx, cz)) || runWorld.has(key(cx, cz))) continue;245266 // Vegetation needs the canopy airspace claim (made at plan time in246267 // TRACK cells) — cells whose air an older line owns stay bare rather247- // than growing a tree through someone's channel.268+ // than growing a tree through someone's channel. Decor sits on the269+ // cell's own terrace.248270 const tx = Math.round(cx / TRACK_SCALE);249271 const tz = Math.round(cz / TRACK_SCALE);272+ const cellTop = terraceTop(tx, tz);250273 const ownsAir = occ251274 .at(tx, tz)252- .some((b) => b.ref === ref && b.y1 > topY + 0.5);275+ .some((b) => b.ref === ref && b.y1 > cellTop + 0.5);253276 if (!ownsAir) continue;254- placements.push({ ...p, pos: [p.pos[0], worldTop, p.pos[2]] });277+ placements.push({278+ ...p,279+ pos: [p.pos[0], cellTop * TRACK_SCALE, p.pos[2]],280+ });255281 }256282 }257283
M src/gen/endless/occupancy.ts +5 -2
M src/gen/endless/path.ts +176 -15
@@ -99,8 +99,12 @@ export interface IslandCrossing {
9999 /** Local index range of cells riding the island top. */100100 start: number;101101 len: number;102- /** Island top surface height == baseY of the anchored cells. */102+ /** Island surface height at the run's ENTRY (sloped runs terrace from103+ * here — see `profile`). */103104 topY: number;105+ /** Terrace top per run cell (floor base — a slope window's cells share106+ * its LOW base; the ramp bridges). Flat runs: all topY. */107+ profile: number[];104108 /** Blob margin (Chebyshev rings around the run) the mass may fill. */105109 margin: number;106110 /** Extra channel cells beside the run (a diamond's branch columns) —
@@ -681,11 +685,67 @@ class SegmentBuilder {
681685 const runLen = rng.int(5, 11);682686 const margin = runLen >= 8 ? rng.int(2, 4) : rng.int(1, 3);683687 684- // The run itself: flat, mostly straight, an occasional bend so the mass688+ // The run itself: mostly straight, an occasional bend so the mass685689 // isn't always a bar. First/last cells stay straight (clean shoreline686690 // ports). Mass needs at least one block below every run cell.691+ //692+ // ISLAND VERTICALITY (M12): longer runs may embed ONE slope window693+ // (0.5 or 1.0, climb or descent, from the shared vocab — terrace694+ // step ≤ 0.5/cell by construction) with ≥2 flat cells on each side.695+ // The mass terraces under the profile: every run cell is anchored at696+ // ITS height, and the envelope follows (nearest-run-cell tops).697+ interface RunSlope {698+ at: number;699+ option: SlopeOption;700+ climb: boolean;701+ }702+ let slope: RunSlope | null = null;703+ if (runLen >= 7 && rng.child("slope").chance(0.55)) {704+ const sRng = rng.child("slope/pick");705+ const climb = sRng.chance(0.45);706+ const delta = runLen >= 8 && sRng.chance(0.4) ? "1" : "0.5";707+ const vocab = climb ? CLIMB_VOCAB : DESCENT_VOCAB;708+ const options = vocab[delta].filter((o) =>709+ o.totalLen <= runLen - 4 &&710+ // Islands terrace at ≤0.5 per cell — no chute caps, no711+ // drop-stacks (asIs single-cell falls) inside a shoreline run.712+ o.parts.every((p) => p.delta / p.len <= 0.5 + 1e-9 && !p.asIs)713+ );714+ if (options.length > 0) {715+ const option = sRng.pick(options);716+ slope = {717+ at: sRng.int(2, runLen - option.totalLen - 1),718+ option,719+ climb,720+ };721+ }722+ }723+ const slopeCells = new Map<number, Step>();724+ if (slope) {725+ const parts = slope.climb726+ ? slope.option.parts727+ : slope.option.parts.slice().reverse();728+ let j = slope.at;729+ for (const part of parts) {730+ const signed = slope.climb ? part.delta : -part.delta;731+ for (let k = 0; k < part.len; k++) {732+ const lo = (slope.climb ? 0 : signed) - 1; // mass headroom below733+ const hi = (slope.climb ? signed : 0) + CHANNEL_CLEARANCE;734+ slopeCells.set(j++, {735+ move: "F",736+ band: [lo, hi],737+ ...(k === 0 ? { stepAfter: signed } : {}),738+ });739+ }740+ }741+ }687742 const steps: Step[] = [];688743 for (let j = 0; j < runLen; j++) {744+ const fromSlope = slopeCells.get(j);745+ if (fromSlope) {746+ steps.push(fromSlope);747+ continue;748+ }689749 const edge = j === 0 || j === runLen - 1;690750 const move = !edge && rng.chance(0.28)691751 ? (rng.chance(0.5) ? "L" : "R")
@@ -701,6 +761,52 @@ class SegmentBuilder {
701761 if (!this.commitSteps(steps)) return false;702762 const runCells = probed.cells.map((p) => [p.x, p.z] as const);703763 764+ // Slope reservations mirror commitSlope (parts in traversal order);765+ // big island descents get their boost zone like any chute.766+ if (slope) {767+ const parts = slope.climb768+ ? slope.option.parts769+ : slope.option.parts.slice().reverse();770+ let at = start + slope.at;771+ for (const part of parts) {772+ this.reservations.push({773+ start: at,774+ len: part.len,775+ pieceIds: part.ids,776+ reversed: !slope.climb && !part.asIs,777+ });778+ at += part.len;779+ }780+ const totalDrop = slope.option.parts.reduce((a, p) => a + p.delta, 0);781+ if (!slope.climb && totalDrop >= 1.0) {782+ const bStart = Math.max(start, start + slope.at - 2);783+ this.mods.push({784+ start: bStart,785+ len: start + slope.at + slope.option.totalLen - bStart,786+ kind: "boost",787+ });788+ }789+ }790+791+ // Terrace profile: flat cells anchor at their OWN base; each slope792+ // PART's cells share that part's low base (the ramp sits on the low793+ // terrace and bridges up — exactly how finite-mode ramps ground).794+ const baseYs = runCells.map((_, j) => this.cells[start + j].baseY);795+ const partLow = new Map<number, number>();796+ if (slope) {797+ const parts = slope.climb798+ ? slope.option.parts799+ : slope.option.parts.slice().reverse();800+ let at = slope.at;801+ for (const part of parts) {802+ const entryBase = baseYs[at];803+ const low = slope.climb ? entryBase : entryBase - part.delta;804+ for (let k = 0; k < part.len; k++) partLow.set(at + k, low);805+ at += part.len;806+ }807+ }808+ const profile = baseYs.map((y, j) => partLow.get(j) ?? y);809+704810 // Split–merge diamond on the island run (M11 branch respawns): find705811 // a straight stretch of ≥5 cells, check + claim both flank columns at706812 // deck level, and reserve the composite diamond window. The branch
@@ -731,10 +837,29 @@ class SegmentBuilder {
731837 );732838 for (const o of offsets) {733839 const j0 = s0 + o;840+ // Flat + unreserved only: a diamond can't straddle a slope841+ // window (its deck is one level by construction).842+ if (843+ this.reservations.some((r) =>844+ r.start < start + j0 + total && start + j0 < r.start + r.len845+ )846+ ) continue;847+ const flatY = this.cells[start + j0].baseY;848+ // Neighbors must share the level too, or the window's edge849+ // terrace (min-of-neighbors) drops below the deck and the850+ // split-double corner floats.851+ if (852+ (this.cells[start + j0 - 1]?.baseY ?? flatY) !== flatY ||853+ (this.cells[start + j0 + total]?.baseY ?? flatY) !== flatY854+ ) continue;734855 const flanks: (readonly [number, number])[] = [];735856 let ok = true;736857 for (let k = 0; k < total && ok; k++) {737858 const cell = this.cells[start + j0 + k];859+ if (cell.baseY !== flatY) {860+ ok = false;861+ break;862+ }738863 for (739864 const side of [turnLeft(cell.dirIn), turnRight(cell.dirIn)]740865 ) {
@@ -743,7 +868,7 @@ class SegmentBuilder {
743868 const fz = cell.z + dz;744869 if (745870 runCells.some(([rx, rz]) => rx === fx && rz === fz) ||746- !this.occ.isFree(fx, fz, topY, topY + CHANNEL_CLEARANCE)871+ !this.occ.isFree(fx, fz, flatY, flatY + CHANNEL_CLEARANCE)747872 ) {748873 ok = false;749874 break;
@@ -753,7 +878,13 @@ class SegmentBuilder {
753878 }754879 if (!ok) continue;755880 for (const [fx, fz] of flanks) {756- this.occ.claim(fx, fz, topY, topY + CHANNEL_CLEARANCE, this.ref);881+ this.occ.claim(882+ fx,883+ fz,884+ flatY,885+ flatY + CHANNEL_CLEARANCE,886+ this.ref,887+ );757888 }758889 branchCells.push(...flanks);759890 this.reservations.push({
@@ -803,20 +934,49 @@ class SegmentBuilder {
803934 );804935 }805936 }937+ // Terrace tops for the envelope: each cell takes the floor base of938+ // its NEAREST run cell (Chebyshev; ties pick the lower top — a low939+ // terrace beside the channel can never intrude into it). Flat runs940+ // reduce to the old constant-top behavior exactly.941+ const terraceTop = (cx: number, cz: number): number => {942+ let best = Infinity;943+ let top = topY;944+ for (let j = 0; j < runCells.length; j++) {945+ const [rx, rz] = runCells[j];946+ const d = Math.max(Math.abs(cx - rx), Math.abs(cz - rz));947+ if (d < best || (d === best && profile[j] < top)) {948+ best = d;949+ top = profile[j];950+ }951+ }952+ return top;953+ };954+ // Own-segment bands are transparent ONLY on the run's own cells (and955+ // a diamond's branch columns) — their channel claims carry the −1956+ // mass-headroom band the envelope must fill. Everywhere else a957+ // same-segment channel (the island's own approach corridor!) is as958+ // solid as any older line: skipping it let terraced mass plow959+ // through the corridor (det-seed jammed a marble in it).960+ const ownCells = new Set([961+ ...runCells.map(([x, z]) => key(x, z)),962+ ...branchCells.map(([x, z]) => key(x, z)),963+ ]);806964 for (const [cx, cz] of envelope.values()) {965+ const cellTop = terraceTop(cx, cz);807966 // Deepest free band under the top: stop at the first older claim.808- let floor = topY - ISLAND_MAX_DEPTH;967+ let floor = cellTop - ISLAND_MAX_DEPTH;809968 for (const b of this.occ.at(cx, cz)) {810- if (b.ref === this.ref || b.ref === isleRef) continue;811- if (b.y1 <= topY + 1e-9 && b.y1 > floor) floor = b.y1;812- // A band overlapping [floor, topY) from above (old track between813- // floor and top) — mass can't reach the top here; skip the cell.814- if (b.y0 < topY - 1e-9 && b.y1 > topY - 1e-9) {815- floor = topY;969+ if (b.ref === isleRef) continue;970+ if (b.ref === this.ref && ownCells.has(key(cx, cz))) continue;971+ if (b.y1 <= cellTop + 1e-9 && b.y1 > floor) floor = b.y1;972+ // A band overlapping [floor, cellTop) from above (old track973+ // between floor and top) — mass can't reach the top here; skip.974+ if (b.y0 < cellTop - 1e-9 && b.y1 > cellTop - 1e-9) {975+ floor = cellTop;816976 }817977 }818- if (topY - floor < 0.999) continue; // no room for even one block819- this.occ.claim(cx, cz, floor, topY, isleRef);978+ if (cellTop - floor < 0.999) continue; // no room for even one block979+ this.occ.claim(cx, cz, floor, cellTop, isleRef);820980 // Canopy airspace: non-run surface cells may grow tall decor (trees,821981 // banners); claim the air above them so no future line threads822982 // through a crown. Cells whose airspace is already spoken for simply
@@ -824,9 +984,9 @@ class SegmentBuilder {
824984 if (825985 !runCells.some(([rx, rz]) => rx === cx && rz === cz) &&826986 !corridor.has(key(cx, cz)) &&827- this.occ.isFree(cx, cz, topY, topY + CANOPY_HEIGHT)987+ this.occ.isFree(cx, cz, cellTop, cellTop + CANOPY_HEIGHT)828988 ) {829- this.occ.claim(cx, cz, topY, topY + CANOPY_HEIGHT, isleRef);989+ this.occ.claim(cx, cz, cellTop, cellTop + CANOPY_HEIGHT, isleRef);830990 }831991 }832992
@@ -835,6 +995,7 @@ class SegmentBuilder {
835995 start,836996 len: runLen,837997 topY,998+ profile,838999 margin,8391000 ...(branchCells.length > 0 ? { branchCells } : {}),8401001 });
M tests/endless_test.ts +14 -5
@@ -115,26 +115,35 @@ Deno.test("endless: 20-seed fuzz — continuity, clearance, anchors, memory", ()
115115 // ×TRACK_SCALE — the slalom rule).116116 for (const crossing of seg.plan.crossings) {117117 const isle = seg.islands.find((x) => x.id === crossing.id)!;118- const worldTop = crossing.topY * TRACK_SCALE;119118 for (let j = 0; j < crossing.len; j++) {120119 const c = seg.plan.cells[crossing.start + j];121- assertEquals(c.baseY, crossing.topY, "anchored cell at island top");120+ const cellTop = crossing.profile[j];121+ // M12 verticality: a run cell rides its own terrace — at the122+ // terrace top exactly, or bridging up to one slope-part delta123+ // above (a 1.0 ramp-long window grounds on its low side).124+ assert(125+ c.baseY >= cellTop - 1e-9 && c.baseY <= cellTop + 1.0 + 1e-9,126+ `seed ${n} island ${crossing.id}: cell ${j} base ${c.baseY} ` +127+ `off its terrace ${cellTop}`,128+ );129+ const cellTopWorld = cellTop * TRACK_SCALE;122130 const supported = isle.placements.some((p) =>123131 p.role === "support" &&124132 Math.abs(p.pos[0] - c.x * TRACK_SCALE) <= 0.51 &&125133 Math.abs(p.pos[2] - c.z * TRACK_SCALE) <= 0.51 &&126- p.pos[1] < worldTop134+ p.pos[1] < cellTopWorld127135 );128136 assert(129137 supported,130138 `seed ${n} island ${crossing.id}: no mass under (${c.x},${c.z})`,131139 );132140 }133- // Island mass never intrudes above the surface the track rides.141+ // Island mass never intrudes above the highest terrace.142+ const maxTopWorld = Math.max(...crossing.profile) * TRACK_SCALE;134143 for (const p of isle.placements) {135144 if (p.role !== "support") continue;136145 assert(137- p.pos[1] <= worldTop - 0.24,146+ p.pos[1] <= maxTopWorld - 0.24,138147 `mass block top above island surface at ${p.pos}`,139148 );140149 }
M tests/physics_test.ts +4 -4
M tests/scenario_test.ts +64 -5
@@ -171,15 +171,15 @@ function assertStreamInvariants(seed: string, through: number): void {
171171 }172172 for (const crossing of seg.plan.crossings) {173173 const isle = seg.islands.find((x) => x.id === crossing.id)!;174- const worldTop = crossing.topY * TRACK_SCALE;175174 for (let j = 0; j < crossing.len; j++) {176175 const c = seg.plan.cells[crossing.start + j];176+ const cellTopWorld = crossing.profile[j] * TRACK_SCALE;177177 assert(178178 isle.placements.some((p) =>179179 p.role === "support" &&180180 Math.abs(p.pos[0] - c.x * TRACK_SCALE) <= 0.51 &&181181 Math.abs(p.pos[2] - c.z * TRACK_SCALE) <= 0.51 &&182- p.pos[1] < worldTop182+ p.pos[1] < cellTopWorld183183 ),184184 `${seed} seg ${i}: island ${crossing.id} missing mass under run`,185185 );
@@ -342,6 +342,64 @@ Deno.test("bump lips: bumps realize ONLY fully inside a boost zone", () => {
342342 assert(bumps > 0, "no bump lips emerged across the corpus");343343 });344344 345+Deno.test("island verticality: sloped runs terrace, every cell anchored at its height", () => {346+ // M12: island runs may embed ONE slope window. Rules: terrace steps are347+ // bounded by the slope vocab (≤0.5 per cell along the profile); every348+ // run cell has mass topping out exactly at ITS floor base (the terrace349+ // it rides or bridges from); the run enters and exits through ≥2 flat350+ // shoreline cells; sloped islands must actually EMERGE.351+ const seeds = [352+ "cua-seed-1",353+ "det-seed",354+ "atmo-seed",355+ ...Array.from({ length: 15 }, (_, i) => `jump-scan-${i}`),356+ ];357+ let sloped = 0;358+ for (const seed of seeds) {359+ const w = new EndlessWorld(seed, catalog);360+ for (let i = 0; i < 5; i++) {361+ const seg = w.get(i);362+ for (const crossing of seg.plan.crossings) {363+ const { profile } = crossing;364+ const isle = seg.islands.find((x) => x.id === crossing.id)!;365+ const varied = profile.some((t) => t !== profile[0]);366+ if (varied) sloped++;367+ for (let j = 0; j < profile.length; j++) {368+ if (j > 0) {369+ // A terrace step is at most one slope part's delta (a 1.0370+ // ramp-long window bridges a full-level step).371+ assert(372+ Math.abs(profile[j] - profile[j - 1]) <= 1.0 + 1e-9,373+ `${seed} seg ${i}: terrace step > 1.0 at run cell ${j}`,374+ );375+ }376+ // Mass tops out exactly at the cell's terrace (top course377+ // underside within one block of the deck, top at the deck).378+ const c = seg.plan.cells[crossing.start + j];379+ const topWorld = profile[j] * TRACK_SCALE;380+ assert(381+ isle.placements.some((p) =>382+ p.role === "support" &&383+ Math.abs(p.pos[0] - c.x * TRACK_SCALE) <= 0.51 &&384+ Math.abs(p.pos[2] - c.z * TRACK_SCALE) <= 0.51 &&385+ p.pos[1] >= topWorld - 1.01 && p.pos[1] < topWorld386+ ),387+ `${seed} seg ${i}: run cell ${j} not anchored at terrace ` +388+ `${profile[j]}`,389+ );390+ }391+ // Shoreline: first/last 2 run cells flat at the entry/exit level.392+ assert(393+ profile[0] === profile[1] &&394+ profile[profile.length - 1] === profile[profile.length - 2],395+ `${seed} seg ${i}: island run lacks flat shoreline cells`,396+ );397+ }398+ }399+ }400+ assert(sloped > 0, "no sloped island runs emerged across the corpus");401+});402+345403 Deno.test("island diamonds: branches live on island track with claimed flanks", () => {346404 // Branch-respawn contract (rule-based): every diamond reservation sits347405 // fully inside an island run's straight cells, its crossing records the
@@ -379,16 +437,17 @@ Deno.test("island diamonds: branches live on island track with claimed flanks",
379437 crossing.branchCells.length >= r.len * 2,380438 `${seed} seg ${i}: diamond @${r.start} missing branch columns`,381439 );382- // Mass under every branch cell, and no decor placed on it.440+ // Mass under every branch cell, and no decor placed on it. The441+ // diamond sits on ONE terrace — its level is the window's cells'.383442 const isle = seg.islands.find((x) => x.id === crossing.id)!;384- const worldTop = crossing.topY * TRACK_SCALE;443+ const diamondTop = seg.plan.cells[r.start].baseY * TRACK_SCALE;385444 for (const [bx, bz] of crossing.branchCells) {386445 assert(387446 isle.placements.some((p) =>388447 p.role === "support" &&389448 Math.abs(p.pos[0] - bx * TRACK_SCALE) <= 0.51 &&390449 Math.abs(p.pos[2] - bz * TRACK_SCALE) <= 0.51 &&391- p.pos[1] < worldTop450+ p.pos[1] < diamondTop392451 ),393452 `${seed} seg ${i}: no mass under branch cell (${bx},${bz})`,394453 );