// Pure, seeded world generation — no DB, no Date, no Math.random. The same seed always // yields the same world, which is what makes unit tests (and save regeneration) deterministic. export interface WorldSystem { name: string x: number // 0–100 coordinate space, rendered directly by NodeGraph y: number } export const START_X = 50 export const START_Y = 50 const START_RING = 6 // neighbours around the starting system on a new game const EXPAND_RING = 4 // new neighbours generated when jumping into an undiscovered system const MIN_SPACING = 16 // minimum distance between any two systems in the 0–100 space // mulberry32 — small fast seeded PRNG; plenty for name/position picking. export function mulberry32(seed: number): () => number { let a = seed >>> 0 return () => { a |= 0 a = (a + 0x6d2b79f5) | 0 let t = Math.imul(a ^ (a >>> 15), 1 | a) t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } const FIRST = [ 'KEPLER', 'VESPER', 'HOLLOW', 'CINDER', 'LANTERN', 'SABLE', 'ORPHEUS', 'MIRE', 'THALASSA', 'KUIPER', 'AURELLE', 'BRIMSTONE', 'EMBER', 'FROST', 'GALE', 'IRON', ] const SECOND = ['DRIFT', 'REACH', 'GATE', 'FIELD', 'VOID', 'DEEP', 'BEND', 'SHARD'] function title(word: string): string { return word.charAt(0) + word.slice(1).toLowerCase() } // Picks a name not in `taken` and adds it, so callers can't accidentally reuse one. export function makeName(rng: () => number, taken: Set): string { for (;;) { const name = `${title(FIRST[Math.floor(rng() * FIRST.length)])} ${title(SECOND[Math.floor(rng() * SECOND.length)])}` if (!taken.has(name)) { taken.add(name) return name } } } function clamp(v: number): number { return Math.min(96, Math.max(4, v)) } // Places `count` systems on a ring around (cx,cy), nudging each candidate until it is at // least MIN_SPACING away from every known position. After 12 tries it accepts the last // candidate anyway — ponytail: a slightly crowded node beats an infinite loop; with ≤6 // rings per region and spacing 16 in a 0–100 box this never triggers in practice. function placeRing( rng: () => number, cx: number, cy: number, count: number, takenNames: Set, takenPositions: WorldSystem[], ): WorldSystem[] { const placed: WorldSystem[] = [] for (let i = 0; i < count; i++) { const baseAngle = (i / count) * Math.PI * 2 + (rng() - 0.5) * 0.6 let x = cx let y = cy for (let attempt = 0; attempt < 12; attempt++) { const angle = baseAngle + attempt * 0.45 const radius = 26 + rng() * 8 x = clamp(cx + Math.cos(angle) * radius) y = clamp(cy + Math.sin(angle) * radius) if ([...takenPositions, ...placed].every((p) => Math.hypot(p.x - x, p.y - y) >= MIN_SPACING)) break } placed.push({ name: makeName(rng, takenNames), x, y }) } return placed } // Edges for a ring of n members around an anchor at index 0: anchor↔each member plus the // cycle connecting consecutive members (so every system has ≥2 routes and you can always // get back). Pairs are indices into [anchor, ...members]. function ringEdges(n: number): [number, number][] { const edges: [number, number][] = [] for (let i = 0; i < n; i++) { edges.push([0, i + 1]) edges.push([i + 1, ((i + 1) % n) + 1]) } return edges } export interface StartRegion { start: WorldSystem neighbors: WorldSystem[] edges: [number, number][] // indices into [start, ...neighbors] } // New game: the starting system at chart centre plus a ring of undiscovered neighbours. export function generateStartRegion(rng: () => number): StartRegion { const taken = new Set() const start: WorldSystem = { name: makeName(rng, taken), x: START_X, y: START_Y } const neighbors = placeRing(rng, START_X, START_Y, START_RING, taken, [start]) return { start, neighbors, edges: ringEdges(neighbors.length) } } export interface RingRegion { systems: WorldSystem[] edges: [number, number][] // indices into [anchor, ...systems] — index 0 is the anchor itself } // Jumping into an undiscovered system grows the chart: a fresh ring of (undiscovered) // neighbours around it. `takenNames`/`takenPositions` are every system already in the save. export function generateRing( rng: () => number, anchorX: number, anchorY: number, takenNames: Set, takenPositions: WorldSystem[], ): RingRegion { const systems = placeRing(rng, anchorX, anchorY, EXPAND_RING, takenNames, takenPositions) return { systems, edges: ringEdges(systems.length) } }