clone

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

main
marbles / tests / scenario_test.ts
scenario_test.ts 45.5K
1/**2 * M7 — scenario validation harness (USER RULE: validate against RULES and3 * NAMED SCENARIOS, not time-based fuzzing).4 *5 * Three layers:6 *7 * 1. TRANSITION MATRIX — every pair of piece variants that realization8 * could butt together (width-compatible ports) must agree on channel9 * floor height within the loop-gap epsilon. Enumerated from the10 * registry: no time dimension, no sampling — if a future piece lands11 * with a bad floor, this names the exact pair.12 *13 * 2. NAMED REGRESSIONS — every planner/generator bug found so far, pinned14 * by its original seed. These failed once; they must never fail again:15 * - island-run-self-intersection (endless-fuzz-2, seg 15): a turny16 * island run looped onto itself inside one probe.17 * - wfc-extras-strand (fuzz-1, seg 15): WFC footprint-extra claims18 * landed after the exit-corridor guard and stranded the successor.19 * - canopy-cul-de-sac (fuzz-5, seg 13): island canopy claims sealed a20 * corridor deeper than the flat-continuation check.21 * - planning-trap-entry (deep-50, seg 8): a trapped entry pose no retry22 * could fix — the successor-probe gate exists because of this seed.23 *24 * 3. SURVIVABILITY SCAFFOLD — the shape M10's physics scenarios plug into:25 * a scenario = generator work + invariant assertions. Physics kinds26 * (piece traversal at speed-envelope extremes) arrive with the physics27 * marble.28 */29import { assert, assertEquals } from "@std/assert";30import { buildRegistry } from "../src/gen/registry.ts";31import { EndlessWorld, KEEP_SEGMENTS } from "../src/gen/endless/world.ts";32import { TRACK_SCALE } from "../src/gen/endless/scale.ts";33import type { DecorCatalog } from "../src/gen/decor.ts";34import { goldenCatalog } from "../tools/pin_endless.ts";3536// The catalog the client streams with (all decor packs — M18a).37const catalog: DecorCatalog = goldenCatalog();3839// ---------------------------------------------------------------------------40// 1. Transition matrix41// ---------------------------------------------------------------------------4243Deno.test("transition matrix: width-compatible pairs agree on floor height", () => {44 const registry = buildRegistry();45 const FLOOR_EPS = 0.06; // the validate loop-gap epsilon46 const failures: string[] = [];47 // Group by port width so only realizable transitions are checked.48 for (const a of registry.variants) {49 const aOut = a.widthOut ?? "narrow";50 for (const b of registry.variants) {51 const bIn = b.widthIn ?? "narrow";52 if (aOut !== bIn) continue;53 const gap = Math.abs(a.floorOut - b.floorIn);54 if (gap > FLOOR_EPS) {55 failures.push(56 `${a.id}${a.reversed ? "(rev)" : ""} → ${b.id}${57 b.reversed ? "(rev)" : ""58 }: floor ${a.floorOut} → ${b.floorIn} (gap ${gap.toFixed(3)})`,59 );60 }61 }62 }63 assert(64 failures.length === 0,65 `floor-height mismatches at realizable transitions:\n${66 failures.slice(0, 12).join("\n")67 }${failures.length > 12 ? `\n… +${failures.length - 12} more` : ""}`,68 );69});7071Deno.test("transition matrix: every variant starts and ends on the port lattice", () => {72 const registry = buildRegistry();73 const bad: string[] = [];74 for (const v of registry.variants) {75 // Entry frame: entry port at (0, ·, −0.5); floors sit floorIn above the76 // entry base. Centerline must begin exactly there.77 const first = v.centerline[0];78 const start = first.kind === "line"79 ? first.a80 : first.kind === "cubic"81 ? first.p[0]82 : null;83 if (start) {84 const dx = Math.abs(start[0] - 0);85 const dz = Math.abs(start[2] - -0.5);86 const dy = Math.abs(start[1] - v.floorIn);87 if (dx > 1e-6 || dz > 1e-6 || dy > 1e-6) {88 bad.push(89 `${v.id}${v.reversed ? "(rev)" : ""}: centerline starts at ` +90 `(${start[0]}, ${start[1]}, ${91 start[2]92 }), expected (0, ${v.floorIn}, -0.5)`,93 );94 }95 }96 // Arc-starting pieces (bends: diameter ON the port face) are checked by97 // validateRegistry's port-continuity sweep; here we only pin the lattice98 // for line/cubic starts, which is where new authoring mistakes land.99 }100 assert(bad.length === 0, `off-lattice ports:\n${bad.join("\n")}`);101});102103// ---------------------------------------------------------------------------104// 2. Named regressions — bugs stay fixed105// ---------------------------------------------------------------------------106107interface StreamScenario {108 name: string;109 seed: string;110 /** Generate through this segment index (the original failure point). */111 through: number;112 why: string;113}114115const STREAM_REGRESSIONS: StreamScenario[] = [116 {117 name: "island-run-self-intersection",118 seed: "endless-fuzz-2",119 through: 16,120 why:121 "turny island run looped onto itself within one probe (probe now checks pending cells)",122 },123 {124 name: "wfc-extras-strand",125 seed: "fuzz-1",126 through: 16,127 why:128 "footprint-extra claims landed after the exit guard (realization now runs inside the planning tx)",129 },130 {131 name: "canopy-cul-de-sac",132 seed: "fuzz-5",133 through: 14,134 why:135 "canopy claims sealed a corridor deeper than the flat check (deep exit check + corridor exemption)",136 },137 {138 name: "planning-trap-entry",139 seed: "deep-50",140 through: 10,141 why:142 "trapped entry pose unfixable by retries (successor-probe gate exists because of this seed)",143 },144];145146/** Shared co-live stream invariants (same rules as the endless fuzz).147 * Full-helix ensembles revisit their own cells by construction (the148 * underpass) — within-window pairs are exempt from the collision rule,149 * exactly as in the endless fuzz test. */150function assertStreamInvariants(seed: string, through: number): void {151 const w = new EndlessWorld(seed, catalog);152 const channels = new Map<string, { y: number; seg: number; idx: number }[]>();153 for (let i = 0; i <= through; i++) {154 const seg = w.get(i);155 const helixSpans = seg.plan.reservations156 .filter((r) => /^helix-(large-)?(left|right)$/.test(r.pieceIds[0]))157 .map((r) => [r.start, r.start + r.len] as const);158 const spanOf = (idx: number) =>159 helixSpans.findIndex(([a, b]) => idx >= a && idx < b);160 for (const [ci, c] of seg.plan.cells.entries()) {161 const k = `${c.x},${c.z}`;162 const ys = channels.get(k) ?? [];163 for (const e of ys) {164 if (e.seg <= i - KEEP_SEGMENTS) continue;165 if (e.seg === i && spanOf(ci) >= 0 && spanOf(e.idx) === spanOf(ci)) {166 continue; // same full-helix ensemble167 }168 assert(169 Math.abs(e.y - c.baseY) >= 0.98,170 `${seed} seg ${i}: channel collision at ${k} (${e.y} vs ${c.baseY})`,171 );172 }173 ys.push({ y: c.baseY, seg: i, idx: ci });174 channels.set(k, ys);175 }176 for (const crossing of seg.plan.crossings) {177 const isle = seg.islands.find((x) => x.id === crossing.id)!;178 for (let j = 0; j < crossing.len; j++) {179 const c = seg.plan.cells[crossing.start + j];180 const cellTopWorld = crossing.profile[j] * TRACK_SCALE;181 assert(182 isle.placements.some((p) =>183 p.role === "support" &&184 Math.abs(p.pos[0] - c.x * TRACK_SCALE) <= 0.51 &&185 Math.abs(p.pos[2] - c.z * TRACK_SCALE) <= 0.51 &&186 p.pos[1] < cellTopWorld187 ),188 `${seed} seg ${i}: island ${crossing.id} missing mass under run`,189 );190 }191 }192 }193}194195for (const s of STREAM_REGRESSIONS) {196 Deno.test(`regression ${s.name} (${s.seed} → seg ${s.through})`, () => {197 // Generating through the original failure point without throwing IS the198 // core assertion; the invariants make it strict.199 assertStreamInvariants(s.seed, s.through);200 });201}202203// ---------------------------------------------------------------------------204// 3. Set-piece rules — the JUMP gap (M11)205// ---------------------------------------------------------------------------206207Deno.test("jump set-piece: every planned jump obeys the trajectory contract", () => {208 // Rule-based, no time dimension: for every gap the planner commits, the209 // layout MUST be 3 flat straight approach cells under a boost zone → the210 // gap (jump: 1 cell reserved slant-b reversed; propeller: 2 cells211 // reserved ramp/slant-long-b reversed, flown at 0.35 gravity) → ≥2 flat212 // straight landing cells exactly 0.5 track lower. The trajectory math213 // (launch ≈ 6.1 world u/s; 1.0-world drop in 0.316 s ≈ the 2.0-world214 // gap, or 0.535 s under propeller gravity ≈ the 4.0-world gap) is priced215 // INTO this shape; a gap violating it is a marble thrown into the void.216 const seeds = [217 "cua-seed-1",218 "det-seed",219 "atmo-seed",220 ...Array.from({ length: 12 }, (_, i) => `jump-scan-${i}`),221 ];222 let jumps = 0;223 let propellers = 0;224 let shocks = 0;225 for (const seed of seeds) {226 const w = new EndlessWorld(seed, catalog);227 for (let i = 0; i < 5; i++) {228 const seg = w.get(i);229 const { cells, mods, reservations } = seg.plan;230 for (const m of mods) {231 if (m.kind === "shock") {232 shocks++;233 continue;234 }235 if (m.kind !== "jump" && m.kind !== "propeller") continue;236 const propeller = m.kind === "propeller";237 if (propeller) propellers++;238 else jumps++;239 const g = m.start;240 const gapLen = propeller ? 2 : 1;241 const gapIds = propeller242 ? ["ramp-long-b", "slant-long-b"]243 : ["slant-b"];244 assert(245 m.len === gapLen,246 `${seed} seg ${i}: ${m.kind} zone must be ${gapLen} cell(s)`,247 );248 const res = reservations.find((r) => r.start === g);249 assert(250 res !== undefined && res.len === gapLen &&251 res.pieceIds.every((id) => gapIds.includes(id)) &&252 res.reversed === true,253 `${seed} seg ${i}: gap @${g} not reserved as ${gapIds}(rev)`,254 );255 const lipY = cells[g].baseY;256 for (let k = g - 3; k < g; k++) {257 assert(258 cells[k]?.move === "F" && cells[k].baseY === lipY,259 `${seed} seg ${i}: approach cell ${k} not flat straight`,260 );261 }262 assert(263 mods.some((b) =>264 b.kind === "boost" && b.start <= g - 3 && b.start + b.len >= g265 ),266 `${seed} seg ${i}: ${m.kind} @${g} lacks its boost approach`,267 );268 for (let k = g; k < g + gapLen; k++) {269 assert(270 cells[k].move === "F",271 `${seed} seg ${i}: gap cell ${k} must be straight`,272 );273 }274 for (let k = g + gapLen; k <= g + gapLen + 1; k++) {275 assert(276 cells[k]?.move === "F" && cells[k].baseY === lipY - 0.5,277 `${seed} seg ${i}: landing cell ${k} not flat at lip−0.5`,278 );279 }280 // Pad visual: the lip carries its banner so the set-piece reads —281 // but ONLY when the lip realized as a plain straight window (USER282 // RULE: no banners on curved-channel pieces, none under tunnel283 // arches).284 const lip = cells[g - 1];285 const lipWindow = seg.windowRanges.findIndex((w) =>286 g - 1 >= w.start && g - 1 < w.start + w.len287 );288 const lipIsPlain = lipWindow >= 0 &&289 seg.pieceFamilies[lipWindow] === "straight";290 const bannerId = propeller ? "banner-double-high" : "banner-high";291 const hasBanner = seg.placements.some((p) =>292 p.pieceId === bannerId &&293 Math.abs(p.pos[0] - lip.x * TRACK_SCALE) < 0.01 &&294 Math.abs(p.pos[2] - lip.z * TRACK_SCALE) < 0.01295 );296 assert(297 hasBanner === lipIsPlain,298 `${seed} seg ${i}: ${m.kind} @${g} banner=${hasBanner} but lip ` +299 `window is ${seg.pieceFamilies[lipWindow] ?? "none"}`,300 );301 }302 // Shock rule: every ≥1.5 chute descent has its impact-valley zone.303 for (const r of seg.plan.reservations) {304 const isChuteCap = r.pieceIds.some((id) =>305 id === "ramp-start-d" || id === "slant-long-d" ||306 id === "ramp-long-d"307 );308 if (!isChuteCap) continue;309 // Find the 1.5-descent's overall end: the LAST reservation of the310 // contiguous chain starting here would be complex to recover;311 // instead assert a shock zone starts within 3 cells after this312 // window's end (chains are ≤ 2 parts of ≤ 2 cells).313 assert(314 seg.plan.mods.some((mm) =>315 mm.kind === "shock" && mm.start >= r.start + r.len - 1 &&316 mm.start <= r.start + r.len + 2317 ),318 `${seed} seg ${i}: 1.5-chute @${r.start} has no shock zone`,319 );320 }321 }322 }323 assert(jumps > 0, "no plain jumps emerged across the scenario corpus");324 assert(propellers > 0, "no propeller jumps emerged across the corpus");325 assert(shocks > 0, "no shock zones emerged across the corpus");326});327328Deno.test("bump lips: bumps realize ONLY fully inside a boost zone", () => {329 // The momentum rule made physical: a bump window's 4 cells must sit330 // inside one boost zone (sprint runs) — the crest launch is priced by331 // the zone's speed. A bump outside a zone is the pre-M11 hazard the332 // exclude exists for.333 const seeds = [334 "cua-seed-1",335 "det-seed",336 "atmo-seed",337 ...Array.from({ length: 15 }, (_, i) => `jump-scan-${i}`),338 // M21 re-scan: sprint zones migrated with the climate bias — the339 // grav family carries the bump exemplars now.340 ...Array.from({ length: 10 }, (_, i) => `grav-scan-${i}`),341 ];342 let bumps = 0;343 for (const seed of seeds) {344 const w = new EndlessWorld(seed, catalog);345 for (let i = 0; i < 5; i++) {346 const seg = w.get(i);347 seg.pieceFamilies.forEach((fam, wi) => {348 if (fam !== "bump") return;349 bumps++;350 // Windows align 1:1 with windowMods; a bump fully inside a boost351 // zone resolves to "boost" (nothing outranks it on flat cells).352 assert(353 seg.windowMods[wi] === "boost",354 `${seed} seg ${i}: bump window ${wi} outside a boost zone`,355 );356 });357 }358 }359 assert(bumps > 0, "no bump lips emerged across the corpus");360});361362Deno.test("regression island-block-in-channel (hctqaix2hl): mass never blocks a co-live channel", () => {363 // USER REPORT (screenshot, seed hctqaix2hl seg 25): island mass blocks364 // stood inside the channel and stopped the marble dead — the envelope365 // loop was blind to same-segment channel claims (the island's own366 // approach corridor). Placement-level check: no island block may367 // intersect any co-live channel's clearance band.368 const blockH = (id: string) =>369 id.includes("Quarter") ? 0.25 : id.includes("Half") ? 0.5 : 1;370 for (const seed of ["hctqaix2hl", "det-seed"]) {371 const w = new EndlessWorld(seed, catalog);372 const chans: { x: number; z: number; y: number; seg: number }[] = [];373 for (let i = 0; i <= 26; i++) {374 const seg = w.get(i);375 for (const c of seg.plan.cells) {376 chans.push({ x: c.x, z: c.z, y: c.baseY, seg: i });377 }378 for (const isle of seg.islands) {379 for (const p of isle.placements) {380 if (p.role !== "support") continue;381 const h = blockH(p.pieceId);382 const [bx, by, bz] = p.pos;383 for (const ch of chans) {384 if (ch.seg <= i - KEEP_SEGMENTS) continue;385 if (386 Math.abs(ch.x * TRACK_SCALE - bx) > 1.01 ||387 Math.abs(ch.z * TRACK_SCALE - bz) > 1.01388 ) continue;389 const y0 = ch.y * TRACK_SCALE;390 const y1 = y0 + 1.98;391 assert(392 !(by < y1 - 1e-6 && y0 < by + h - 1e-6),393 `${seed} seg ${i}: island ${isle.id} block at (${p.pos}) ` +394 `intersects seg ${ch.seg} channel (${ch.x},${ch.z})`,395 );396 }397 }398 }399 }400 }401});402403Deno.test("zone banners: only on plain straight windows (never curves/tunnels)", () => {404 // USER RULE: flags/banners cannot go on curved pieces (waves/wiggles405 // swing the channel away from the posts) or share a cell with a tunnel406 // arch. Every banner placement must map to a family-"straight" window.407 const seeds = [408 "det-seed",409 "atmo-seed",410 ...Array.from({ length: 10 }, (_, i) => `jump-scan-${i}`),411 ];412 let banners = 0;413 for (const seed of seeds) {414 const w = new EndlessWorld(seed, catalog);415 for (let i = 0; i < 5; i++) {416 const seg = w.get(i);417 for (const p of seg.placements) {418 if (!p.pieceId.startsWith("banner")) continue;419 banners++;420 const cx = Math.round(p.pos[0] / TRACK_SCALE);421 const cz = Math.round(p.pos[2] / TRACK_SCALE);422 const idx = seg.plan.cells.findIndex((c) => c.x === cx && c.z === cz);423 assert(idx >= 0, `${seed} seg ${i}: banner off the line at ${p.pos}`);424 const wi = seg.windowRanges.findIndex((r) =>425 idx >= r.start && idx < r.start + r.len426 );427 assert(428 wi >= 0 && seg.pieceFamilies[wi] === "straight",429 `${seed} seg ${i}: banner on a ${430 seg.pieceFamilies[wi] ?? "?"431 } window at cell (${cx},${cz})`,432 );433 }434 }435 }436 assert(banners > 0, "no banners emerged across the corpus");437});438439Deno.test("gravity zones: flat straight runs with brake runway, and they READ", () => {440 // M13 gravity rotation: a gravity zone tilts WORLD gravity forward441 // along the run. Rules per committed zone: every zone cell is a flat442 // straight "F" cell at one base (the surge is gravity's, not a slope's),443 // the zone leaves ≥2 cells of same-clause runway before the next444 // non-straight thing could arrive (planner stops the mod 2 short of its445 // clause — verified as 2 flat straight cells beyond the zone), and the446 // zone's placements carry zone="gravity" so the client recolors them.447 const seeds = [448 "det-seed",449 "atmo-seed",450 ...Array.from({ length: 12 }, (_, i) => `grav-scan-${i}`),451 ];452 let zones = 0;453 for (const seed of seeds) {454 const w = new EndlessWorld(seed, catalog);455 for (let i = 0; i < 5; i++) {456 const seg = w.get(i);457 const { cells, mods } = seg.plan;458 for (const m of mods) {459 if (m.kind !== "gravity") continue;460 zones++;461 const baseY = cells[m.start].baseY;462 for (let k = m.start; k < m.start + m.len; k++) {463 assert(464 cells[k]?.move === "F" && cells[k].baseY === baseY,465 `${seed} seg ${i}: gravity zone cell ${k} not flat straight`,466 );467 }468 for (let k = m.start + m.len; k < m.start + m.len + 2; k++) {469 assert(470 cells[k]?.move === "F" && cells[k].baseY === baseY,471 `${seed} seg ${i}: gravity runway cell ${k} not flat straight`,472 );473 }474 // The surface READS: at least one placement in the zone's span is475 // tagged zone="gravity" (unless a higher-priority mod overlaps476 // the same window — rare; the zone must still be flat above).477 // Cell match is Chebyshev ≤ 1: wave meshes anchor their origin a478 // column off the centerline (grav-scan-3's whole zone realized479 // as one wave window and the exact-cell match missed its tag).480 const zoneXZ: [number, number][] = [];481 for (let k = m.start; k < m.start + m.len; k++) {482 zoneXZ.push([cells[k].x, cells[k].z]);483 }484 const tagged = seg.placements.some((p) => {485 if (p.zone !== "gravity") return false;486 const px = Math.round(p.pos[0] / TRACK_SCALE);487 const pz = Math.round(p.pos[2] / TRACK_SCALE);488 return zoneXZ.some(([zx, zz]) =>489 Math.max(Math.abs(px - zx), Math.abs(pz - zz)) <= 1490 );491 });492 const overlapped = mods.some((o) =>493 o !== m && o.kind !== "gravity" &&494 o.start < m.start + m.len && m.start < o.start + o.len495 );496 assert(497 tagged || overlapped,498 `${seed} seg ${i}: gravity zone @${m.start} has no tagged placement`,499 );500 }501 }502 }503 assert(zones > 0, "no gravity zones emerged across the corpus");504});505506Deno.test("pipe chapters: every track placement carries its island's chapter", () => {507 // USER RULE: the pipe color is PER ISLAND. Every track placement in an508 // endless segment carries a chapter (the most recent island's id at its509 // cells); chapters never decrease along a segment's windows, step only510 // at island run starts, and the stream shows ≥2 chapters early (colors511 // actually change).512 const seeds = ["det-seed", "atmo-seed", "jump-scan-3"];513 const chaptersSeen = new Set<number>();514 for (const seed of seeds) {515 const w = new EndlessWorld(seed, catalog);516 let prevSegLast = -1;517 for (let i = 0; i < 4; i++) {518 const seg = w.get(i);519 let last = prevSegLast;520 for (const p of seg.placements) {521 if (p.role !== "track") continue;522 assert(523 p.chapter !== undefined,524 `${seed} seg ${i}: track placement ${p.pieceId} missing chapter`,525 );526 chaptersSeen.add(p.chapter!);527 }528 // Windows walk forward: chapter per window start must be529 // non-decreasing and only ever step to an island id in this stream.530 for (const r of seg.windowRanges) {531 let ch = prevSegLast;532 for (const c of seg.plan.crossings) {533 if (c.start <= r.start && c.id > ch) ch = c.id;534 }535 assert(ch >= last, `${seed} seg ${i}: chapter decreased`);536 last = ch;537 }538 prevSegLast = last;539 }540 }541 assert(542 chaptersSeen.size >= 2,543 "streams never changed pipe chapter — per-island color is dead",544 );545});546547Deno.test("island verticality: sloped runs terrace, every cell anchored at its height", () => {548 // M12: island runs may embed ONE slope window. Rules: terrace steps are549 // bounded by the slope vocab (≤0.5 per cell along the profile); every550 // run cell has mass topping out exactly at ITS floor base (the terrace551 // it rides or bridges from); the run enters and exits through ≥2 flat552 // shoreline cells; sloped islands must actually EMERGE.553 const seeds = [554 "cua-seed-1",555 "det-seed",556 "atmo-seed",557 ...Array.from({ length: 15 }, (_, i) => `jump-scan-${i}`),558 ];559 let sloped = 0;560 for (const seed of seeds) {561 const w = new EndlessWorld(seed, catalog);562 for (let i = 0; i < 5; i++) {563 const seg = w.get(i);564 for (const crossing of seg.plan.crossings) {565 const { profile } = crossing;566 const isle = seg.islands.find((x) => x.id === crossing.id)!;567 const varied = profile.some((t) => t !== profile[0]);568 if (varied) sloped++;569 for (let j = 0; j < profile.length; j++) {570 if (j > 0) {571 // A terrace step is at most one slope part's delta (a 1.0572 // ramp-long window bridges a full-level step).573 assert(574 Math.abs(profile[j] - profile[j - 1]) <= 1.0 + 1e-9,575 `${seed} seg ${i}: terrace step > 1.0 at run cell ${j}`,576 );577 }578 // Mass tops out exactly at the cell's terrace (top course579 // underside within one block of the deck, top at the deck).580 const c = seg.plan.cells[crossing.start + j];581 const topWorld = profile[j] * TRACK_SCALE;582 assert(583 isle.placements.some((p) =>584 p.role === "support" &&585 Math.abs(p.pos[0] - c.x * TRACK_SCALE) <= 0.51 &&586 Math.abs(p.pos[2] - c.z * TRACK_SCALE) <= 0.51 &&587 p.pos[1] >= topWorld - 1.01 && p.pos[1] < topWorld588 ),589 `${seed} seg ${i}: run cell ${j} not anchored at terrace ` +590 `${profile[j]}`,591 );592 }593 // Shoreline: first/last 2 run cells flat at the entry/exit level.594 assert(595 profile[0] === profile[1] &&596 profile[profile.length - 1] === profile[profile.length - 2],597 `${seed} seg ${i}: island run lacks flat shoreline cells`,598 );599 }600 }601 }602 assert(sloped > 0, "no sloped island runs emerged across the corpus");603});604605Deno.test("island wedges: slope/diagonal trims respect the surrounding surface", () => {606 // USER RULE (2026-08-09): pyramid/wedge blocks must respect the surface607 // they are on — no illogical rotations. Per emitted trim block: a608 // cliff_blockSlope's descending face (local +x under its rotation) must609 // look onto air a FULL course deep (neighbor column absent or its top610 // at/below the wedge's base); a cliff_blockDiagonal's cut corner spans two611 // adjacent open dirs AND the diagonal cell between them must be open612 // too, or the cut face jams into that block's corner.613 const seeds = [614 "det-seed",615 "atmo-seed",616 "hctqaix2hl",617 ...Array.from({ length: 10 }, (_, i) => `jump-scan-${i}`),618 ];619 const heightOf = (id: string): number =>620 id.includes("blockQuarter") ? 0.25 : id.includes("blockHalf") ? 0.5 : 1;621 const DIRV: readonly (readonly [number, number])[] = [622 [1, 0],623 [0, 1],624 [-1, 0],625 [0, -1],626 ];627 let slopes = 0;628 let diagonals = 0;629 for (const seed of seeds) {630 const w = new EndlessWorld(seed, catalog);631 for (let i = 0; i < 6; i++) {632 for (const isle of w.get(i).islands) {633 // Rebuild the mass surface: per cell, the highest block top.634 const tops = new Map<string, number>();635 for (const p of isle.placements) {636 if (p.role !== "support" || !p.pieceId.includes("cliff_")) continue;637 const k = `${p.pos[0]},${p.pos[2]}`;638 const top = p.pos[1] + heightOf(p.pieceId);639 if ((tops.get(k) ?? -Infinity) < top) tops.set(k, top);640 }641 const openFrom = (p: { pos: readonly number[] }, w2: number) => {642 const [dx, dz] = DIRV[w2];643 const t = tops.get(`${p.pos[0] + dx},${p.pos[2] + dz}`);644 return t === undefined || t <= p.pos[1] + 0.01;645 };646 for (const p of isle.placements) {647 if (p.pieceId.includes("cliff_blockSlope")) {648 slopes++;649 const w2 = (4 - p.rotation) % 4; // local +x → world dir650 assert(651 openFrom(p, w2),652 `${seed} seg ${i}: slope at (${p.pos}) descends into a ` +653 `higher neighbor (dir ${w2})`,654 );655 } else if (p.pieceId.includes("cliff_blockDiagonal")) {656 diagonals++;657 const k0 = (4 - p.rotation) % 4; // cut anchor (local +x)658 const k1 = (k0 + 1) % 4; // cut partner (local +z)659 const [dx0, dz0] = DIRV[k0];660 const [dx1, dz1] = DIRV[k1];661 const diagTop = tops.get(662 `${p.pos[0] + dx0 + dx1},${p.pos[2] + dz0 + dz1}`,663 );664 assert(665 openFrom(p, k0) && openFrom(p, k1) &&666 (diagTop === undefined || diagTop <= p.pos[1] + 0.01),667 `${seed} seg ${i}: diagonal cut at (${p.pos}) jams into a ` +668 `higher neighbor (dirs ${k0},${k1})`,669 );670 }671 }672 }673 }674 }675 assert(slopes > 0, "no slope trims emerged across the corpus");676 assert(diagonals > 0, "no diagonal trims emerged across the corpus");677});678679Deno.test("bouncer pins: mushroom bumpers only on wide island decks, clear of zones", () => {680 // M13 bouncers. Rules per bumper placement: it stands on a straight-wide681 // window cell of an island crossing (the ONLY channel wide enough to682 // pass a pin — recapture is the wide walls, survivability by683 // construction), offset laterally ≤ 0.25 track cells from the wide684 // centerline, standing ON the channel floor (+0.25 track over the deck685 // base), and never inside a planner zone (a boost bleeding into the686 // deck must launch true, not pinball).687 const seeds = [688 "det-seed",689 ...Array.from({ length: 15 }, (_, i) => `wide-scan-${i}`),690 ];691 let pins = 0;692 for (const seed of seeds) {693 const w = new EndlessWorld(seed, catalog);694 for (let i = 0; i < 5; i++) {695 const seg = w.get(i);696 for (const p of seg.placements) {697 if (!p.bumper) continue;698 pins++;699 assert(700 p.pieceId === "nature-kit:mushroom_red",701 `${seed} seg ${i}: bumper is ${p.pieceId}`,702 );703 const cx = Math.round(p.pos[0] / TRACK_SCALE);704 const cz = Math.round(p.pos[2] / TRACK_SCALE);705 const idx = seg.plan.cells.findIndex((c) => c.x === cx && c.z === cz);706 assert(idx >= 0, `${seed} seg ${i}: bumper off the line at ${p.pos}`);707 const cell = seg.plan.cells[idx];708 // Lateral offset within the pin envelope, standing on the floor.709 const offX = Math.abs(p.pos[0] / TRACK_SCALE - cell.x);710 const offZ = Math.abs(p.pos[2] / TRACK_SCALE - cell.z);711 assert(712 offX <= 0.25 + 1e-9 && offZ <= 0.25 + 1e-9,713 `${seed} seg ${i}: bumper too far off-center (${offX},${offZ})`,714 );715 assert(716 Math.abs(p.pos[1] / TRACK_SCALE - (cell.baseY + 0.25)) < 1e-9,717 `${seed} seg ${i}: bumper not on the channel floor`,718 );719 // Host window is a wide deck inside an island crossing.720 const wi = seg.windowRanges.findIndex((r) =>721 idx >= r.start && idx < r.start + r.len722 );723 assert(724 wi >= 0 && seg.pieceFamilies[wi] === "wide",725 `${seed} seg ${i}: bumper on a ${726 seg.pieceFamilies[wi] ?? "?"727 } window`,728 );729 assert(730 seg.plan.crossings.some((c) =>731 idx >= c.start && idx < c.start + c.len732 ),733 `${seed} seg ${i}: bumper outside any island crossing`,734 );735 assert(736 !seg.plan.mods.some((m) => idx >= m.start && idx < m.start + m.len),737 `${seed} seg ${i}: bumper inside a planner zone`,738 );739 }740 }741 }742 assert(pins > 0, "no bouncer pins emerged across the corpus");743});744745Deno.test("island diamonds: branches live on island track with claimed flanks", () => {746 // Branch-respawn contract (rule-based): every diamond reservation sits747 // fully inside an island run's straight cells, its crossing records the748 // branch columns (2 per window cell), and every branch cell is part of749 // the island's mass patch (checked via the island builder's placements:750 // mass directly under each branch cell, no decor on it).751 const seeds = [752 "cua-seed-1",753 "drift-seed",754 ...Array.from({ length: 12 }, (_, i) => `jump-scan-${i}`),755 ];756 let diamonds = 0;757 for (const seed of seeds) {758 const w = new EndlessWorld(seed, catalog);759 for (let i = 0; i < 5; i++) {760 const seg = w.get(i);761 for (const r of seg.plan.reservations) {762 if (!r.pieceIds[0]?.startsWith("diamond")) continue;763 diamonds++;764 const crossing = seg.plan.crossings.find((c) =>765 r.start >= c.start && r.start + r.len <= c.start + c.len766 );767 assert(768 crossing !== undefined,769 `${seed} seg ${i}: diamond @${r.start} not inside an island run`,770 );771 for (let j = 0; j < r.len; j++) {772 assert(773 seg.plan.cells[r.start + j].move === "F",774 `${seed} seg ${i}: diamond cell ${r.start + j} not straight`,775 );776 }777 assert(778 crossing.branchCells !== undefined &&779 crossing.branchCells.length >= r.len * 2,780 `${seed} seg ${i}: diamond @${r.start} missing branch columns`,781 );782 // Mass under every branch cell, and no decor placed on it. The783 // diamond sits on ONE terrace — its level is the window's cells'.784 const isle = seg.islands.find((x) => x.id === crossing.id)!;785 const diamondTop = seg.plan.cells[r.start].baseY * TRACK_SCALE;786 for (const [bx, bz] of crossing.branchCells) {787 assert(788 isle.placements.some((p) =>789 p.role === "support" &&790 Math.abs(p.pos[0] - bx * TRACK_SCALE) <= 0.51 &&791 Math.abs(p.pos[2] - bz * TRACK_SCALE) <= 0.51 &&792 p.pos[1] < diamondTop793 ),794 `${seed} seg ${i}: no mass under branch cell (${bx},${bz})`,795 );796 for (const p of isle.placements) {797 if (p.role !== "decor") continue;798 const inCell = Math.abs(p.pos[0] - bx * TRACK_SCALE) < 1 &&799 Math.abs(p.pos[2] - bz * TRACK_SCALE) < 1;800 assert(801 !inCell,802 `${seed} seg ${i}: decor on branch cell (${bx},${bz})`,803 );804 }805 }806 // The path window carries the alt centerline through to the807 // client (scaled): the loop segment for this window has alt.808 assert(809 seg.path.segments.some((ls) => ls.alt !== undefined),810 `${seed} seg ${i}: diamond window lost its alt centerline`,811 );812 }813 }814 }815 assert(diamonds > 0, "no island diamonds emerged across the corpus");816});817818// ---------------------------------------------------------------------------819// Full-helix spiral dives in the void (promoted backlog)820// ---------------------------------------------------------------------------821822Deno.test("helix dive: full-helix ensembles in the void obey the window contract", () => {823 // Rules, per committed full-helix reservation across a seed corpus:824 // - the window's moves equal the reserved def's moves exactly (realize825 // matches by moves — a drifted window would fall back to something826 // else silently);827 // - step-at-start: cell 0 carries the entry base, every interior cell828 // the exit base (entry − 2), and the line continues at −2;829 // - the ensemble REVISITS its first cells (the underpass) — that's the830 // duplicate-cell-tolerant property this clause exists for;831 // - the window realizes as ONE helix-family window (the composite def,832 // not a fallback), and its placements survive to the client;833 // - at most one full helix per segment (set-piece, not texture).834 const registry = buildRegistry();835 const isFull = (id: string) => /^helix-(large-)?(left|right)$/.test(id);836 let seen = 0;837 for (let n = 0; n < 8; n++) {838 const seed = `helix-scan-${n}`;839 const w = new EndlessWorld(seed, catalog);840 for (let i = 0; i < 8; i++) {841 const seg = w.get(i);842 const fulls = seg.plan.reservations.filter((r) => isFull(r.pieceIds[0]));843 assert(844 fulls.length <= 1,845 `${seed} seg ${i}: ${fulls.length} full helixes`,846 );847 for (const r of fulls) {848 seen++;849 const def = registry.variants.find(850 (v) => v.id === r.pieceIds[0] && !v.reversed,851 )!;852 const cells = seg.plan.cells.slice(r.start, r.start + r.len);853 assert(854 cells.map((c) => c.move).join("") === def.moves,855 `${seed} seg ${i}: window moves diverge from ${def.id}`,856 );857 const entryY = cells[0].baseY;858 for (let j = 1; j < cells.length; j++) {859 assert(860 Math.abs(cells[j].baseY - (entryY - 2)) < 1e-9,861 `${seed} seg ${i}: interior cell ${j} off the exit base`,862 );863 }864 const after = seg.plan.cells[r.start + r.len];865 if (after) {866 assert(867 Math.abs(after.baseY - (entryY - 2)) < 1e-9,868 `${seed} seg ${i}: line does not continue at −2 after the dive`,869 );870 }871 const coords = cells.map((c) => `${c.x},${c.z}`);872 assert(873 new Set(coords).size < coords.length,874 `${seed} seg ${i}: no revisited cells — not a full helix?`,875 );876 // Realized as the composite: exactly one window covers the span,877 // family helix.878 const win = seg.windowRanges.findIndex(879 (wr) => wr.start === r.start && wr.len === r.len,880 );881 assert(882 win >= 0 && seg.pieceFamilies[win] === "helix",883 `${seed} seg ${i}: reservation did not realize as one helix window`,884 );885 // The composite's mesh reaches the client (placement at the window).886 assert(887 seg.placements.some((p) =>888 p.pieceId === def.meshId || (!def.meshId && p.pieceId === def.id)889 ),890 `${seed} seg ${i}: no placement for the helix composite`,891 );892 }893 }894 }895 assert(seen >= 4, `too few full helixes across the corpus (${seen})`);896});897898// ---------------------------------------------------------------------------899// Ground vocabulary on islands (promoted backlog): an island top IS local900// ground — wide runs, stub sidings, fans, and solid piece forms come back901// where the window rides its terrace. (Tunnels were never gated.)902// ---------------------------------------------------------------------------903904Deno.test("island ground vocabulary: wide runs, furniture, solids obey the rules", () => {905 const CAP_IDS = new Set(["end-rounded", "end-square"]);906 const SOLID_PREFIXES = [907 "corner-solid",908 "curve-solid",909 "slant-solid",910 "ramp-long-solid",911 "ramp-start-solid",912 "ramp-end-solid",913 "bend-solid",914 "wave-solid",915 "bump-solid",916 ];917 let wides = 0;918 let furnished = 0;919 let solids = 0;920 for (let n = 0; n < 10; n++) {921 const seed = `vocab-${n}`;922 const w = new EndlessWorld(seed, catalog);923 for (let i = 0; i < 8; i++) {924 const seg = w.get(i);925 const { crossings } = seg.plan;926 const inCrossing = (idx: number) =>927 crossings.find((c) => idx >= c.start && idx < c.start + c.len);928 const grounded = (idx: number) => {929 const c = inCrossing(idx);930 return c !== undefined &&931 seg.plan.cells[idx].baseY === c.profile[idx - c.start];932 };933934 // WIDE RUNS: island-only, flat on the terrace, funnel-bracketed,935 // flanks recorded as track patch with mass beneath.936 for (let wi = 0; wi < seg.pieceFamilies.length; wi++) {937 if (seg.pieceFamilies[wi] !== "wide") continue;938 wides++;939 const range = seg.windowRanges[wi];940 for (let j = range.start; j < range.start + range.len; j++) {941 assert(grounded(j), `${seed} seg ${i}: wide cell ${j} off ground`);942 }943 assert(944 seg.pieceFamilies[wi - 1] === "funnel" ||945 seg.pieceFamilies[wi - 1] === "wide",946 `${seed} seg ${i}: wide window ${wi} not funnel-bracketed before`,947 );948 assert(949 seg.pieceFamilies[wi + 1] === "funnel" ||950 seg.pieceFamilies[wi + 1] === "wide",951 `${seed} seg ${i}: wide window ${wi} not funnel-bracketed after`,952 );953 const c = inCrossing(range.start)!;954 assert(955 (c.wideFlanks ?? []).length > 0,956 `${seed} seg ${i}: wide run without recorded flanks`,957 );958 }959960 // FURNITURE: every cap/fan placement stands on a planner side cell;961 // every mutated split host is a recorded furniture host.962 const sideSet = new Set(963 crossings.flatMap((c) =>964 (c.sideCells ?? []).map((sc) => `${sc.cell[0]},${sc.cell[1]}`)965 ),966 );967 const hostSet = new Set(968 crossings.flatMap((c) => (c.sideCells ?? []).map((sc) => sc.host)),969 );970 // Every ledge whose host realized as a plain straight must carry971 // exactly one furnishing (cap or fan); a split host pairs with a972 // cap (the siding), a straight host with a fan.973 const placementAt = (974 cx: number,975 cz: number,976 cy: number,977 ids: (id: string) => boolean,978 ) =>979 seg.placements.find((p) =>980 p.role === "track" && ids(p.pieceId) &&981 Math.round(p.pos[0] / TRACK_SCALE) === cx &&982 Math.round(p.pos[2] / TRACK_SCALE) === cz &&983 // Same level — an old drop-stack receiver in the same column984 // must not alias as this ledge's furnishing.985 Math.abs(p.pos[1] / TRACK_SCALE - cy) < 0.3986 );987 const familyAt = (idx: number): string | null => {988 for (let wi = 0; wi < seg.windowRanges.length; wi++) {989 const r = seg.windowRanges[wi];990 if (idx >= r.start && idx < r.start + r.len) {991 return seg.pieceFamilies[wi];992 }993 }994 return null;995 };996 for (const c of crossings) {997 for (const sc of c.sideCells ?? []) {998 // Furnished only when the host window realized as a PLAIN999 // straight (a tunnel's mesh is "straight" + an arch overlay —1000 // no side port there).1001 if (familyAt(sc.host) !== "straight") continue;1002 const host = seg.plan.cells[sc.host];1003 const hostPiece = placementAt(1004 host.x,1005 host.z,1006 host.baseY,1007 (id) => id === "straight" || id === "split",1008 );1009 if (!hostPiece) continue; // host realized as something else1010 const furnishing = placementAt(1011 sc.cell[0],1012 sc.cell[1],1013 host.baseY,1014 (id) => CAP_IDS.has(id) || id.startsWith("fan-"),1015 );1016 assert(1017 furnishing,1018 `${seed} seg ${i}: bare ledge at ${sc.cell} (host ${hostPiece.pieceId})`,1019 );1020 furnished++;1021 if (hostPiece.pieceId === "split") {1022 assert(1023 CAP_IDS.has(furnishing.pieceId),1024 `${seed} seg ${i}: split host with non-cap ${furnishing.pieceId}`,1025 );1026 } else {1027 assert(1028 furnishing.pieceId.startsWith("fan-"),1029 `${seed} seg ${i}: straight host with non-fan ${furnishing.pieceId}`,1030 );1031 }1032 }1033 }1034 for (const p of seg.placements) {1035 if (p.role !== "track") continue;1036 const cellKey = `${Math.round(p.pos[0] / TRACK_SCALE)},${1037 Math.round(p.pos[2] / TRACK_SCALE)1038 }`;1039 // Fans exist ONLY as ledge furniture in endless.1040 if (p.pieceId.startsWith("fan-")) {1041 assert(1042 sideSet.has(cellKey),1043 `${seed} seg ${i}: fan off its ledge (${cellKey})`,1044 );1045 }1046 if (p.pieceId === "split") {1047 // A split junction is either a furnished ledge host, a takeable1048 // spur host (the M13 spur wired into endless), or a braid1049 // fork/merge host (M17).1050 const hostCell = [...hostSet].some((h) => {1051 const c = seg.plan.cells[h];1052 return `${c.x},${c.z}` === cellKey;1053 }) || seg.plan.spurs.some((s) => {1054 const c = seg.plan.cells[s.host];1055 return `${c.x},${c.z}` === cellKey;1056 }) || seg.plan.braids.some((b) => {1057 const f = seg.plan.cells[b.start];1058 const mg = seg.plan.cells[b.start + b.len - 1];1059 return `${f.x},${f.z}` === cellKey ||1060 `${mg.x},${mg.z}` === cellKey;1061 });1062 assert(1063 hostCell,1064 `${seed} seg ${i}: split junction at ${cellKey} has no ledge`,1065 );1066 }1067 // SOLIDS: only near grounded island cells (an island top IS1068 // local ground; the void grows no solids).1069 if (SOLID_PREFIXES.some((s) => p.pieceId.startsWith(s))) {1070 solids++;1071 const near = seg.plan.cells.some((c, idx) =>1072 grounded(idx) &&1073 Math.abs(c.x - p.pos[0] / TRACK_SCALE) <= 3 &&1074 Math.abs(c.z - p.pos[2] / TRACK_SCALE) <= 31075 );1076 assert(1077 near,1078 `${seed} seg ${i}: solid ${p.pieceId} far from island ground`,1079 );1080 }1081 }1082 }1083 }1084 assert(wides > 0, "no wide runs emerged across the corpus");1085 assert(furnished > 0, "no furniture emerged across the corpus");1086 assert(solids > 0, "no solids emerged across the corpus");1087});10881089// ---------------------------------------------------------------------------1090// Gust walls (M19a): pulsed cross fans — pinned bank + corpus-wide laws.1091// ---------------------------------------------------------------------------10921093Deno.test("gust walls: pulses in band, banks phase-locked, pairs anti-phase", () => {1094 // Pinned exemplar: wind-scan-9 grows a phase-locked bank in segment 41095 // (M24 re-scan; wide-scan-30/13 held earlier pins).1096 const pinned = new EndlessWorld("wind-scan-9", catalog);1097 let pinnedBank = 0;1098 for (let i = 0; i <= 4; i++) {1099 const gusts = pinned.get(i).placements.filter((p) => p.wind?.pulse);1100 if (gusts.length >= 2) {1101 const periods = gusts.map((g) => g.wind!.pulse!.period);1102 if (periods.every((p) => p === periods[0])) pinnedBank++;1103 }1104 }1105 assert(pinnedBank > 0, "wind-scan-9 lost its phase-locked gust bank");11061107 // Corpus-wide laws over the standard scan families.1108 const seeds = [1109 ...Array.from({ length: 15 }, (_, i) => `wide-scan-${i}`),1110 ...Array.from({ length: 12 }, (_, i) => `grav-scan-${i}`),1111 ...Array.from({ length: 12 }, (_, i) => `jump-scan-${i}`),1112 ];1113 let gusts = 0;1114 for (const seed of seeds) {1115 const w = new EndlessWorld(seed, catalog);1116 for (let i = 0; i < 6; i++) {1117 const seg = w.get(i);1118 const segGusts = seg.placements.filter((p) => p.wind?.pulse);1119 for (const p of segGusts) {1120 gusts++;1121 const pulse = p.wind!.pulse!;1122 assertEquals(pulse.duty, 0.45, `${seed} seg ${i}: gust duty`);1123 assert(1124 pulse.period >= 2.4 - 1e-9 && pulse.period <= 3.6 + 1e-9,1125 `${seed} seg ${i}: gust period ${pulse.period}`,1126 );1127 assert(pulse.phase >= -1e-9, `${seed} seg ${i}: negative phase`);1128 // A gust wall is a full-strength blockade and still a fan on a1129 // ledge (the fan rule test covers cell rounding; here: strength).1130 assertEquals(p.wind!.strength, 14, `${seed} seg ${i}: gust strength`);1131 }1132 // Same-cell opposed pairs must be anti-phase on a shared period.1133 const byCell = new Map<string, typeof segGusts>();1134 for (const p of segGusts) {1135 const k = `${Math.round(p.pos[0])},${Math.round(p.pos[1])},${1136 Math.round(p.pos[2])1137 }`;1138 byCell.set(k, [...(byCell.get(k) ?? []), p]);1139 }1140 for (const list of byCell.values()) {1141 if (list.length < 2) continue;1142 const [a, b] = list;1143 assertEquals(a.wind!.pulse!.period, b.wind!.pulse!.period);1144 const diff = Math.abs(a.wind!.pulse!.phase - b.wind!.pulse!.phase);1145 assert(1146 Math.abs(diff - a.wind!.pulse!.period / 2) < 1e-9,1147 `${seed} seg ${i}: opposed pair not anti-phase`,1148 );1149 }1150 }1151 }1152 assert(gusts > 0, "no gust walls emerged across the corpus");1153});