Files
probability/src/sim/galaxy.js
T

159 lines
5.9 KiB
JavaScript

import { makeRng, hash32 } from './rng.js';
const SYSTEM_NAMES = [
'Arcadia', 'Boreas', 'Cygni', 'Drakon', 'Eridu', 'Fenrir', 'Gorgon', 'Helix',
'Icarus', 'Juno', 'Kepler', 'Lyra', 'Mira', 'Nyx', 'Orion', 'Pavo',
'Quasar', 'Rigel', 'Sirius', 'Talos', 'Umbra', 'Vega', 'Wraith', 'Xandar',
'Yara', 'Zenith', 'Altair', 'Bellatrix', 'Castor', 'Deneb',
];
const PLANET_SUFFIX = ['Prime', 'II', 'III'];
const PROFILES = [
{ kind: 'agri', produces: { food: 10, meds: 3 }, consumes: { fuel: 4, tech: 3 } },
{ kind: 'mining', produces: { ore: 12, fuel: 6 }, consumes: { food: 6, meds: 2 } },
{ kind: 'industrial', produces: { tech: 6 }, consumes: { ore: 8, fuel: 5, food: 5 } },
{ kind: 'refinery', produces: { fuel: 10 }, consumes: { ore: 6, food: 4 } },
{ kind: 'resort', produces: { luxury: 5 }, consumes: { food: 7, meds: 4, fuel: 3 } },
];
const ALL_GOODS = ['ore', 'food', 'fuel', 'tech', 'meds', 'luxury'];
export function otherEnd(lane, systemId) {
return lane.a === systemId ? lane.b : lane.a;
}
export function generateGalaxy(seed, { systemCount = 25 } = {}) {
const rng = makeRng(hash32('galaxy:' + seed));
// 1. Place systems with minimum spacing (relaxes if placement gets tight).
const systems = [];
let minDist = 95, attempts = 0;
while (systems.length < systemCount) {
const x = 60 + rng() * 880, y = 50 + rng() * 600;
if (systems.every((s) => Math.hypot(s.x - x, s.y - y) > minDist)) {
systems.push({ id: systems.length, name: SYSTEM_NAMES[systems.length % SYSTEM_NAMES.length], x, y, planets: [], lanes: [] });
}
if (++attempts > 2000) { minDist *= 0.9; attempts = 0; }
}
// 2. Lanes: each system to its 2 nearest neighbors, deduplicated.
const laneKeys = new Set();
const key = (a, b) => (a < b ? `${a}-${b}` : `${b}-${a}`);
for (const s of systems) {
const near = systems
.filter((o) => o.id !== s.id)
.sort((p, q) => Math.hypot(p.x - s.x, p.y - s.y) - Math.hypot(q.x - s.x, q.y - s.y));
laneKeys.add(key(s.id, near[0].id));
laneKeys.add(key(s.id, near[1].id));
}
// 3. Connectivity repair: link closest pair across components until one component.
const parent = systems.map((_, i) => i);
const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
const union = (a, b) => { parent[find(a)] = find(b); };
for (const k of laneKeys) { const [a, b] = k.split('-').map(Number); union(a, b); }
for (;;) {
const roots = new Set(systems.map((s) => find(s.id)));
if (roots.size === 1) break;
let best = null;
for (const s of systems) for (const o of systems) {
if (find(s.id) === find(o.id)) continue;
const d = Math.hypot(s.x - o.x, s.y - o.y);
if (!best || d < best.d) best = { a: s.id, b: o.id, d };
}
laneKeys.add(key(best.a, best.b));
union(best.a, best.b);
}
// 4. Materialize lanes with travel time and hazard.
const lanes = [...laneKeys].sort().map((k, i) => {
const [a, b] = k.split('-').map(Number);
const dist = Math.hypot(systems[a].x - systems[b].x, systems[a].y - systems[b].y);
const days = Math.min(4, Math.max(1, Math.round(dist / 80)));
const hazard = rng() < 0.2 ? +(0.3 + rng() * 0.4).toFixed(3) : +(rng() * 0.15).toFixed(3);
return { id: i, a, b, days, hazard };
});
for (const lane of lanes) {
systems[lane.a].lanes.push(lane.id);
systems[lane.b].lanes.push(lane.id);
}
// 5. Planets with economy profiles and a render orbit.
const planets = [];
for (const s of systems) {
const n = 1 + Math.floor(rng() * 3);
for (let i = 0; i < n; i++) {
const profile = PROFILES[Math.floor(rng() * PROFILES.length)];
const stock = {};
for (const g of ALL_GOODS) {
if (profile.produces[g]) stock[g] = Math.floor(150 + rng() * 150);
else if (profile.consumes[g]) stock[g] = Math.floor(40 + rng() * 80);
else stock[g] = Math.floor(80 + rng() * 70);
}
const planet = {
id: planets.length, systemId: s.id,
name: `${s.name} ${PLANET_SUFFIX[i]}`, kind: profile.kind,
produces: { ...profile.produces }, consumes: { ...profile.consumes },
stock, stockTime: 0,
orbit: { r: 90 + i * 55, a: rng() * Math.PI * 2 },
};
planets.push(planet);
s.planets.push(planet.id);
}
}
return { systems, lanes, planets };
}
// All-pairs hop counts (BFS per system). hops[a][b] = lanes to cross.
export function hopMatrix(galaxy) {
const n = galaxy.systems.length;
const m = Array.from({ length: n }, () => Array(n).fill(Infinity));
for (let src = 0; src < n; src++) {
m[src][src] = 0;
const queue = [src];
while (queue.length) {
const cur = queue.shift();
for (const laneId of galaxy.systems[cur].lanes) {
const next = otherEnd(galaxy.lanes[laneId], cur);
if (m[src][next] === Infinity) {
m[src][next] = m[src][cur] + 1;
queue.push(next);
}
}
}
}
return m;
}
// Dijkstra by travel days. Returns { lanes: [laneId...], days } or null.
export function findRoute(galaxy, fromSys, toSys) {
if (fromSys === toSys) return { lanes: [], days: 0 };
const dist = new Map([[fromSys, 0]]);
const prevLane = new Map();
const visited = new Set();
for (;;) {
let cur = null, best = Infinity;
for (const [s, d] of dist) if (!visited.has(s) && d < best) { best = d; cur = s; }
if (cur === null) return null;
if (cur === toSys) break;
visited.add(cur);
for (const laneId of galaxy.systems[cur].lanes) {
const lane = galaxy.lanes[laneId];
const next = otherEnd(lane, cur);
const nd = best + lane.days;
if (nd < (dist.get(next) ?? Infinity)) {
dist.set(next, nd);
prevLane.set(next, laneId);
}
}
}
const laneIds = [];
let s = toSys;
while (s !== fromSys) {
const laneId = prevLane.get(s);
laneIds.unshift(laneId);
s = otherEnd(galaxy.lanes[laneId], s);
}
return { lanes: laneIds, days: dist.get(toSys) };
}