Files
SpaceWanderer/lib/game/crew.test.ts
kamilkrzywda 06f8a28e96
CI / build + unit + e2e (pull_request) Successful in 2m38s
fix(game): numbered name fallback when pool exhausted — server no longer freezes past 128 systems
Production freeze reported after the v0.9.3 (#21) deploy: every API route
hung and all panels stayed at LOADING. Root cause was not the #21 layout
(verified healthy across all 7 tabs/viewports) but makeName's unbounded
rejection loop over a finite 16x8=128 system-name pool — once a save grew
past ~128 systems, any jump into an undiscovered system spun forever on
Node's single thread (sql.js is synchronous), blocking the whole process.

makeName and makeCrewName now fall back to numbered variants ('Kepler Drift 2')
once their pools are exhausted, so they always terminate.
2026-09-29 09:18:00 +00:00

88 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { CREW_CHANCE, generateStartingCrew, makeCrewName, rollCrewMember, startingCrewRng } from './crew'
import { explorationRng } from './world'
describe('generateStartingCrew', () => {
it('gives a new game 2–3 members, each with exactly one good and one bad trait', () => {
for (const seed of [1, 7, 42, 999]) {
const crew = generateStartingCrew(startingCrewRng(String(seed)))
expect(crew.length).toBeGreaterThanOrEqual(2)
expect(crew.length).toBeLessThanOrEqual(3)
for (const member of crew) {
expect(member.name.trim().length).toBeGreaterThan(0)
expect(member.role.trim().length).toBeGreaterThan(0)
expect(member.traitGood.trim().length).toBeGreaterThan(0)
expect(member.traitBad.trim().length).toBeGreaterThan(0)
}
// Names are unique within the crew; roles too (drawn without replacement).
expect(new Set(crew.map((m) => m.name)).size).toBe(crew.length)
expect(new Set(crew.map((m) => m.role)).size).toBe(crew.length)
}
})
it('is deterministic under a fixed seed and differs across seeds', () => {
const a = generateStartingCrew(startingCrewRng('42'))
expect(generateStartingCrew(startingCrewRng('42'))).toEqual(a) // same save → same crew
const b = generateStartingCrew(startingCrewRng('43'))
expect(JSON.stringify(b)).not.toBe(JSON.stringify(a)) // different seed → different crew
})
})
describe('rollCrewMember', () => {
it('finds a member with the documented probability and never reuses a taken name', () => {
const rng = startingCrewRng('42')
let found = 0
for (let i = 0; i < 1000; i++) found += rollCrewMember(rng, new Set<string>()) ? 1 : 0
// 18% ± a comfortable band — catches an inverted or missing chance without flaking.
expect(found).toBeGreaterThan(90)
expect(found).toBeLessThan(270)
const taken = new Set(['Ava Vance'])
for (let i = 0; i < 50; i++) {
const member = rollCrewMember(startingCrewRng(String(i)), taken)
if (member) expect(member.name).not.toBe('Ava Vance') // never reuses a roster name
}
})
it('is deterministic per save+planet when drawn from the exploration stream', () => {
const first = rollCrewMember(explorationRng('42', 13), new Set<string>())
expect(rollCrewMember(explorationRng('42', 13), new Set<string>())).toEqual(first) // same save+planet → same result
// Across many planets both outcomes occur (someone is found, someone isn't).
let found = 0
for (let planetId = 1; planetId <= 200; planetId++) {
if (rollCrewMember(explorationRng('42', planetId), new Set<string>())) found++
}
expect(found).toBeGreaterThan(0)
expect(found).toBeLessThan(200)
})
it('keeps the crew chance documented and sane', () => {
expect(CREW_CHANCE).toBeGreaterThanOrEqual(0.15)
expect(CREW_CHANCE).toBeLessThanOrEqual(0.2)
})
})
describe('makeCrewName', () => {
it('never reuses a taken name and always terminates', () => {
const rng = startingCrewRng('7')
const taken = new Set<string>()
for (let i = 0; i < 40; i++) makeCrewName(rng, taken)
expect(taken.size).toBe(40)
})
it('terminates with numbered variants once all 352 base names are taken', () => {
const rng = startingCrewRng('7')
const taken = new Set<string>()
for (let i = 0; i < 352; i++) makeCrewName(rng, taken) // exhaust the whole pool
expect(taken.size).toBe(352)
const a = makeCrewName(rng, taken) // must return instead of spinning forever
const b = makeCrewName(rng, taken)
expect(a).not.toBe(b)
expect(taken.has(a)).toBe(true)
expect(taken.has(b)).toBe(true)
})
})