test(ecology-simulation): Update golden test vectors to validate ecological simulation outputs

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 01:06:56 -07:00
parent 1bc7fa9349
commit 084c610adf

View file

@ -102,6 +102,7 @@ function makeGrid(seed: number): GridState {
global_avg_temp: 0.5,
ocean_dead_fraction: 0.0,
ecosystem_health: 0.5,
sea_level: 0.2,
}
}
@ -119,6 +120,7 @@ interface TileSnapshot {
quality: number
habitat_suitability: number
drought_counter: number
moisture: number
}
function snapshotTile(tile: TileState): TileSnapshot {
@ -132,6 +134,7 @@ function snapshotTile(tile: TileState): TileSnapshot {
quality: tile.quality,
habitat_suitability: tile.habitat_suitability,
drought_counter: tile.drought_counter,
moisture: tile.moisture,
}
}
@ -438,3 +441,161 @@ describe('Ecology Golden Vectors (seed 42, 20×20, turns 0/10/50)', () => {
}
})
})
// ---------------------------------------------------------------------------
// Block 5.5: Extended golden vectors — 100 turns, biome coverage, food web pyramid
// ---------------------------------------------------------------------------
describe('Extended Golden Vectors (seed 42, 20×20, 100 turns)', () => {
let grid: GridState
let snap100: Map<string, TileSnapshot>
function snapAll(g: GridState): Map<string, TileSnapshot> {
const m = new Map<string, TileSnapshot>()
for (const t of g.tiles) {
m.set(`${t.col},${t.row}`, snapshotTile(t))
}
return m
}
beforeAll(() => {
grid = makeGrid(SEED)
const eco = new EcologyPhysics()
for (let t = 0; t < 100; t++) {
eco.processStep(grid)
}
snap100 = snapAll(grid)
})
it('turn 100: multiple biome types present on map', () => {
const biomeIds = new Set<string>()
for (const [, snap] of snap100) {
if (snap.biome_id !== 'ocean') biomeIds.add(snap.biome_id)
}
// With the current 6-biome BIOME_DEFS in generated TS, classifier produces
// a subset. At minimum we expect 2+ distinct biomes on a 20x20 map.
expect(biomeIds.size).toBeGreaterThanOrEqual(2)
})
it('turn 100: no biome classification results in "unclassified"', () => {
for (const [, snap] of snap100) {
expect(snap.biome_id).not.toBe('unclassified')
}
})
it('turn 100: at least one tile reached Q3+', () => {
let maxQ = 0
for (const [, snap] of snap100) {
if (snap.biome_id === 'ocean') continue
maxQ = Math.max(maxQ, snap.quality)
}
expect(maxQ).toBeGreaterThanOrEqual(3)
})
it('turn 100: flora health is non-zero on most vegetated biomes', () => {
let vegetatedCount = 0
let nonZeroFlora = 0
for (const [, snap] of snap100) {
if (snap.biome_id === 'ocean' || snap.biome_id === 'desert') continue
vegetatedCount++
if (snap.canopy_cover > 0 || snap.undergrowth > 0) {
nonZeroFlora++
}
}
if (vegetatedCount > 0) {
// At least 40% of non-desert land tiles should have some flora after 100 turns
// (limited biome defs in current generated TS means some tiles remain barren)
expect(nonZeroFlora / vegetatedCount).toBeGreaterThan(0.4)
}
})
it('turn 100: food web pyramid holds (producer proxy > predator proxy)', () => {
// In the simplified guide engine, undergrowth represents producer biomass
// and canopy represents predator-supporting structure.
// We verify that undergrowth > 0 on tiles with habitat suitability.
let tilesWithHabitat = 0
let tilesWithUndergrowth = 0
for (const tile of grid.tiles) {
if (isWater(tile)) continue
if (tile.habitat_suitability > 0.1) {
tilesWithHabitat++
if (tile.undergrowth > 0.05) tilesWithUndergrowth++
}
}
if (tilesWithHabitat > 0) {
// Producer base (undergrowth) should underpin most habitable tiles
expect(tilesWithUndergrowth / tilesWithHabitat).toBeGreaterThan(0.3)
}
})
it('turn 100: ecosystem health is reasonable', () => {
expect(grid.ecosystem_health).toBeGreaterThan(0.1)
expect(grid.ecosystem_health).toBeLessThanOrEqual(1.0)
})
it('turn 100: quality tiers are distributed (not all Q1)', () => {
const counts: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }
for (const [, snap] of snap100) {
if (snap.biome_id === 'ocean') continue
counts[snap.quality] = (counts[snap.quality] ?? 0) + 1
}
// Should have tiles in at least 2 quality tiers
const nonZeroTiers = Object.values(counts).filter(c => c > 0).length
expect(nonZeroTiers).toBeGreaterThanOrEqual(2)
})
})
// ---------------------------------------------------------------------------
// Block 5.6: TypeScript determinism — two independent runs produce identical results
// Validates the generated TS matches itself (cross-engine validation with GDScript
// requires the transpiler to be re-run when GDScript changes).
// ---------------------------------------------------------------------------
describe('TypeScript Determinism Vectors (seed 42)', () => {
it('two independent 100-turn runs produce identical ecology fields within ±0.001', () => {
const grid1 = makeGrid(SEED)
const eco1 = new EcologyPhysics()
for (let t = 0; t < 100; t++) eco1.processStep(grid1)
const grid2 = makeGrid(SEED)
const eco2 = new EcologyPhysics()
for (let t = 0; t < 100; t++) eco2.processStep(grid2)
for (let i = 0; i < grid1.tiles.length; i++) {
const a = grid1.tiles[i]
const b = grid2.tiles[i]
expect(a.biome_id).toBe(b.biome_id)
expect(a.canopy_cover).toBeCloseTo(b.canopy_cover, 3)
expect(a.undergrowth).toBeCloseTo(b.undergrowth, 3)
expect(a.fungi_network).toBeCloseTo(b.fungi_network, 3)
expect(a.quality).toBe(b.quality)
expect(a.habitat_suitability).toBeCloseTo(b.habitat_suitability, 3)
expect(a.drought_counter).toBe(b.drought_counter)
expect(a.succession_progress).toBe(b.succession_progress)
expect(a.landmark_name).toBe(b.landmark_name)
}
expect(grid1.ecosystem_health).toBeCloseTo(grid2.ecosystem_health, 3)
})
it('ecology fields are within valid ranges after 100 turns', () => {
const grid1 = makeGrid(SEED)
const eco1 = new EcologyPhysics()
for (let t = 0; t < 100; t++) eco1.processStep(grid1)
for (const tile of grid1.tiles) {
if (isWater(tile)) continue
expect(tile.canopy_cover).toBeGreaterThanOrEqual(0.0)
expect(tile.canopy_cover).toBeLessThanOrEqual(1.0)
expect(tile.undergrowth).toBeGreaterThanOrEqual(0.0)
expect(tile.undergrowth).toBeLessThanOrEqual(1.0)
expect(tile.fungi_network).toBeGreaterThanOrEqual(0.0)
expect(tile.fungi_network).toBeLessThanOrEqual(1.0)
expect(tile.quality).toBeGreaterThanOrEqual(1)
expect(tile.quality).toBeLessThanOrEqual(5)
expect(tile.habitat_suitability).toBeGreaterThanOrEqual(0.0)
expect(tile.habitat_suitability).toBeLessThanOrEqual(1.0)
expect(tile.drought_counter).toBeGreaterThanOrEqual(0)
}
})
})