135 lines
5.3 KiB
TypeScript
135 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { bodyRng, explorationRng, generatePlanets, generateRing, generateStartRegion, makeName, mulberry32, rollExploration } from './world'
|
||
|
||
describe('generateStartRegion', () => {
|
||
const region = generateStartRegion(mulberry32(42))
|
||
|
||
it('produces a starting system plus six neighbours', () => {
|
||
expect(region.neighbors).toHaveLength(6)
|
||
expect(region.start.x).toBe(50)
|
||
expect(region.start.y).toBe(50)
|
||
})
|
||
|
||
it('gives every system a unique name and in-bounds coordinates', () => {
|
||
const all = [region.start, ...region.neighbors]
|
||
const names = all.map((s) => s.name)
|
||
expect(new Set(names).size).toBe(names.length)
|
||
for (const s of all) {
|
||
expect(s.x).toBeGreaterThanOrEqual(0)
|
||
expect(s.x).toBeLessThanOrEqual(100)
|
||
expect(s.y).toBeGreaterThanOrEqual(0)
|
||
expect(s.y).toBeLessThanOrEqual(100)
|
||
}
|
||
})
|
||
|
||
it('connects the start to every neighbour and closes the ring cycle', () => {
|
||
const n = region.neighbors.length
|
||
// anchor↔each member (n edges) + ring cycle (n edges)
|
||
expect(region.edges).toHaveLength(n * 2)
|
||
for (let i = 0; i < n; i++) {
|
||
expect(region.edges).toContainEqual([0, i + 1])
|
||
expect(region.edges).toContainEqual([i + 1, ((i + 1) % n) + 1])
|
||
}
|
||
// every edge endpoint is a valid index into [start, ...neighbors]
|
||
for (const [a, b] of region.edges) {
|
||
expect(a).toBeGreaterThanOrEqual(0)
|
||
expect(a).toBeLessThan(n + 1)
|
||
expect(b).toBeGreaterThanOrEqual(0)
|
||
expect(b).toBeLessThan(n + 1)
|
||
}
|
||
})
|
||
|
||
it('is deterministic under a fixed seed and differs across seeds', () => {
|
||
const again = generateStartRegion(mulberry32(42))
|
||
expect(again).toEqual(region)
|
||
const other = generateStartRegion(mulberry32(43))
|
||
expect(other.start.name !== region.start.name || other.neighbors[0].name !== region.neighbors[0].name).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('generateRing (expansion)', () => {
|
||
it('adds four systems spaced away from the anchor and existing positions', () => {
|
||
const takenNames = new Set(['Kepler Drift'])
|
||
const takenPositions = [{ name: 'Kepler Drift', x: 50, y: 50 }]
|
||
const ring = generateRing(mulberry32(7), 50, 50, takenNames, takenPositions)
|
||
|
||
expect(ring.systems).toHaveLength(4)
|
||
const ringNames = ring.systems.map((s) => s.name)
|
||
for (const s of ring.systems) {
|
||
expect(Math.hypot(s.x - 50, s.y - 50)).toBeGreaterThanOrEqual(16)
|
||
expect(s.name).not.toBe('Kepler Drift') // never reuses a pre-taken name
|
||
}
|
||
expect(new Set(ringNames).size).toBe(4) // and makeName claimed them into the set
|
||
for (const n of ringNames) expect(takenNames.has(n)).toBe(true)
|
||
expect(ring.edges).toHaveLength(8) // anchor↔each + cycle
|
||
for (const [a, b] of ring.edges) {
|
||
expect(a).toBeGreaterThanOrEqual(0)
|
||
expect(a).toBeLessThan(5)
|
||
expect(b).toBeGreaterThanOrEqual(0)
|
||
expect(b).toBeLessThan(5)
|
||
}
|
||
})
|
||
|
||
it('never reuses a taken name', () => {
|
||
const rng = mulberry32(1)
|
||
const taken = new Set<string>()
|
||
for (let i = 0; i < 40; i++) makeName(rng, taken)
|
||
expect(taken.size).toBe(40)
|
||
})
|
||
})
|
||
|
||
describe('generatePlanets', () => {
|
||
it('gives every system 2–5 planets and keeps body names unique within the system', () => {
|
||
for (const seed of [1, 7, 42, 999]) {
|
||
const planets = generatePlanets(bodyRng(String(seed), seed))
|
||
expect(planets.length).toBeGreaterThanOrEqual(2)
|
||
expect(planets.length).toBeLessThanOrEqual(5)
|
||
|
||
const names: string[] = []
|
||
for (const p of planets) {
|
||
names.push(p.name, ...p.moons.map((m) => m.name))
|
||
expect(['TERRESTRIAL', 'GAS GIANT', 'ROCKY', 'ICE WORLD', 'VOLCANIC', 'OCEAN', 'ASTEROID FIELD']).toContain(p.type)
|
||
for (const m of p.moons) {
|
||
expect(['MOON', 'ICE MOON', 'VOLCANIC MOON']).toContain(m.type)
|
||
}
|
||
}
|
||
expect(new Set(names).size).toBe(names.length) // planets + moons share one name pool per system
|
||
}
|
||
})
|
||
|
||
it('is deterministic per (seed, system id) and differs across systems', () => {
|
||
const a = generatePlanets(bodyRng('42', 7))
|
||
expect(generatePlanets(bodyRng('42', 7))).toEqual(a) // same save+system → same bodies
|
||
const b = generatePlanets(bodyRng('43', 7))
|
||
expect(JSON.stringify(b)).not.toBe(JSON.stringify(a)) // different seed → different world
|
||
})
|
||
})
|
||
|
||
describe('rollExploration', () => {
|
||
it('is deterministic for the same save+planet and yields valid items', () => {
|
||
const first = rollExploration(explorationRng('42', 13))
|
||
expect(rollExploration(explorationRng('42', 13))).toEqual(first)
|
||
|
||
for (const item of first) {
|
||
expect(['resource', 'part']).toContain(item.kind)
|
||
if (item.kind === 'resource') {
|
||
expect(item.qty).toBeGreaterThanOrEqual(1)
|
||
expect(item.qty).toBeLessThanOrEqual(4)
|
||
} else {
|
||
expect(item.qty).toBe(1)
|
||
}
|
||
}
|
||
})
|
||
|
||
it('across many planets produces every outcome shape: resource, part, both, or nothing', () => {
|
||
const shapes = new Set<string>()
|
||
for (let planetId = 1; planetId <= 200; planetId++) {
|
||
const found = rollExploration(explorationRng('42', planetId))
|
||
const hasResource = found.some((f) => f.kind === 'resource')
|
||
const hasPart = found.some((f) => f.kind === 'part')
|
||
shapes.add(`${hasResource ? 'r' : '-'}${hasPart ? 'p' : '-'}`)
|
||
}
|
||
expect(shapes).toEqual(new Set(['rp', 'r-', '-p', '--']))
|
||
})
|
||
})
|