clone

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

main
marbles / src / gen / validate.ts
validate.ts 2.1K
1/**2 * Post-generation invariant checks (dev builds and tests).3 */4import type { Track } from "../shared/types.ts";5import { segPoint } from "./geom.ts";67export function validateTrack(track: Track): string[] {8 const errors: string[] = [];9 const segs = track.loop.segments;10 if (segs.length === 0) return ["empty loop"];1112 // 1. Centerline continuity around the closed loop (C0, ε for the small13 // floor-height mismatches between piece families, e.g. tunnel 0.20 vs 0.25).14 for (let i = 0; i < segs.length; i++) {15 const cur = segs[i].curve;16 const nxt = segs[(i + 1) % segs.length].curve;17 const a = segPoint(cur[cur.length - 1], 1);18 const b = segPoint(nxt[0], 0);19 const horiz = Math.hypot(a[0] - b[0], a[2] - b[2]);20 const vert = Math.abs(a[1] - b[1]);21 if (horiz > 1e-6 || vert > 0.06) {22 errors.push(23 `loop gap after segment ${i}: horiz ${horiz.toExponential(2)}, vert ${24 vert.toFixed(3)25 }`,26 );27 }28 }2930 // 2. Elevation closure: net rise around the loop is zero (implied by 1's31 // vertical check chaining, but assert the sum explicitly for clarity).32 let rise = 0;33 for (const seg of segs) {34 const a = segPoint(seg.curve[0], 0);35 const b = segPoint(seg.curve[seg.curve.length - 1], 1);36 rise += b[1] - a[1];37 }38 if (Math.abs(rise) > 0.06 * segs.length) {39 errors.push(`elevation does not close: net rise ${rise}`);40 }4142 // 3. Track placements do not share cells (footprints already enforced by43 // realization; assert supports/decor don't sit inside track cells).44 const trackPos = new Set(45 track.placements.filter((p) => p.role === "track").map((p) =>46 `${Math.round(p.pos[0] * 2)},${Math.round(p.pos[2] * 2)}`47 ),48 );49 for (const p of track.placements) {50 if (p.role !== "decor") continue;51 const k = `${Math.round(p.pos[0] * 2)},${Math.round(p.pos[2] * 2)}`;52 if (trackPos.has(k)) {53 errors.push(54 `decor ${p.pieceId} at ${p.pos} overlaps a track piece origin`,55 );56 }57 }5859 // 4. Loop length sanity.60 if (!(track.loop.totalLength > 10)) {61 errors.push(`suspiciously short loop: ${track.loop.totalLength}`);62 }6364 return errors;65}