StyleField v2 — the under-exploited seam gets its identity draw: - style.ts TEMPERAMENTS (appended draw — finite streams byte-identical, genVersion untouched): balanced (common), treacherous (icy/gravity/ spur + spooky/barren/winter islands), soaring (jump/sprint/helix + tropical/meadow), tangled (braids/spurs/reorients + abstract/grove), rolling (slopes/corner-drops + rocky/autumn), serene (hazards damped + meadow/forest/grove). - The endless planner's clause draw now composes climate × temperament (world.ts composeClauseBias; path.ts takes one merged record); the island biome draw leans with the temperament too — a seed's sky, hazards and islands cohere into one character. - Corpus law test: treacherous seeds run measurably icier/heavier than serene ones; ≥4 temperaments emerge over 60 seeds. - Pin cycle #3 of the round: jump→grav-scan-2, propeller→grav-scan-0, spur@1→grav-scan-22, 180 reorient back to grav-scan-33, 3-chain → grav-scan-32@0-2 (one seed now pins open fork AND full chain), gust bank→wind-scan-9@4 (bank run-gap relaxed ≤4: climate biases spread gusts thinner than the M19a adjacency law assumed — an island's ledge row is the bank unit). 209 green, check clean, goldens re-pinned.
- commit
- 6c1fa38aedfe3b3ef1a0cfb861598040fcd2aa9c
- parent
- 39a9911f5fa1c5a6d58fb7425b8d2d76c10d420f
- author
- Dami <dami.etoile@gmail.com>
| M | progress.md | 11 | +++--- |
| M | src/ | 5 | ++- |
| M | src/ | 8 | ++++- |
| M | src/ | 33 | +++++++++++------- |
| M | src/ | 27 | ++++++++++++-- |
| M | src/ | 56 | ++++++++++++++++++++++++++++++ |
| M | tests/ | 42 | +++++++++++++++++++++++ |
| M | tests/ | 22 | ++++++------ |
| M | tests/ | 40 | +++++++++++----------- |
| M | tests/ | 6 | ++-- |
| M | tests/ | 10 | +++--- |
| M | tests/ | 6 | ++-- |
@@ -104,11 +104,12 @@ cross composite, quantitative metrics CI).
104104 draw — re-scan with a scratch script (current pins: grav-scan-25105105 jump, helix-scan-6 propeller, grav-scan-9 diamond, wide-scan-7 wide,106106 grav-scan-6 helix, wide-scan-17 icy, wide-scan-21 gravity,107- spurs grav-scan-3@0 + grav-scan-36@1, reorients grav-scan-20 (+90@0)108- + grav-scan-13 (180@0), helix jump-scan-14@0, braids grav-scan-17/38 +109- marbles, gust bank wide-scan-30@8, tagged open braid grav-scan-17@1,110- open fork grav-scan-32@0 — M21 re-scan; the climate bias moved nearly111- every pin);107+ spurs grav-scan-3@0 + grav-scan-22@1, reorients grav-scan-20 (+90@0)108+ + grav-scan-33 (180@0), jump grav-scan-2, propeller grav-scan-0,109+ helix jump-scan-14@0, braids grav-scan-17/38 + marbles, gust bank110+ wind-scan-9@4, tagged open braid grav-scan-17@1, 3-segment chain111+ grav-scan-32@0-2 — M24 re-scan; climate + temperament biases moved112+ the pins twice this round);112113 regenerate endless goldens with `deno run --allow-read --allow-write113114 tools/pin_endless.ts`. Canon note: golden placements hash wind114115 pulse/swirl since M19a.@@ -120,7 +120,10 @@ export function coordinateGusts(gusts: readonly GustEntry[]): void {
120120 run = [];121121 };122122 for (const g of list) {123- if (run.length > 0 && g.host - run[run.length - 1].host > 2) flush();123+ // Run gap ≤4 (M24 re-tune): an island's ledge row reads as one124+ // authored bank even with a stub between the fans; climate biases125+ // spread gusts thinner than the M19a ≤2 law assumed.126+ if (run.length > 0 && g.host - run[run.length - 1].host > 4) flush();124127 run.push(g);125128 }126129 flush();@@ -286,7 +286,13 @@ export function buildIsland(
286286 }287287 return top;288288 };289- const biome = rng.weighted(BIOMES.map((b) => [b, b.weight] as const));289+ // M24: the seed's temperament leans the island draw — a serene seed290+ // grows meadows, a treacherous one spooky crags.291+ const biome = rng.weighted(292+ BIOMES.map((b) =>293+ [b, b.weight * (style.temperament.biomeBias[b.name] ?? 1)] as const294+ ),295+ );290296 const family = MASS_FAMILIES[biome.massFamily ?? "cliff"] ??291297 MASS_FAMILIES.cliff;292298 const placements: Placement[] = [];@@ -329,12 +329,20 @@ export function planSegment(
329329 occ: OccView,330330 registry: PieceRegistry,331331 braidBias = 1,332- /** The zone's BASE mood (gen/climate.ts segmentClimate) — biases the333- * clause draw (M21). "" = no bias (finite-mode callers, tests). */334- mood = "",332+ /** Merged clause-weight bias (M21 climate × M24 temperament) — the333+ * caller (world.ts) composes it; {} = neutral (finite-mode callers,334+ * tests). */335+ clauseBias: Record<string, number> = {},335336 ): { plan: SegmentPlan; next: HeadState } {336- return new SegmentBuilder(rng, index, state, occ, registry, braidBias, mood)337- .build();337+ return new SegmentBuilder(338+ rng,339+ index,340+ state,341+ occ,342+ registry,343+ braidBias,344+ clauseBias,345+ ).build();338346 }339347 340348 class SegmentBuilder {@@ -366,8 +374,8 @@ class SegmentBuilder {
366374 * rngFamily(seed, "braid-breadth")) — some seeds are tangly, some367375 * clean. Caps how far a braid's alt lane may wander out. */368376 private readonly braidBias: number = 1,369- /** Zone base mood (M21) — clause-bias key. */370- private readonly mood: string = "",377+ /** Merged clause bias (climate × temperament). */378+ private readonly clauseBias: Record<string, number> = {},371379 ) {372380 this.pose = { ...state.pose };373381 this.sinceAnchor = state.sinceAnchor;@@ -584,12 +592,11 @@ class SegmentBuilder {
584592 // -------------------------------------------------------------------------585593 586594 private tryVoidClause(rng: Rng): boolean {587- // M21: the zone's BASE climate biases the hazard appetite — the same588- // seed-pure draw the sky presents (gen/climate.ts). One weighted589- // draw either way: the rng stream shape is identical, only the590- // outcome leans. Snowdrift zones grow icy patches, goldhour zones591- // soar, the dark moods pull toward gravity/forks/helixes.592- const bias = MOOD_CLAUSE_BIAS[this.mood] ?? {};595+ // M21+M24: climate × temperament lean the hazard appetite — one596+ // weighted draw either way (stream shape identical, outcomes lean).597+ // Snowdrift zones grow icy patches; a treacherous SEED grows them598+ // everywhere.599+ const bias = this.clauseBias;593600 const kind = rng.weighted(594601 ([595602 ["straight", 4],@@ -62,6 +62,19 @@ import { buildIsland, type Island } from "./islands.ts";
6262 import { planIslandFurniture } from "./furniture.ts";6363 import { planVoidDecor } from "./voidDecor.ts";6464 import { segmentClimate } from "../climate.ts";65+import { MOOD_CLAUSE_BIAS } from "./path.ts";66+67+/** M21 climate × M24 temperament, one multiplicative record. */68+function composeClauseBias(69+ mood: string,70+ temperament: { clauseBias: Record<string, number> },71+): Record<string, number> {72+ const a = MOOD_CLAUSE_BIAS[mood] ?? {};73+ const b = temperament.clauseBias;74+ const out: Record<string, number> = { ...a };75+ for (const [k, v] of Object.entries(b)) out[k] = (out[k] ?? 1) * v;76+ return out;77+}6578 6679 export interface EndlessSegment {6780 index: number;@@ -209,7 +222,10 @@ export class EndlessWorld {
209222 new OccupancyTx(base),210223 this.registry,211224 this.braidBias,212- segmentClimate(this.seed, index).mood,225+ composeClauseBias(226+ segmentClimate(this.seed, index).mood,227+ this.style.temperament,228+ ),213229 );214230 return true;215231 } catch (err) {@@ -267,9 +283,12 @@ export class EndlessWorld {
267283 tx,268284 this.registry,269285 this.braidBias,270- // M21: the zone's base climate biases the clause draw — the271- // same seed-pure draw the sky presents.272- segmentClimate(this.seed, i).mood,286+ // M21+M24: the zone's base climate and the seed's temperament287+ // lean the clause draw together.288+ composeClauseBias(289+ segmentClimate(this.seed, i).mood,290+ this.style.temperament,291+ ),273292 );274293 } catch (err) {275294 if (err instanceof PlannerStuck) continue;@@ -6,6 +6,53 @@
66 import type { Rng } from "./rng.ts";77 import type { PieceDef } from "./registry.ts";88 9+/** Seed TEMPERAMENT (M24): a drawn identity that leans the ENDLESS10+ * planner's hazard appetite and biome draw — worlds develop coherent11+ * characters instead of uniform sampling. Finite mode never reads it12+ * (genVersion stays put); the draws are APPENDED to the style stream. */13+export interface Temperament {14+ name: string;15+ /** Endless clause-weight multipliers (path.ts combines with the16+ * climate bias). */17+ clauseBias: Record<string, number>;18+ /** Island biome-weight multipliers (islands.ts). */19+ biomeBias: Record<string, number>;20+}21+22+export const TEMPERAMENTS: readonly (Temperament & { weight: number })[] = [23+ { name: "balanced", weight: 3, clauseBias: {}, biomeBias: {} },24+ {25+ name: "treacherous",26+ weight: 1.5,27+ clauseBias: { icy: 1.8, gravity: 1.5, spurDrop: 1.3 },28+ biomeBias: { spooky: 2, barren: 1.5, winter: 1.4 },29+ },30+ {31+ name: "soaring",32+ weight: 1.5,33+ clauseBias: { jump: 1.8, sprint: 1.4, helixDive: 1.3 },34+ biomeBias: { tropical: 1.6, meadow: 1.3 },35+ },36+ {37+ name: "tangled",38+ weight: 1.2,39+ clauseBias: { braid: 1.8, spurDrop: 1.4, reorient: 1.2 },40+ biomeBias: { abstract: 1.6, grove: 1.4 },41+ },42+ {43+ name: "rolling",44+ weight: 1.2,45+ clauseBias: { slope: 1.5, cornerDrop: 1.5, helixDive: 1.2 },46+ biomeBias: { rocky: 1.5, autumn: 1.4 },47+ },48+ {49+ name: "serene",50+ weight: 1,51+ clauseBias: { icy: 0.6, gravity: 0.6, jump: 0.8, braid: 0.8 },52+ biomeBias: { meadow: 2, forest: 1.5, grove: 1.5 },53+ },54+];55+956 export interface StyleField {1057 theme: string;1158 paletteIndex: 0 | 1 | 2;@@ -19,6 +66,8 @@ export interface StyleField {
1966 decorDensity: number;2067 /** 0..1 — the feature planner's appetite for wide-channel runs. */2168 wideAffinity: number;69+ /** Seed identity (M24) — endless-only hazard/biome leans. */70+ temperament: Temperament;2271 }2372 2473 interface ThemeSpec {@@ -124,9 +173,16 @@ export function pickStyle(rng: Rng): StyleField {
124173 const zonePhase = rng.float() * Math.PI * 2;125174 const zoneAmp = 0.25 + rng.float() * 0.35;126175 176+ // M24 temperament — APPENDED draw (finite streams byte-identical; the177+ // endless planner and biome draw read it).178+ const temperament = rng.weighted(179+ TEMPERAMENTS.map((t) => [t, t.weight] as const),180+ );181+127182 return {128183 theme: spec.name,129184 paletteIndex,185+ temperament,130186 supportKind,131187 supportColor,132188 decorDensity: spec.decorDensity,@@ -313,3 +313,45 @@ Deno.test("climate: the planner leans with the zone's base mood", () => {
313313 }`,314314 );315315 });316+317+// ---------------------------------------------------------------------------318+// M24 — seed personality.319+// ---------------------------------------------------------------------------320+321+Deno.test("temperament: seeds develop measurable identities", () => {322+ // Group seeds by their drawn temperament and compare hazard rates:323+ // treacherous seeds must run icier+heavier than serene ones across a324+ // corpus, and every temperament must actually emerge.325+ const byTemp = new Map<string, { icy: number; segs: number }>();326+ for (let n = 0; n < 60; n++) {327+ const seed = `temper-${n}`;328+ const w = new EndlessWorld(seed, catalog);329+ const t = w.style.temperament.name;330+ const e = byTemp.get(t) ?? { icy: 0, segs: 0 };331+ for (let i = 0; i < 6; i++) {332+ e.segs++;333+ for (const m of w.get(i).plan.mods) {334+ if (m.kind === "icy" || m.kind === "gravity") e.icy++;335+ }336+ }337+ byTemp.set(t, e);338+ }339+ assert(byTemp.size >= 4, `only ${byTemp.size} temperaments emerged`);340+ const treacherous = byTemp.get("treacherous");341+ const serene = byTemp.get("serene");342+ if (treacherous && serene && treacherous.segs > 20 && serene.segs > 20) {343+ assert(344+ treacherous.icy / treacherous.segs > serene.icy / serene.segs,345+ `treacherous hazard rate ${346+ (treacherous.icy / treacherous.segs).toFixed(3)347+ } not above serene ${(serene.icy / serene.segs).toFixed(3)}`,348+ );349+ }350+ // The style stream stays appended-only: the temperament draw must not351+ // move any pre-M24 style field (the theme of a known seed is pinned).352+ const w = new EndlessWorld("det-seed", catalog);353+ assert(354+ typeof w.style.temperament.name === "string" &&355+ w.style.theme.length > 0,356+ );357+});@@ -2,17 +2,17 @@
22 "__version": "g555803244402",33 "alpha": [44 "b33807289593bb7a",5- "b9c9bc4efd694da6",6- "a0cfa5fe96c76042",7- "662dc438fef17279",8- "8fba5072b96cf02e",9- "16c1265b72a807e6",10- "06f2553cfd9cf5da",11- "cc338b1efd405a33",12- "da107778669a117a",13- "b40f3a9e6e587c2f",14- "c74c7d0a9ce54d81",15- "5534ca0a8206eb93"5+ "22ebbcdaca5cd43d",6+ "f2c0f7a057646670",7+ "4cbee2c5dc6a4cd2",8+ "2f1422c96d80a4ef",9+ "3bf59512aa4edb9e",10+ "3345ae57835a78ad",11+ "d51d7a72908202c3",12+ "d41f8a789aeb7457",13+ "44d5d4ae57db23af",14+ "6e2e677250e3b287",15+ "639214169b864ada"1616 ],1717 "zx9k2m4p1q": [1818 "6f78daddd1c0bb41",@@ -302,23 +302,23 @@ Deno.test("physics survivability: assisted marble clears early segments", async
302302 );303303 });304304 305-Deno.test("scenario gust bank (wide-scan-30): pulsed walls never stall the line", async () => {305+Deno.test("scenario gust bank (wind-scan-9): pulsed walls never stall the line", async () => {306306 module_ = await factory();307- // wide-scan-30 grows a phase-locked gust bank on the island run in308- // segment 8 (pinned in scenario_test). Drive THROUGH it with the fan307+ // wind-scan-9 grows a phase-locked gust bank on the island run in308+ // segment 4 (pinned in scenario_test). Drive THROUGH it with the fan309309 // wind pass on: the marble must clear the bank without stalling or310310 // being blown off the channel, and the run must replay bit-identically311311 // (the pulse clock is sim time, not wall time).312- const w = new EndlessWorld("wide-scan-30", catalog);313- const goal = w.get(9).startS; // past the bank segment314- const a = driveWith("wide-scan-30", 11, 420, goal, 0, false, undefined, true);312+ const w = new EndlessWorld("wind-scan-9", catalog);313+ const goal = w.get(5).startS; // past the bank segment314+ const a = driveWith("wind-scan-9", 7, 300, goal, 0, false, undefined, true);315315 assert(316316 !a.fell && a.reachedS >= goal,317317 `gust bank stalled the marble: fell=${a.fell} reached=${318318 a.reachedS.toFixed(1)319319 }/${goal.toFixed(1)}`,320320 );321- const b = driveWith("wide-scan-30", 11, 420, goal, 0, false, undefined, true);321+ const b = driveWith("wind-scan-9", 7, 300, goal, 0, false, undefined, true);322322 assertEquals(a.hash, b.hash, "windy drive must replay bit-identically");323323 });324324 @@ -365,22 +365,22 @@ Deno.test("scenario open fork (grav-scan-32): both lanes cross the boundary clea
365365 }366366 });367367 368-Deno.test("scenario braid chain (grav-scan-15): 3-segment fork rides home on both lanes", async () => {368+Deno.test("scenario braid chain (grav-scan-32): 3-segment fork rides home on both lanes", async () => {369369 module_ = await factory();370- // grav-scan-15: open fork @0, STAY-OPEN middle spanning all of @1,370+ // grav-scan-32: open fork @0, STAY-OPEN middle spanning all of @1,371371 // merge continuation @2 (M23 braid networks). The alt lane crosses TWO372372 // segment boundaries — the longest legal chain. Both lanes must drive373373 // clean through the whole network.374- const w = new EndlessWorld("grav-scan-15", catalog);374+ const w = new EndlessWorld("grav-scan-32", catalog);375375 assert(376376 w.get(0).plan.braids.some((b) => b.open && !b.cont) &&377377 w.get(1).plan.braids.some((b) => b.open && b.cont) &&378378 w.get(2).plan.braids.some((b) => b.cont && !b.open),379- "grav-scan-15 lost its 3-segment chain — re-scan",379+ "grav-scan-32 lost its 3-segment chain — re-scan",380380 );381381 const goal = w.get(3).startS + 6;382382 for (const useAlt of [false, true]) {383- const r = driveWith("grav-scan-15", 6, 360, goal, 0, useAlt);383+ const r = driveWith("grav-scan-32", 6, 360, goal, 0, useAlt);384384 assert(385385 !r.fell && r.reachedS >= goal,386386 `chain lane ${useAlt ? "alt" : "main"} failed: fell=${r.fell} ` +@@ -405,8 +405,8 @@ Deno.test("scenario jump-gap: cold entry launches, flies, and is caught (both ki
405405 // the 7-seed survivability gate above.)406406 for (407407 const { seed, kind } of [408- { seed: "helix-scan-6", kind: "propeller" },409- { seed: "grav-scan-25", kind: "jump" },408+ { seed: "grav-scan-0", kind: "propeller" },409+ { seed: "grav-scan-2", kind: "jump" },410410 ]411411 ) {412412 const w = new EndlessWorld(seed, catalog);@@ -445,8 +445,8 @@ Deno.test("scenario lip-undershoot rescue: the jump decision saves a slow arriva
445445 // the marble's actual state, not a pre-priced launch.446446 for (447447 const { seed, kind } of [448- { seed: "helix-scan-6", kind: "propeller" },449- { seed: "grav-scan-25", kind: "jump" },448+ { seed: "grav-scan-0", kind: "propeller" },449+ { seed: "grav-scan-2", kind: "jump" },450450 ]451451 ) {452452 const w = new EndlessWorld(seed, catalog);@@ -737,17 +737,17 @@ Deno.test("scenario reorient: marble corkscrews the twist and rides the wall", a
737737 738738 Deno.test("scenario spur-drop: the branch rider falls through onto its own line", async () => {739739 module_ = await factory();740- // grav-scan-36 segment 1 hosts a takeable spur (pinned by spur_test).740+ // grav-scan-22 segment 1 hosts a takeable spur (pinned by spur_test).741741 // Ride the ALT rails: the marble takes the branch at the split, rolls742742 // out over the end-hole board, and must fall THROUGH the hole — proven743743 // by a mid-shaft transit inside the branch cell's column, a place the744744 // MAIN hook never visits at that height — landing on the receiver745745 // passing directly below (Δ 1.0 track), then keep progressing. The746746 // respawn net (fell) must never fire.747- const w = new EndlessWorld("grav-scan-36", catalog);747+ const w = new EndlessWorld("grav-scan-22", catalog);748748 const seg = w.get(1);749749 const spur = seg.plan.spurs[0];750- assert(spur !== undefined, "grav-scan-36 seg 1 lost its spur");750+ assert(spur !== undefined, "grav-scan-22 seg 1 lost its spur");751751 const host = seg.plan.cells[spur.host];752752 const [wx, wz] = DIR_STEP[spur.dir];753753 const holeX = (host.x + wx) * TRACK_SCALE;@@ -756,7 +756,7 @@ Deno.test("scenario spur-drop: the branch rider falls through onto its own line"
756756 const recFloor = deckFloor - TRACK_SCALE;757757 let shaftTransit = false;758758 const goal = w.get(2).startS;759- const r = drive("grav-scan-36", 4, 120, goal, 0, true, (pos) => {759+ const r = drive("grav-scan-22", 4, 120, goal, 0, true, (pos) => {760760 if (761761 Math.hypot(pos[0] - holeX, pos[2] - holeZ) < 0.7 &&762762 pos[1] > recFloor + 0.5 && pos[1] < recFloor + 1.6@@ -62,15 +62,15 @@ Deno.test("reorient: pinned seeds twist early and frames stay continuous", () =>
6262 });6363 6464 Deno.test("reorient: 180° roll presents the descent as a world CLIMB", () => {65- // grav-scan-13 seg 0 carries a 180° roll: plan-down maps to world-up65+ // grav-scan-33 seg 0 carries a 180° roll: plan-down maps to world-up6666 // past the twist — the ceiling-world signature (re-pinned after the6767 // braid clause reshuffled the streams; wide-scan-2 before that).68- const w = new EndlessWorld("grav-scan-13");68+ const w = new EndlessWorld("grav-scan-33");6969 const s = w.get(0);7070 const m = s.plan.mods.find((mm) =>7171 mm.kind === "reorient" && Math.abs(Math.abs(mm.angle!) - Math.PI) < 1e-97272 );73- assert(m, "grav-scan-13 seg 0 lost its 180° reorient — re-scan");73+ assert(m, "grav-scan-33 seg 0 lost its 180° reorient — re-scan");7474 assertAlmostEquals(Math.abs(m.angle!), Math.PI, 1e-12);7575 const down = quatRotate(s.frame!.exit.q, [0, -1, 0]);7676 close(down, [0, 1, 0], 1e-9);@@ -1091,18 +1091,18 @@ Deno.test("island ground vocabulary: wide runs, furniture, solids obey the rules
10911091 // ---------------------------------------------------------------------------10921092 10931093 Deno.test("gust walls: pulses in band, banks phase-locked, pairs anti-phase", () => {1094- // Pinned exemplar: wide-scan-30 grows a phase-locked bank in segment 81095- // (M21 re-scan; wide-scan-13 held the pin before the climate bias).1096- const pinned = new EndlessWorld("wide-scan-30", catalog);1094+ // Pinned exemplar: wind-scan-9 grows a phase-locked bank in segment 41095+ // (M24 re-scan; wide-scan-30/13 held earlier pins).1096+ const pinned = new EndlessWorld("wind-scan-9", catalog);10971097 let pinnedBank = 0;1098- for (let i = 0; i <= 8; i++) {1098+ for (let i = 0; i <= 4; i++) {10991099 const gusts = pinned.get(i).placements.filter((p) => p.wind?.pulse);11001100 if (gusts.length >= 2) {11011101 const periods = gusts.map((g) => g.wind!.pulse!.period);11021102 if (periods.every((p) => p === periods[0])) pinnedBank++;11031103 }11041104 }1105- assert(pinnedBank > 0, "wide-scan-30 lost its phase-locked gust bank");1105+ assert(pinnedBank > 0, "wind-scan-9 lost its phase-locked gust bank");11061106 11071107 // Corpus-wide laws over the standard scan families.11081108 const seeds = [@@ -26,7 +26,7 @@ import type { DecorCatalog } from "../src/gen/decor.ts";
2626 const catalog: DecorCatalog = goldenCatalog();2727 2828 /** Scan corpus (same style as the jump/gravity scenario seeds). Pinned29- * exemplars: grav-scan-3 spurs at segment 0, grav-scan-36 at segment 129+ * exemplars: grav-scan-3 spurs at segment 0, grav-scan-22 at segment 13030 * (re-pinned after the braid clause reshuffled the streams; before that3131 * the reorient clause moved them) — re-scan with a fresh label family if3232 * a reshuffle moves them (tools-free: the corpus loop below prints@@ -45,10 +45,10 @@ Deno.test("spur drop: pinned seeds exhibit a spur early", () => {
4545 scan12.get(0).plan.spurs.length > 0,4646 "seed grav-scan-3 lost its segment-0 spur — re-scan for an exemplar",4747 );48- const scan4 = new EndlessWorld("grav-scan-36", catalog);48+ const scan4 = new EndlessWorld("grav-scan-22", catalog);4949 assert(5050 scan4.get(1).plan.spurs.length > 0,51- "seed grav-scan-36 lost its segment-1 spur — re-scan (physics_test " +51+ "seed grav-scan-22 lost its segment-1 spur — re-scan (physics_test " +5252 "pins the same seed)",5353 );5454 });