db+game: crew table and pure seeded crew generation (starting crew + exploration roll)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user