clone

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

main
marbles / src / gen / supports.ts
supports.ts 6.6K
1/**2 * Pillar vocabulary (USER rule: pillars are PIECES of different widths,3 * heights and patterns — compose them relationally, don't repeat one4 * column). Dynamic families over one measured law, shared by finite5 * assembly, endless viaducts, and takeable spur decks.6 *7 * Measured kit facts (2026-08-09): every support segment is 0.5 tall8 * (tops 0.7 with the cradle at 0.5); footprints (w×d):9 * four bottom 1.0×1.0 middle 0.9×0.9 (4-leg tower)10 * two bottom 1.0×0.3 middle 0.9×0.2 (flat 2-leg frame)11 * single bottom 0.6×0.6 middle 0.4×0.4 (slender post)12 *13 * RELATIONAL LAW — containment upward: a piece may stand on the piece14 * below only if its footprint fits inside it (ε 0.05). four carries15 * everything, two carries two, single carries single; single NEVER16 * stands on two (a 0.4-deep post on a 0.2-deep frame floats). `bottom`17 * segments are feet at ground contact AND transition collars mid-stack18 * — a collar only appears where its skirt obeys the same containment,19 * otherwise the kind change is a plain middle.20 *21 * Patterns are families over that law, drawn deterministically per22 * stack (position hash — the structural greedy stays rng-free, the23 * standing user rule): uniform in the seed's style kind, uniform in an24 * off kind, or tapered (a wide `four` base run, then a slender upper25 * run with a collar). New kinds = new table rows; the law is data.26 */27import type { Dir, Placement } from "../shared/types.ts";2829export type SupportKind = "single" | "two" | "four";3031interface Footprint {32 w: number;33 d: number;34}3536const MIDDLE_FOOT: Record<SupportKind, Footprint> = {37 four: { w: 0.9, d: 0.9 },38 two: { w: 0.9, d: 0.2 },39 single: { w: 0.4, d: 0.4 },40};41const BOTTOM_FOOT: Record<SupportKind, Footprint> = {42 four: { w: 1.0, d: 1.0 },43 two: { w: 1.0, d: 0.3 },44 single: { w: 0.6, d: 0.6 },45};4647/** Containment with authoring tolerance. */48export function fitsOn(above: Footprint, below: Footprint): boolean {49 return above.w <= below.w + 0.05 && above.d <= below.d + 0.05;50}5152/** May a run of `upper` begin on top of a `lower` middle segment? */53export function mayStandOn(upper: SupportKind, lower: SupportKind): boolean {54 return fitsOn(MIDDLE_FOOT[upper], MIDDLE_FOOT[lower]);55}5657/** Deterministic per-stack hash stream (no rng — position is the seed,58 * matching the tunnel-variant / island-jitter recipe). */59function stackHash(x: number, z: number, base: number): () => number {60 let h = Math.imul(Math.round(x * 8) | 0, 374761393) ^61 Math.imul(Math.round(z * 8) | 0, 668265263) ^62 Math.imul(Math.round(base * 8) | 0, 1274126177);63 return () => {64 h = Math.imul(h ^ (h >>> 13), 1103515245) + 12345;65 return ((h ^ (h >>> 16)) >>> 0) / 4294967296;66 };67}6869/** Shaft plan: the kind occupying each 0.5 slot, bottom → top.70 *71 * `extended` (M19c) admits two more patterns — LOOP MODE ONLY: the flag72 * is passed by finite-mode call sites (assemble !open, takeable). The73 * endless viaduct/spur stacks share this hash space, so an ungated74 * pattern change would shift endless goldens from a loop milestone; the75 * endless golden test is the tripwire. Both patterns obey the76 * containment law (narrower always ON wider — four never stands on the77 * thin two frame). */78export function planStack(79 bias: SupportKind,80 slots: number,81 draw: () => number,82 extended = false,83): SupportKind[] {84 if (slots <= 0) return [];85 const r = draw();86 if (extended && slots >= 4 && r < 0.18) {87 // Three-band taper: four → two → single, wide plinth to needle.88 const a = Math.max(1, Math.round(slots * (0.25 + draw() * 0.2)));89 const b = Math.max(1, Math.round((slots - a) * (0.4 + draw() * 0.2)));90 return [91 ...Array<SupportKind>(a).fill("four"),92 ...Array<SupportKind>(Math.min(b, slots - a - 1)).fill("two"),93 ...Array<SupportKind>(Math.max(1, slots - a - Math.min(b, slots - a - 1)))94 .fill("single"),95 ];96 }97 if (extended && slots >= 2 && r >= 0.18 && r < 0.34) {98 // Pedestal: one wide four course, then a slender column.99 const upper: SupportKind = draw() < 0.5 ? "two" : "single";100 return ["four", ...Array<SupportKind>(slots - 1).fill(upper)];101 }102 // Tapered tower: a wide `four` base run carrying a slender upper run.103 if (slots >= 3 && r < 0.3) {104 const upper: SupportKind = bias === "four"105 ? (draw() < 0.5 ? "two" : "single")106 : bias;107 const lowerLen = Math.max(108 1,109 Math.min(slots - 1, Math.round(slots * (0.35 + draw() * 0.3))),110 );111 return [112 ...Array<SupportKind>(lowerLen).fill("four"),113 ...Array<SupportKind>(slots - lowerLen).fill(upper),114 ];115 }116 // Uniform in an off kind — variety between stacks of one stretch.117 if (r < 0.5) {118 const others: SupportKind[] = (["four", "two", "single"] as const)119 .filter((k) => k !== bias);120 return Array(slots).fill(others[draw() < 0.5 ? 0 : 1]);121 }122 // Uniform in the seed's style kind (the dominant silhouette).123 return Array(slots).fill(bias);124}125126/**127 * Compose one pillar into `placements`. `height` is the channel-base128 * height above `baseAt` (multiples of 0.5); cradle stacks put the129 * 0.7-tall top piece at height − 0.5 (its cradle lands exactly at the130 * channel base), plain columns end flush. The top piece takes the131 * uppermost shaft's kind so the arms grow from the right silhouette.132 */133export function emitSupportStack(134 placements: Placement[],135 prefix: string,136 bias: SupportKind,137 x: number,138 z: number,139 height: number,140 dir: Dir,141 cradle: boolean,142 baseAt = 0,143 /** M19c extended pattern pool — loop-mode call sites only (see144 * planStack: the endless golden test is the gate's tripwire). */145 extended = false,146) {147 const rotation = ((1 - dir + 4) % 4) as Dir;148 const draw = stackHash(x, z, baseAt);149 const slots = Math.max(0, Math.round((height - (cradle ? 0.5 : 0)) / 0.5));150 const plan = planStack(bias, slots, draw, extended);151 const piece = (kind: SupportKind, segment: string, y: number) => {152 placements.push({153 pieceId: `${prefix}-${kind}-${segment}`,154 pos: [x, y + baseAt, z],155 rotation,156 role: "support",157 });158 };159 for (let i = 0; i < plan.length; i++) {160 const kind = plan[i];161 const below = i > 0 ? plan[i - 1] : null;162 // Feet at contact; collars at kind changes when the skirt fits the163 // shaft below (the containment law) — else a plain middle.164 const collar = below !== null && below !== kind &&165 fitsOn(BOTTOM_FOOT[kind], MIDDLE_FOOT[below]);166 piece(167 kind,168 below === null ? "bottom" : collar ? "bottom" : "middle",169 i * 0.5,170 );171 }172 if (cradle && height >= 0.5 - 1e-9) {173 const topKind = plan.length > 0 ? plan[plan.length - 1] : bias;174 piece(topKind, "top", height - 0.5);175 }176}