clone

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

main
marbles / tools / coverage.ts
coverage.ts 4.6K
1#!/usr/bin/env -S deno run --allow-read2/**3 * Coverage dashboard (M4): generate N seeds and report which Marble Kit4 * piece ids appear in placements, how often, and which are missing —5 * against the curated exemption list below. Exit 1 if a non-exempt piece6 * never appears.7 *8 * deno run --allow-read tools/coverage.ts [seeds=10000]9 */10import { generateTrack } from "../src/gen/generateTrack.ts";1112const N = Number(Deno.args[0] ?? 10000);1314/** Pieces that deliberately never appear, with the reason. */15const EXEMPT: Record<string, string> = {16 // The marble itself — the client picks one variant (marble-center-high).17 "marble-low": "marble variant (client uses marble-center-high)",18 "marble-high": "marble variant",19 "marble-center-low": "marble variant",20 "marble-center-butterfly-low": "marble variant",21 "marble-center-butterfly-high": "marble variant",22 // Tier-a verticals rise 0.125/cell — off every usable height grid (the23 // support stack and the elevation vocabulary work in 0.25/0.5 steps).24 "slant-a": "tier-a delta 0.125 off the height grid",25 "slant-solid-a": "tier-a",26 "slant-long-a": "tier-a",27 "slant-solid-long-a": "tier-a",28 "ramp-start-a": "tier-a",29 "ramp-start-solid-a": "tier-a",30 "ramp-end-a": "tier-a",31 "ramp-end-solid-a": "tier-a",32 "ramp-long-a": "tier-a",33 "ramp-long-solid-a": "tier-a",34 // Single-cell slants above the steepness caps (climb 0.5, descent 0.75).35 "slant-c": "1.0/cell — steeper than any cap",36 "slant-solid-c": "1.0/cell",37 "slant-d": "1.5/cell — steeper than any cap",38 "slant-solid-d": "1.5/cell",39 // Double-saddle cradle tops: 0.4 pitch matches no channel spacing we40 // compose (measured).41 "support-single-top-double": "double-saddle cradle, unmatched pitch",42 "support-two-top-double": "double-saddle cradle",43 "support-four-top-double": "double-saddle cradle",44 "support-color-single-top-double": "double-saddle cradle",45 "support-color-two-top-double": "double-saddle cradle",46 "support-color-four-top-double": "double-saddle cradle",47 // The tee-spur (M13) realizes the open form; the solid variant has no48 // ground-adjacency host in the spur macro (the T stands in open apron).49 "split-double-sides-solid": "tee splitter (solid form; spur is open)",50 // The diamond composite can't solid-swap its overlay half-pair.51 "split-double-solid": "diamond overlays don't solid-swap",52 // Solids are GROUND-ONLY (user rule: elevated track reads better open,53 // supports visible) — solids of pieces that only ever exist elevated are54 // structurally unreachable.55 "ramp-end-solid-b": "climb tail always elevated; solids ground-only",56 "ramp-end-solid-c": "climb tail always elevated",57 "ramp-end-solid-d": "descent-only tier, always elevated",58 "ramp-start-solid-d": "descent-only tier, always elevated",59 "ramp-long-solid-d": "1.5-chute is descent-only, always elevated",60 "slant-solid-long-d": "1.5-chute is descent-only, always elevated",61};6263const manifest = JSON.parse(64 await Deno.readTextFile(65 new URL("../assets/marble-kit.manifest.json", import.meta.url),66 ),67);68const allIds: string[] = (manifest.pieces ?? manifest).map(69 (p: { id: string }) => p.id,70);7172const catalog = {73 packs: [{74 slug: "nature-kit",75 ids: [76 "tree_a",77 "tree_dead",78 "rock_a",79 "stone_b",80 "grass_a",81 "plant_a",82 "flower_a",83 "mushroom_a",84 "stump_a",85 "log_a",86 "cliff_a",87 "fence_a",88 "crop_a",89 ],90 }],91};9293const counts = new Map<string, number>();94const t0 = performance.now();95for (let i = 0; i < N; i++) {96 const t = generateTrack(`coverage-${i}`, catalog);97 for (const p of t.placements) {98 if (p.pieceId.includes(":")) continue; // other packs99 counts.set(p.pieceId, (counts.get(p.pieceId) ?? 0) + 1);100 }101 if (i % 1000 === 999) {102 console.error(`… ${i + 1}/${N} (${Math.round(performance.now() - t0)}ms)`);103 }104}105106// The client always renders one marble.107counts.set("marble-center-high", N);108109const rows = allIds110 .map((id) => ({ id, n: counts.get(id) ?? 0 }))111 .sort((a, b) => a.n - b.n || a.id.localeCompare(b.id));112113let missing = 0;114console.log(`\n=== coverage over ${N} seeds ===`);115for (const { id, n } of rows) {116 if (n === 0 && EXEMPT[id]) {117 console.log(` EXEMPT ${id} (${EXEMPT[id]})`);118 } else if (n === 0) {119 console.log(` MISSING ${id}`);120 missing++;121 }122}123console.log(`\nrarest present pieces:`);124for (const { id, n } of rows.filter((r) => r.n > 0).slice(0, 15)) {125 console.log(` ${String(n).padStart(6)} ${id}`);126}127console.log(128 `\n${allIds.length} kit pieces: ${129 rows.filter((r) => r.n > 0).length130 } present, ${Object.keys(EXEMPT).length} exempt, ${missing} missing`,131);132if (missing > 0) Deno.exit(1);