clone

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

main
marbles / tests / void_decor_test.ts
void_decor_test.ts 6.4K
1/**2 * Floating void decor (M18b): deterministic, sparse, clear of the line,3 * and STRUCTURALLY collision-free — the proxy path never sees the4 * voidDecor field.5 */6import { assert, assertEquals } from "@std/assert";7import { EndlessWorld } from "../src/gen/endless/world.ts";8import {9 bumperProxies,10 islandProxies,11 segmentProxies,12} from "../src/gen/endless/proxies.ts";13import {14 VOID_CLEARANCE,15 VOID_MAX_PIECES,16} from "../src/gen/endless/voidDecor.ts";17import { segLength, segPoint } from "../src/gen/geom.ts";18import type { DecorCatalog } from "../src/gen/decor.ts";19import { goldenCatalog } from "../tools/pin_endless.ts";2021const catalog: DecorCatalog = goldenCatalog();2223Deno.test("void decor: identical across worlds of the same seed", () => {24 const a = new EndlessWorld("void-det", catalog);25 const b = new EndlessWorld("void-det", catalog);26 for (let i = 0; i < 12; i++) {27 assertEquals(a.get(i).voidDecor, b.get(i).voidDecor, `segment ${i}`);28 }29});3031Deno.test("void decor: sparse, resolvable, floating, clear of the line", () => {32 const known = new Map(catalog.packs.map((p) => [p.slug, new Set(p.ids)]));33 let pieces = 0;34 let drifted = 0;35 const seenPacks = new Set<string>();36 for (let n = 0; n < 30; n++) {37 const w = new EndlessWorld(`void-${n}`, catalog);38 for (let i = 0; i < 8; i++) {39 const seg = w.get(i);40 assert(41 seg.voidDecor.length <= VOID_MAX_PIECES,42 `void-${n} seg ${i}: ${seg.voidDecor.length} pieces over budget`,43 );44 // Path samples for the clearance re-check (main + alt).45 const samples: (readonly [number, number, number] | number[])[] = [];46 for (47 const path of [seg.path, ...(seg.altPath ? [seg.altPath] : [])]48 ) {49 for (const s of path.segments) {50 for (const c of s.curve) {51 const steps = Math.max(1, Math.round(segLength(c) / 2));52 for (let k = 0; k <= steps; k++) samples.push(segPoint(c, k / steps));53 }54 }55 }56 for (const p of seg.voidDecor) {57 pieces++;58 assertEquals(p.role, "decor", "void decor must be decor-role");59 assert(p.float, `void-${n} seg ${i}: piece without float params`);60 assert(61 p.float!.amp > 0 && p.float!.period > 0,62 "float params must be positive",63 );64 if (p.float!.drift) drifted++;65 const colon = p.pieceId.indexOf(":");66 assert(colon > 0, `unprefixed void piece ${p.pieceId}`);67 seenPacks.add(p.pieceId.slice(0, colon));68 assert(69 known.get(p.pieceId.slice(0, colon))?.has(p.pieceId.slice(colon + 1)),70 `void-${n} seg ${i}: unresolvable ${p.pieceId}`,71 );72 for (const s of samples) {73 const d = Math.hypot(74 p.pos[0] - s[0],75 p.pos[1] - s[1],76 p.pos[2] - s[2],77 );78 assert(79 d >= VOID_CLEARANCE - 1e-9,80 `void-${n} seg ${i}: ${p.pieceId} ${d.toFixed(2)}u from the line`,81 );82 }83 }84 }85 }86 // Distribution law: sparse but alive — and multi-pack across a corpus.87 assert(pieces > 50, `only ${pieces} void pieces across the corpus`);88 assert(pieces < 30 * 8 * VOID_MAX_PIECES * 0.5, "void decor reads as crowd");89 assert(drifted > 0, "no drifting far-band pieces emerged");90 assert(seenPacks.size >= 2, `only packs ${[...seenPacks]} emerged`);91});9293Deno.test("void decor: proxies are blind to it (non-collision contract)", () => {94 const w = new EndlessWorld("void-det", catalog);95 for (let i = 0; i < 6; i++) {96 const seg = w.get(i);97 if (seg.voidDecor.length === 0) continue;98 const withVoid = [99 ...segmentProxies(seg),100 ...seg.islands.flatMap((isle) => islandProxies(isle.placements)),101 ...bumperProxies(seg.placements),102 ];103 const stripped = { ...seg, voidDecor: [] };104 const without = [105 ...segmentProxies(stripped),106 ...stripped.islands.flatMap((isle) => islandProxies(isle.placements)),107 ...bumperProxies(stripped.placements),108 ];109 assertEquals(withVoid.length, without.length, `segment ${i}`);110 }111});112113Deno.test("void decor: events are rare and never consecutive", () => {114 // The quiet-after rule: two far-band events never land on back-to-back115 // segments. Detect events structurally (space-kit craft/meteor far116 // clusters are the event vocabulary's signature pieces).117 const EVENT_IDS = /^space-kit:(meteor$|meteor_detailed$|craft_|satelliteDish)/;118 let events = 0;119 let segments = 0;120 for (let n = 0; n < 20; n++) {121 const w = new EndlessWorld(`void-ev-${n}`, catalog);122 let prev = false;123 for (let i = 0; i < 10; i++) {124 segments++;125 const has = w.get(i).voidDecor.some((p) => EVENT_IDS.test(p.pieceId));126 if (has) {127 events++;128 assert(!prev, `void-ev-${n}: events on consecutive segments ${i - 1},${i}`);129 }130 prev = has;131 }132 }133 assert(events > 0, "no void events across the corpus");134 assert(events / segments < 0.2, `events too dense: ${events}/${segments}`);135});136137Deno.test("void decor: no floating ground props; shards bob as one body", () => {138 // User correction: bushes/rocks floating in air read wrong. The only139 // nature-kit pieces allowed in the void are shard components (cliff140 // courses + the grass cap); every grass cap must ride a cliff course141 // at the same (x,z) sharing the SAME float params.142 const SHARD_OK = /^nature-kit:(cliff_block(Quarter|Half)?_(rock|stone)|ground_grass)$/;143 let shards = 0;144 for (let n = 0; n < 20; n++) {145 const w = new EndlessWorld(`void-${n}`, catalog);146 for (let i = 0; i < 8; i++) {147 const vd = w.get(i).voidDecor;148 for (const p of vd) {149 if (!p.pieceId.startsWith("nature-kit:")) continue;150 assert(151 SHARD_OK.test(p.pieceId),152 `void-${n} seg ${i}: floating ground prop ${p.pieceId}`,153 );154 if (p.pieceId === "nature-kit:ground_grass") {155 shards++;156 const under = vd.find((q) =>157 q !== p && q.pieceId.includes("cliff_block") &&158 Math.abs(q.pos[0] - p.pos[0]) < 1e-9 &&159 Math.abs(q.pos[2] - p.pos[2]) < 1e-9160 );161 assert(under, `void-${n} seg ${i}: capless grass in the void`);162 assertEquals(under!.float, p.float, "shard pieces must share float");163 assertEquals(under!.scale, p.scale, "shard pieces must share scale");164 }165 }166 }167 }168 assert(shards > 0, "no island shards emerged across the corpus");169});