clone

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

M13: tee-spur — split-double-sides as a spur terminal (T stem off a split junction, arms capped); exemption lifted, goldens regenerated

Dami committed c50ef0f
commit
c50ef0fe6d5588870304226d2d521084319d3f4b
parent
967a1572182338eefbb334f849536f3ddd85e6de
author
Dami <dami.etoile@gmail.com>
4 files changed, +109 -5
Msrc/gen/stubs.ts51+++++++++++++++++++++++++++++
Mtests/golden_tracks.json4+-
Mtests/track_test.ts53++++++++++++++++++++++++++++++
Mtools/coverage.ts6++--
M src/gen/stubs.ts +51 -0
@@ -170,6 +170,57 @@ export function planStubs(
170170 }171171 usedHosts.add(cand.placement);172172 173+ // TEE-SPUR (M13): the branch stems into a split-double-sides — a174+ // T-junction with NO through port (unusable on the ring, which kept175+ // it coverage-exempt until now) whose two arms dead-end in caps. A176+ // 3×2 footprint + 2 cap cells must be free; rarity is honest.177+ const teePlaced = cand.junction === "split" && rng.chance(0.3) &&178+ (() => {179+ const w = cand.dir;180+ const rT = ((1 - w + 4) % 4) as Dir;181+ const [s1x, s1z] = cand.branch[0];182+ const cellAt = (ex: number, ez: number): [number, number] => {183+ const [ox, oz] = rot(rT, ex, ez);184+ return [s1x + ox, s1z + oz];185+ };186+ const footprint: [number, number][] = [];187+ for (const ez of [0, 1]) {188+ for (const ex of [-1, 0, 1]) footprint.push(cellAt(ex, ez));189+ }190+ const capA = cellAt(2, 1); // arm exit toward mesh +x191+ const capB = cellAt(-2, 1);192+ const all = [...footprint, capA, capB];193+ if (all.some(([x, z]) => taken.has(key(x, z)))) return false;194+ const [mx, mz] = rot(rT, 0, 0.5);195+ plan.placements.push({196+ pieceId: "split-double-sides",197+ pos: [s1x + mx, 0, s1z + mz],198+ rotation: rT,199+ role: "track",200+ });201+ const dirOf = (dx: number, dz: number): Dir =>202+ dx === 1 ? 0 : dz === 1 ? 1 : dx === -1 ? 2 : 3;203+ for (const [capCell, ex] of [[capA, 1], [capB, -1]] as const) {204+ const [adx, adz] = rot(rT, ex, 0);205+ const armDir = dirOf(adx, adz);206+ plan.placements.push({207+ pieceId: rng.pick(["end-rounded", "end-square"]),208+ pos: [capCell[0], 0, capCell[1]],209+ rotation: ((1 - armDir + 4) % 4) as Dir,210+ role: "track",211+ });212+ }213+ for (const [x, z] of all) {214+ taken.add(key(x, z));215+ plan.cells.push(key(x, z));216+ }217+ return true;218+ })();219+ if (teePlaced) {220+ placed++;221+ continue;222+ }223+173224 const len = rng.chance(0.6) ? 2 : 1;174225 const rotation = ((1 - cand.dir + 4) % 4) as Dir;175226 const cap = rng.pick(CAPS);
M tests/golden_tracks.json +2 -2
M tests/track_test.ts +53 -0
@@ -203,6 +203,59 @@ Deno.test("per-branch diamond supports: the alt branch carries its own columns",
203203 assert(elevatedDiamonds > 0, "no elevated diamonds across the pinned seeds");204204 });205205 206+Deno.test("tee-spur: split-double-sides stems from a junction, arms capped", () => {207+ // M13: the tee splitter (no through port — was coverage-exempt) enters208+ // as a spur terminal. Rules per tee: its stem points back at a `split`209+ // junction host one cell behind the entry, and BOTH arm exits carry a210+ // flush end cap opening toward the junction.211+ const seeds = ["tee-84", "tee-227", "tee-236", "tee-250", "tee-275"];212+ const STEP: readonly (readonly [number, number])[] = [213+ [1, 0],214+ [0, 1],215+ [-1, 0],216+ [0, -1],217+ ];218+ const rot = (r: number, x: number, z: number): [number, number] =>219+ r === 0 ? [x, z] : r === 1 ? [z, -x] : r === 2 ? [-x, -z] : [-z, x];220+ let tees = 0;221+ for (const seed of seeds) {222+ const track = generateTrack(seed, { packs: [] }, { skipDecor: true });223+ for (const p of track.placements) {224+ if (p.pieceId !== "split-double-sides") continue;225+ tees++;226+ const rT = p.rotation;227+ const [mx, mz] = rot(rT, 0, 0.5);228+ const s1: [number, number] = [p.pos[0] - mx, p.pos[2] - mz];229+ const w = ((1 - rT + 4) % 4) as 0 | 1 | 2 | 3;230+ const host: [number, number] = [231+ s1[0] - STEP[w][0],232+ s1[1] - STEP[w][1],233+ ];234+ assert(235+ track.placements.some((q) =>236+ q.pieceId === "split" &&237+ Math.round(q.pos[0]) === host[0] &&238+ Math.round(q.pos[2]) === host[1]239+ ),240+ `${seed}: tee stem has no split junction at (${host})`,241+ );242+ for (const ex of [2, -2]) {243+ const [ox, oz] = rot(rT, ex, 1);244+ const capCell = [s1[0] + ox, s1[1] + oz];245+ assert(246+ track.placements.some((q) =>247+ (q.pieceId === "end-rounded" || q.pieceId === "end-square") &&248+ Math.round(q.pos[0]) === capCell[0] &&249+ Math.round(q.pos[2]) === capCell[1]250+ ),251+ `${seed}: tee arm at (${capCell}) is uncapped`,252+ );253+ }254+ }255+ }256+ assert(tees > 0, "no tee-spurs across the pinned seeds");257+});258+206259 Deno.test("determinism: same seed → deep-equal track", () => {207260 const a = generateTrack("determinism-check");208261 const b = generateTrack("determinism-check");
M tools/coverage.ts +3 -3