game: bodies generated with each system; explore action — deterministic haul, turn cost, inventory accumulation
This commit is contained in:
+99
-1
@@ -3,7 +3,9 @@ import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { openGameDb, type GameDb } from '@/lib/db/client'
|
||||
import { getWorldState, jumpToSystem, type WorldState } from './save'
|
||||
import * as schema from '@/lib/db/schema'
|
||||
import { getSystemState, explorePlanet, getWorldState, jumpToSystem, type WorldState } from './save'
|
||||
import { bodyRng, explorationRng, generatePlanets, rollExploration } from './world'
|
||||
|
||||
const dirs: string[] = []
|
||||
|
||||
@@ -130,3 +132,99 @@ describe('jumpToSystem success path', () => {
|
||||
expect(again.systems).toHaveLength(after.systems.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSystemState', () => {
|
||||
it('generates bodies for every system on new game; moons nest under a parent planet only', async () => {
|
||||
const { db } = await freshFile()
|
||||
const state = await getSystemState(db)
|
||||
|
||||
// Current (start) system: 2–5 planets, all unexplored yet.
|
||||
const planetRows = state.planets.filter((p) => p.parent === null)
|
||||
expect(planetRows.length).toBeGreaterThanOrEqual(2)
|
||||
expect(planetRows.length).toBeLessThanOrEqual(5)
|
||||
const planetIds = new Set(planetRows.map((p) => p.id))
|
||||
for (const row of state.planets) {
|
||||
if (row.parent !== null) expect(planetIds.has(row.parent)).toBe(true) // moons hang off a planet in the same system
|
||||
expect(row.explored).toBe(false)
|
||||
}
|
||||
|
||||
// Every one of the 7 fresh-save systems got bodies.
|
||||
const counts = db.raw.exec('SELECT system_id, COUNT(*) FROM planets GROUP BY system_id')
|
||||
expect(counts[0].values).toHaveLength(7)
|
||||
for (const [, n] of counts[0].values) expect(Number(n)).toBeGreaterThanOrEqual(2)
|
||||
|
||||
expect(state.inventory).toEqual([]) // nothing found before any scan
|
||||
expect(state.turn).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('explorePlanet', () => {
|
||||
it('rejects unknown planets with 404 and already-explored ones with 409, mutating nothing on failure', async () => {
|
||||
const { db } = await freshFile()
|
||||
const state = await getSystemState(db)
|
||||
const target = state.planets[0]
|
||||
|
||||
await expect(explorePlanet(db, 999_999)).rejects.toMatchObject({ status: 404 })
|
||||
await explorePlanet(db, target.id)
|
||||
await expect(explorePlanet(db, target.id)).rejects.toMatchObject({ status: 409 })
|
||||
|
||||
const after = await getSystemState(db)
|
||||
expect(after.turn).toBe(1) // only the successful scan costs a turn
|
||||
expect(after.planets.find((p) => p.id === target.id)?.explored).toBe(true)
|
||||
})
|
||||
|
||||
it('marks explored, bumps the turn, lands finds in inventory and writes an event', async () => {
|
||||
const { db } = await freshFile()
|
||||
const state = await getSystemState(db)
|
||||
const target = state.planets[0]
|
||||
|
||||
const result = await explorePlanet(db, target.id)
|
||||
|
||||
expect(result.turn).toBe(1)
|
||||
// Fresh save: inventory is exactly this scan's haul.
|
||||
for (const item of result.outcome) {
|
||||
expect(result.inventory.find((i) => i.kind === item.kind && i.name === item.name)?.qty).toBe(item.qty)
|
||||
}
|
||||
expect(result.inventory.length).toBe(result.outcome.length)
|
||||
|
||||
const rows = db.raw.exec('SELECT type, turn FROM events ORDER BY id')
|
||||
expect(rows[0].values).toEqual([['new_game', 0], ['explore', 1]])
|
||||
})
|
||||
|
||||
it('is deterministic per save+planet and accumulates qty across scans (known seed)', async () => {
|
||||
// Hand-built save with a known seed so every outcome is recomputable offline.
|
||||
const { db } = await freshFile()
|
||||
const [save] = await db.db.insert(schema.saves).values({ seed: '42', turn: 0, createdAt: new Date().toISOString() }).returning()
|
||||
const [sys] = await db.db
|
||||
.insert(schema.solarSystems)
|
||||
.values({ saveId: save.id, name: 'Test System', x: 50, y: 50, discovered: true, isCurrent: true })
|
||||
.returning({ id: schema.solarSystems.id })
|
||||
|
||||
const planetIds: number[] = []
|
||||
for (const body of generatePlanets(bodyRng('42', sys.id))) {
|
||||
const [p] = await db.db
|
||||
.insert(schema.planets)
|
||||
.values({ systemId: sys.id, parentId: null, name: body.name, type: body.type, hazard: body.hazard })
|
||||
.returning({ id: schema.planets.id })
|
||||
planetIds.push(p.id)
|
||||
}
|
||||
|
||||
const expected = planetIds.map((id) => rollExploration(explorationRng('42', id)))
|
||||
for (let i = 0; i < planetIds.length; i++) {
|
||||
const result = await explorePlanet(db, planetIds[i])
|
||||
expect(result.outcome).toEqual(expected[i]) // same save+planet → same haul
|
||||
}
|
||||
|
||||
// Inventory accumulates: qty per kind+name equals the sum over all scans.
|
||||
const totals = new Map<string, number>()
|
||||
for (const items of expected) {
|
||||
for (const it of items) totals.set(`${it.kind}|${it.name}`, (totals.get(`${it.kind}|${it.name}`) ?? 0) + it.qty)
|
||||
}
|
||||
const state = await getSystemState(db)
|
||||
expect(state.inventory.length).toBe(totals.size)
|
||||
for (const item of state.inventory) {
|
||||
expect(item.qty).toBe(totals.get(`${item.kind}|${item.name}`))
|
||||
}
|
||||
expect(state.turn).toBe(planetIds.length) // one turn per scan
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user