clone

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

Full-helix spiral dives in the void (promoted backlog)

Dami committed 81665b9
tryHelixDive planner clause: the loop-mode full-helix ensemble in the
endless line, duplicate-cell tolerant (block claimed once per cell over
full depth; plan.cells carries revisited coords at both bases). Rule
tests exempt within-window helix pairs; scenario 'helix dive' pins the
window contract + a physics ride of helix-scan-0. Split windows gained
the lateral centering spring (RailMod.center) after fuzz-0 surfaced a
diamond-merge wedge-slab seam wedge. Jump scenario re-pinned to
prop-scan-1. 84 tests green.
commit
81665b9b11a5cd1744675f4320cb8171856cc30b
parent
e76a6b4af1f27ae5fd8ebecde0146f310d4d2301
author
Dami <dami.etoile@gmail.com>
18 files changed, +305 -15
A.playwright-cli/console-2026-08-09T04-05-18-262Z.log1+
A.playwright-cli/console-2026-08-09T04-09-53-935Z.log1+
A.playwright-cli/console-2026-08-09T04-10-00-503Z.log12++++
A.playwright-cli/console-2026-08-09T04-19-43-114Z.log1+
A.playwright-cli/console-2026-08-09T04-19-57-577Z.log1+
A.playwright-cli/page-2026-08-09T04-05-18-533Z.yml5++
A.playwright-cli/page-2026-08-09T04-05-58-619Z.pngbin
A.playwright-cli/page-2026-08-09T04-09-54-031Z.yml5++
A.playwright-cli/page-2026-08-09T04-10-00-598Z.yml5++
A.playwright-cli/page-2026-08-09T04-19-43-204Z.yml5++
A.playwright-cli/page-2026-08-09T04-19-57-674Z.yml5++
A.playwright-cli/page-2026-08-09T04-20-03-598Z.pngbin
Mprogress.md23+++++++
Msrc/client/physics/rail.ts17++++-
Msrc/gen/endless/path.ts92+++++++++++++++++++++++++++++
Mtests/endless_test.ts20+++++-
Mtests/physics_test.ts33++++++++++-
Mtests/scenario_test.ts94+++++++++++++++++++++++++++++-
A .playwright-cli/console-2026-08-09T04-05-18-262Z.log +1 -0
A .playwright-cli/console-2026-08-09T04-09-53-935Z.log +1 -0
A .playwright-cli/console-2026-08-09T04-10-00-503Z.log +12 -0
A .playwright-cli/console-2026-08-09T04-19-43-114Z.log +1 -0
A .playwright-cli/console-2026-08-09T04-19-57-577Z.log +1 -0
A .playwright-cli/page-2026-08-09T04-05-18-533Z.yml +5 -0
A .playwright-cli/page-2026-08-09T04-05-58-619Z.png +0 -0

Binary file.

A .playwright-cli/page-2026-08-09T04-09-54-031Z.yml +5 -0
A .playwright-cli/page-2026-08-09T04-10-00-598Z.yml +5 -0
A .playwright-cli/page-2026-08-09T04-19-43-204Z.yml +5 -0
A .playwright-cli/page-2026-08-09T04-19-57-674Z.yml +5 -0
A .playwright-cli/page-2026-08-09T04-20-03-598Z.png +0 -0

Binary file.

M progress.md +23 -0
@@ -812,6 +812,29 @@ zones (icy needs a visible surface treatment first — zones must READ).
812812 813813 **DONE this round (post-M12, user-directed):**814814 815+- **FULL-HELIX SPIRAL DIVES IN THE VOID** (promoted backlog): new816+ planner clause tryHelixDive (path.ts, weight 0.5, one per segment) —817+ the loop-mode full-helix ensemble (HELIX_LOOP_SPECS, approach corner +818+ 360° spiral dropping 2.0 + underpass back through its own first cells)819+ dropped into the endless line. The duplicate-cell-tolerant path: the820+ generic step machinery can't express revisited cells (per-visit bands821+ at one cell self-collide), so the clause claims each footprint cell822+ ONCE over the whole block depth [−2, clearance] (nothing threads the823+ spiral block), and plan.cells simply carries the revisited coordinates824+ twice at their two bases (cell 0 = entry base, interiors = exit base:825+ step-at-start, exactly what realize expects of a reserved window).826+ Channel-collision rule tests gained a within-window exemption for827+ full-helix reservations (endless fuzz + assertStreamInvariants).828+ Scenario "helix dive" pins the window contract corpus-wide (moves ==829+ def moves, bases, revisits, one helix window, mesh placement) +830+ physics scenario rides helix-scan-0's dive cold. FOUND EN ROUTE831+ (fuzz-0 reshuffle): a diamond-merge wedge — marble parked on the merge832+ wedge-slab edge seam 0.68 off-center; fixed in the shared rail law833+ with RailMod.center on family "split" (the rail follows the branch834+ lane, so the boost-zone centering spring is correct along the whole835+ window). Jump-gap scenario re-pinned (det-seed → prop-scan-1; no seed836+ freeze, pins follow the stream). 84 tests green.837+815838 - **MARBLE RING** (user: a revolving set of peer marbles so the run is816839 constantly alive ahead and behind): src/client/peersCore.ts (pure817840 slot-lifecycle core, headless-tested) + peers.ts (render layer), wired
M src/client/physics/rail.ts +13 -4
@@ -22,6 +22,11 @@ import { assistForce, GRAVITY_Y, MARBLE_RESTITUTION } from "./engine.ts";
2222 export interface RailMod {2323 /** Scales the assist's target/safe speeds (and its authority). */2424 speed?: number;25+ /** Lateral centering spring toward the rail line (split windows: a26+ * marble still oscillating from the branch bend wedges on the merge27+ * wedge-slab edge — fuzz-0 parked at the mouth seam). Boost zones get28+ * this implicitly via speed > 1. */29+ center?: boolean;2530 /** Gravity multiplier while on the tile (chutes pull harder). */2631 grav?: number;2732 /** Ballistic flight (jump gaps): the assist is hands-off entirely. */
@@ -50,6 +55,10 @@ const TILE_MODS: Record<string, RailMod> = {
5055 helix: { speed: 1.35 },5156 slant: { grav: 1.2 },5257 ramp: { grav: 1.15 },58+ // The rail follows the branch lane through a diamond, so centering onto59+ // it is correct along the whole window — and it keeps merge arrivals off60+ // the wedge-slab edge seam.61+ split: { center: true },5362 };5463 5564 /** Planner zone kinds → rail modifiers. Zones outrank family character. */
@@ -193,10 +202,10 @@ export function stationForce(
193202 const f = assistForce(vel, t, mass, climbTy, curveAhead, mod?.speed ?? 1) ??194203 [0, 0, 0];195204 const out: [number, number, number] = [f[0], f[1], f[2]];196- // Lateral centering in boost zones (straight by construction): a197- // spring-damper toward the rail line kills wall-to-wall oscillation198- // within the approach so the marble launches (or sprints) true.199- if (mod?.speed && mod.speed > 1) {205+ // Lateral centering in boost zones (straight by construction) and split206+ // windows: a spring-damper toward the rail line kills wall-to-wall207+ // oscillation so the marble launches true / merges clean.208+ if ((mod?.speed && mod.speed > 1) || mod?.center) {200209 const r = rail.pts[idx];201210 const nl = Math.hypot(t[2], t[0]) || 1;202211 const ux = t[2] / nl;
M src/gen/endless/path.ts +92 -0
@@ -38,6 +38,7 @@ import {
3838 turnRight,3939 windowXform,4040 } from "../geom.ts";41+import { HELIX_LOOP_SPECS } from "../topology.ts";4142 import { CHANNEL_CLEARANCE, type OccView } from "./occupancy.ts";4243 4344 /** Planner dead-end — the caller retries with a fresh rng label on a clean
@@ -376,6 +377,7 @@ class SegmentBuilder {
376377 ["cornerDrop", 1.2],377378 ["jump", 0.8],378379 ["sprint", 0.9],380+ ["helixDive", 0.5],379381 ] as const,380382 );381383 const attempts: (() => boolean)[] = [];
@@ -424,6 +426,8 @@ class SegmentBuilder {
424426 return this.tryJump(rng);425427 case "sprint":426428 return this.trySprint(rng);429+ case "helixDive":430+ return this.tryHelixDive(rng);427431 }428432 for (const attempt of attempts) if (attempt()) return true;429433 return false;
@@ -656,6 +660,94 @@ class SegmentBuilder {
656660 return true;657661 }658662 663+ /**664+ * FULL-HELIX SPIRAL DIVE (promoted backlog): the loop-mode ensemble665+ * dropped into the void — a 360° spiral block whose entry and exit share666+ * one mid-face port edge; the line dives 2.0 and passes back UNDER its667+ * own approach (the underpass straights). The window's moves REVISIT its668+ * first cells, which the generic step machinery can't express (per-visit669+ * bands at one cell would self-collide) — so this clause is the670+ * duplicate-cell-tolerant path: the ensemble claims each footprint cell671+ * ONCE over the whole block depth [−2, clearance] (the spiral occupies672+ * its full column — nothing may thread through the block), and673+ * this.cells simply carries the revisited coordinates twice at their two674+ * bases. Realize matches the reserved def by moves (same as loop mode);675+ * the mesh provides both levels, and the centerline's spiral arc drops676+ * linearly in angle. One per segment — it's a set-piece, not a texture.677+ */678+ private hadHelixDive = false;679+ private tryHelixDive(rng: Rng): boolean {680+ if (this.hadHelixDive) return false;681+ const large = rng.chance(0.35);682+ const turn = rng.chance(0.5) ? "R" : "L";683+ const spec = HELIX_LOOP_SPECS.find(684+ (s) => s.large === large && s.turn === turn,685+ );686+ if (!spec) return false;687+ const def = this.registry.variants.find(688+ (v) => v.id === spec.id && !v.reversed,689+ );690+ if (!def) return false;691+692+ const { x, z, y, dir } = this.pose;693+ const xf = windowXform(x, z, dir, 0);694+ const block: [number, number][] = [];695+ const seen = new Set<string>();696+ for (const [fx, fz] of def.footprint) {697+ const w = applyXform(xf, [fx, 0, fz]);698+ const wx = Math.round(w[0]);699+ const wz = Math.round(w[2]);700+ const k = key(wx, wz);701+ if (seen.has(k)) continue;702+ seen.add(k);703+ if (!this.occ.isFree(wx, wz, y - 2, y + CHANNEL_CLEARANCE)) return false;704+ block.push([wx, wz]);705+ }706+707+ // Walk the ensemble's moves for cell bookkeeping and the exit pose.708+ // Interior cells carry the exit base (step-at-start: the whole −2709+ // lands after the first cell) — exactly what realize expects of a710+ // reserved window.711+ let cx = x;712+ let cz = z;713+ let d = dir;714+ const walked: OpenCell[] = [];715+ for (let j = 0; j < spec.moves.length; j++) {716+ const m = spec.moves[j] as "F" | "L" | "R";717+ const dirIn = d;718+ const dirOut = m === "F" ? d : m === "L" ? turnLeft(d) : turnRight(d);719+ walked.push({720+ x: cx,721+ z: cz,722+ dirIn,723+ dirOut,724+ move: m,725+ baseY: j === 0 ? y : y - 2,726+ });727+ d = dirOut;728+ cx += DIR_STEP[dirOut][0];729+ cz += DIR_STEP[dirOut][1];730+ }731+ const exit: HeadPose = { x: cx, z: cz, y: y - 2, dir: d };732+ if (!hasFlatContinuation(this.occ, exit, 3, seen)) return false;733+734+ const start = this.cells.length;735+ for (const [wx, wz] of block) {736+ this.occ.claim(wx, wz, y - 2, y + CHANNEL_CLEARANCE, this.ref);737+ }738+ this.cells.push(...walked);739+ this.sinceAnchor += spec.moves.length;740+ this.pose = exit;741+ this.reservations.push({742+ start,743+ len: spec.moves.length,744+ pieceIds: [spec.id],745+ reversed: false,746+ });747+ this.hadHelixDive = true;748+ return true;749+ }750+659751 /** Last resort when boxed in: steep 1.5 chute straight down and out. */660752 private tryEscape(rng: Rng): boolean {661753 for (const delta of ["1.5", "1", "0.5"]) {
M tests/endless_test.ts +16 -4
@@ -63,7 +63,7 @@ Deno.test("endless: 20-seed fuzz — continuity, clearance, anchors, memory", ()
6363 // guarantee holds among CO-LIVE segments (within KEEP_SEGMENTS). The6464 // line may legally reoccupy long-culled space — that geometry was6565 // despawned well behind the render window, so nothing visible overlaps.66- const channels = new Map<string, { y: number; seg: number }[]>();66+ const channels = new Map<string, { y: number; seg: number; idx: number }[]>();6767 6868 let prevEnd: readonly [number, number, number] | null = null;6969 let prevExit: { x: number; z: number } | null = null;
@@ -94,19 +94,31 @@ Deno.test("endless: 20-seed fuzz — continuity, clearance, anchors, memory", ()
9494 prevEnd = segPoint(lastCurve, 1);9595 prevExit = { x: seg.plan.exit.x, z: seg.plan.exit.z };9696 97- // -- vertical clearance: same cell + co-live → entries ≥ ~1 apart98- for (const c of seg.plan.cells) {97+ // -- vertical clearance: same cell + co-live → entries ≥ ~1 apart.98+ // Exemption: cells inside ONE reserved full-helix ensemble revisit99+ // their own coordinates by construction (the underpass rides back100+ // through the approach cells; the composite mesh provides both101+ // levels), so within-window pairs don't count as collisions.102+ const helixSpans = seg.plan.reservations103+ .filter((r) => /^helix-(large-)?(left|right)$/.test(r.pieceIds[0]))104+ .map((r) => [r.start, r.start + r.len] as const);105+ const spanOf = (idx: number) =>106+ helixSpans.findIndex(([a, b]) => idx >= a && idx < b);107+ for (const [ci, c] of seg.plan.cells.entries()) {99108 const k = `${c.x},${c.z}`;100109 const ys = channels.get(k) ?? [];101110 for (const e of ys) {102111 if (e.seg <= i - KEEP_SEGMENTS) continue; // culled — legal reuse112+ if (113+ e.seg === i && spanOf(ci) >= 0 && spanOf(e.idx) === spanOf(ci)114+ ) continue; // same full-helix ensemble103115 assert(104116 Math.abs(e.y - c.baseY) >= 0.98,105117 `seed ${n} seg ${i}: channels ${e.y} (seg ${e.seg}) and ` +106118 `${c.baseY} collide at ${k}`,107119 );108120 }109- ys.push({ y: c.baseY, seg: i });121+ ys.push({ y: c.baseY, seg: i, idx: ci });110122 channels.set(k, ys);111123 }112124
M tests/physics_test.ts +30 -3
@@ -185,8 +185,10 @@ Deno.test("physics survivability: assisted marble clears early segments", async
185185 186186 Deno.test("scenario jump-gap: cold entry launches, flies, and is caught (both kinds)", async () => {187187 module_ = await factory();188- // One pinned case per gap kind: det-seed segment 0 plans a PROPELLER189- // (2-cell gap at 0.35 gravity), jump-scan-2 segment 0 a plain JUMP.188+ // One pinned case per gap kind: prop-scan-1 early segments plan a189+ // PROPELLER (2-cell gap at 0.35 gravity), jump-scan-2 a plain JUMP.190+ // (Re-pinned after the helix-dive clause reshuffled every seed's draws191+ // — det-seed's early propeller moved; no seed freeze, so pins follow.)190192 // Locate the flight span on the shared rail, spawn the marble COLD191193 // (base speed 2.2) just before the boost approach, and require it to192194 // cross the gap and keep rolling. This is the envelope MINIMUM — the
@@ -195,7 +197,7 @@ Deno.test("scenario jump-gap: cold entry launches, flies, and is caught (both ki
195197 // landing cells absorb by construction; hot arrivals are exercised by196198 // the 7-seed survivability gate above.)197199 for (const { seed, kind } of [198- { seed: "det-seed", kind: "propeller" },200+ { seed: "prop-scan-1", kind: "propeller" },199201 { seed: "jump-scan-2", kind: "jump" },200202 ]) {201203 const w = new EndlessWorld(seed, catalog);
@@ -255,3 +257,28 @@ Deno.test("physics determinism: identical trajectory hash across runs", async ()
255257 assertEquals(a.hash, b.hash);256258 assertEquals(a.fell, b.fell);257259 });260+261+Deno.test("scenario helix-dive: marble rides the full spiral and the underpass", async () => {262+ module_ = await factory();263+ // helix-scan-0 segment 0 hosts a full-helix ensemble (pinned by the264+ // scenario corpus test). The marble must enter the approach corner,265+ // ride the 360° spiral down 2.0 (helix family mod: banked, ridden266+ // fast), come out through its own underpass, and keep rolling.267+ const w = new EndlessWorld("helix-scan-0", catalog);268+ const seg = w.get(0);269+ const r0 = seg.plan.reservations.find((r) =>270+ /^helix-(large-)?(left|right)$/.test(r.pieceIds[0])271+ );272+ assert(r0 !== undefined, "helix-scan-0 seg 0 lost its full helix");273+ const wi = seg.windowRanges.findIndex((wr) => wr.start === r0.start);274+ let endS = 0;275+ for (let k = 0; k <= wi; k++) endS += seg.path.segments[k].length;276+ const r = drive("helix-scan-0", 3, 60, endS + 8);277+ assert(!r.fell, "marble fell during the helix dive");278+ assert(279+ r.reachedS >= endS + 7,280+ `marble stalled at s=${r.reachedS.toFixed(1)} (helix ends ${281+ endS.toFixed(1)282+ })`,283+ );284+});
M tests/scenario_test.ts +90 -4
@@ -150,23 +150,34 @@ const STREAM_REGRESSIONS: StreamScenario[] = [
150150 },151151 ];152152 153-/** Shared co-live stream invariants (same rules as the endless fuzz). */153+/** Shared co-live stream invariants (same rules as the endless fuzz).154+ * Full-helix ensembles revisit their own cells by construction (the155+ * underpass) — within-window pairs are exempt from the collision rule,156+ * exactly as in the endless fuzz test. */154157 function assertStreamInvariants(seed: string, through: number): void {155158 const w = new EndlessWorld(seed, catalog);156- const channels = new Map<string, { y: number; seg: number }[]>();159+ const channels = new Map<string, { y: number; seg: number; idx: number }[]>();157160 for (let i = 0; i <= through; i++) {158161 const seg = w.get(i);159- for (const c of seg.plan.cells) {162+ const helixSpans = seg.plan.reservations163+ .filter((r) => /^helix-(large-)?(left|right)$/.test(r.pieceIds[0]))164+ .map((r) => [r.start, r.start + r.len] as const);165+ const spanOf = (idx: number) =>166+ helixSpans.findIndex(([a, b]) => idx >= a && idx < b);167+ for (const [ci, c] of seg.plan.cells.entries()) {160168 const k = `${c.x},${c.z}`;161169 const ys = channels.get(k) ?? [];162170 for (const e of ys) {163171 if (e.seg <= i - KEEP_SEGMENTS) continue;172+ if (e.seg === i && spanOf(ci) >= 0 && spanOf(e.idx) === spanOf(ci)) {173+ continue; // same full-helix ensemble174+ }164175 assert(165176 Math.abs(e.y - c.baseY) >= 0.98,166177 `${seed} seg ${i}: channel collision at ${k} (${e.y} vs ${c.baseY})`,167178 );168179 }169- ys.push({ y: c.baseY, seg: i });180+ ys.push({ y: c.baseY, seg: i, idx: ci });170181 channels.set(k, ys);171182 }172183 for (const crossing of seg.plan.crossings) {
@@ -600,3 +611,78 @@ Deno.test("island diamonds: branches live on island track with claimed flanks",
600611 }601612 assert(diamonds > 0, "no island diamonds emerged across the corpus");602613 });614+615+// ---------------------------------------------------------------------------616+// Full-helix spiral dives in the void (promoted backlog)617+// ---------------------------------------------------------------------------618+619+Deno.test("helix dive: full-helix ensembles in the void obey the window contract", () => {620+ // Rules, per committed full-helix reservation across a seed corpus:621+ // - the window's moves equal the reserved def's moves exactly (realize622+ // matches by moves — a drifted window would fall back to something623+ // else silently);624+ // - step-at-start: cell 0 carries the entry base, every interior cell625+ // the exit base (entry − 2), and the line continues at −2;626+ // - the ensemble REVISITS its first cells (the underpass) — that's the627+ // duplicate-cell-tolerant property this clause exists for;628+ // - the window realizes as ONE helix-family window (the composite def,629+ // not a fallback), and its placements survive to the client;630+ // - at most one full helix per segment (set-piece, not texture).631+ const registry = buildRegistry();632+ const isFull = (id: string) => /^helix-(large-)?(left|right)$/.test(id);633+ let seen = 0;634+ for (let n = 0; n < 8; n++) {635+ const seed = `helix-scan-${n}`;636+ const w = new EndlessWorld(seed, catalog);637+ for (let i = 0; i < 8; i++) {638+ const seg = w.get(i);639+ const fulls = seg.plan.reservations.filter((r) => isFull(r.pieceIds[0]));640+ assert(fulls.length <= 1, `${seed} seg ${i}: ${fulls.length} full helixes`);641+ for (const r of fulls) {642+ seen++;643+ const def = registry.variants.find(644+ (v) => v.id === r.pieceIds[0] && !v.reversed,645+ )!;646+ const cells = seg.plan.cells.slice(r.start, r.start + r.len);647+ assert(648+ cells.map((c) => c.move).join("") === def.moves,649+ `${seed} seg ${i}: window moves diverge from ${def.id}`,650+ );651+ const entryY = cells[0].baseY;652+ for (let j = 1; j < cells.length; j++) {653+ assert(654+ Math.abs(cells[j].baseY - (entryY - 2)) < 1e-9,655+ `${seed} seg ${i}: interior cell ${j} off the exit base`,656+ );657+ }658+ const after = seg.plan.cells[r.start + r.len];659+ if (after) {660+ assert(661+ Math.abs(after.baseY - (entryY - 2)) < 1e-9,662+ `${seed} seg ${i}: line does not continue at −2 after the dive`,663+ );664+ }665+ const coords = cells.map((c) => `${c.x},${c.z}`);666+ assert(667+ new Set(coords).size < coords.length,668+ `${seed} seg ${i}: no revisited cells — not a full helix?`,669+ );670+ // Realized as the composite: exactly one window covers the span,671+ // family helix.672+ const win = seg.windowRanges.findIndex(673+ (wr) => wr.start === r.start && wr.len === r.len,674+ );675+ assert(676+ win >= 0 && seg.pieceFamilies[win] === "helix",677+ `${seed} seg ${i}: reservation did not realize as one helix window`,678+ );679+ // The composite's mesh reaches the client (placement at the window).680+ assert(681+ seg.placements.some((p) => p.pieceId === def.meshId || (!def.meshId && p.pieceId === def.id)),682+ `${seed} seg ${i}: no placement for the helix composite`,683+ );684+ }685+ }686+ }687+ assert(seen >= 4, `too few full helixes across the corpus (${seen})`);688+});