Files
glitch_automata_lab/backend/test/caEngines.test.ts
T
2026-07-06 23:11:42 +02:00

274 lines
8.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { listCaEngineRuntimes, registerCaEngineRuntime, stepCaCells, stepCaSimulation } from '../admin/src/caEngines.js'
import type { Cells, SceneParams } from '../admin/src/types.js'
function cells(width: number, height: number, alive: Array<[number, number]> = []): Cells {
const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => false))
for (const [x, y] of alive) grid[y][x] = true
return grid
}
function settings(params: SceneParams = {}): SceneParams {
return {
...params,
simulation: {
engineId: 'game-of-life-2d',
ruleId: 'B3/S23',
neighborhoodId: 'moore',
...params.simulation
}
}
}
describe('CA engine runtimes', () => {
it('registers built-in runtimes that can be selected by engine id', () => {
expect(listCaEngineRuntimes().map((engine) => engine.id)).toEqual(
expect.arrayContaining([
'game-of-life-2d',
'outer-totalistic-2d',
'elementary-1d',
'wildfire-2d',
'naga-3d',
'generic-voxel-ca',
'noop'
])
)
})
it('allows a drop-in engine to register and step cells through the shared contract', () => {
registerCaEngineRuntime({
id: 'test-fill-runtime',
label: 'Test fill runtime',
step: (current) => current.map((row) => row.map(() => true))
})
expect(stepCaCells(cells(2, 2), settings({ simulation: { engineId: 'test-fill-runtime' } }))).toEqual([
[true, true],
[true, true]
])
})
it('rejects duplicate engine runtime ids', () => {
expect(() =>
registerCaEngineRuntime({
id: 'game-of-life-2d',
label: 'Duplicate Life',
step: (current) => current
})
).toThrow('already registered')
})
it('rejects engine output that changes the grid contract', () => {
registerCaEngineRuntime({
id: 'test-invalid-shape-runtime',
label: 'Test invalid shape runtime',
step: () => [[true]]
})
expect(() => stepCaCells(cells(2, 2), settings({ simulation: { engineId: 'test-invalid-shape-runtime' } }))).toThrow(
'preserve the input grid dimensions'
)
})
it('does not mutate the input cell grid while stepping', () => {
const current = cells(3, 3, [
[0, 1],
[1, 1],
[2, 1]
])
const before = current.map((row) => [...row])
const next = stepCaCells(current, settings())
expect(current).toEqual(before)
expect(next).not.toBe(current)
expect(next[0]).not.toBe(current[0])
})
it('steps Conway Life oscillators with the outer-totalistic B/S rule contract', () => {
const blinker = cells(5, 5, [
[2, 1],
[2, 2],
[2, 3]
])
expect(stepCaCells(blinker, settings())).toEqual(
cells(5, 5, [
[1, 2],
[2, 2],
[3, 2]
])
)
})
it('steps elementary 1D rules on the active source row', () => {
const current = cells(3, 3, [[1, 0]])
expect(
stepCaCells(current, settings({ simulation: { engineId: 'elementary-1d', ruleId: 'Rule 90' } }))
).toEqual(cells(3, 3, [
[0, 0],
[2, 0]
]))
})
it('uses fixed boundaries for elementary 1D edge samples when requested', () => {
const current = cells(3, 3, [[0, 0]])
expect(
stepCaCells(current, settings({ simulation: { engineId: 'elementary-1d', ruleId: 'Rule 90', grid: { boundary: 'fixed' } } }))
).toEqual(cells(3, 3, [[1, 0]]))
})
it('steps binary wildfire by cooling burning cells and igniting von Neumann neighbours', () => {
const current = cells(3, 3, [[1, 1]])
expect(
stepCaCells(current, settings({ simulation: { engineId: 'wildfire-2d', spreadProbability: 1 } }))
).toEqual(cells(3, 3, [
[1, 0],
[0, 1],
[2, 1],
[1, 2]
]))
})
it('lets binary wildfire suppress spread with deterministic probability settings', () => {
const current = cells(3, 3, [[1, 1]])
expect(
stepCaCells(current, settings({ simulation: { engineId: 'wildfire-2d', spreadProbability: 0 } }))
).toEqual(cells(3, 3))
})
it('steps Naga 3D sparse coordinates through the shared grid projection contract', () => {
const current = cells(5, 5)
const params = settings({
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [5, 5, 5], boundary: 'wrap' },
initialCondition: {
type: 'cells',
cells: [[2, 2, 2, 1]]
}
}
})
const stepped = stepCaSimulation(current, params)
expect(stepped.cells).toEqual(cells(5, 5, [[1, 2]]))
expect(params.simulation?.initialCondition?.cells).toEqual([[2, 2, 2, 1]])
expect(params.simulation?.nagaTick).toBeUndefined()
expect(stepped.settings.simulation?.initialCondition?.cells).toEqual([[1, 2, 2, 1]])
expect(stepped.settings.simulation?.nagaTick).toBe(1)
})
it('wraps Naga 3D sparse coordinates before computing voxel addresses', () => {
const current = cells(5, 5)
const params = settings({
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [5, 5, 5], boundary: 'wrap' },
initialCondition: {
type: 'cells',
cells: [[4, 2, 2, 4]]
}
}
})
const stepped = stepCaSimulation(current, params)
expect(stepped.settings.simulation?.initialCondition?.cells).toEqual([[0, 2, 2, 4]])
expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]]))
})
it('uses simulation rng_seed to choose deterministic Naga heads', () => {
const current = cells(6, 6)
const baseParams = {
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [6, 6, 6], boundary: 'wrap' },
initialCondition: {
type: 'cells',
cells: [[1, 2, 2, 1], [4, 2, 2, 1]]
}
}
} satisfies SceneParams
const leftSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'bar' }
}))
const rightSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'alpha' }
}))
expect(leftSeed.settings.simulation?.initialCondition?.cells).toEqual([[4, 2, 2, 1], [0, 2, 2, 1]])
expect(rightSeed.settings.simulation?.initialCondition?.cells).toEqual([[1, 2, 2, 1], [3, 2, 2, 1]])
})
it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => {
const current = cells(5, 5)
const params = settings({
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [5, 5, 5], boundary: 'fixed' },
initialCondition: {
type: 'cells',
cells: [[4, 2, 2, 4]]
}
}
})
const stepped = stepCaSimulation(current, params)
expect(stepped.settings.simulation?.initialCondition?.cells).toEqual([[4, 2, 2, 4]])
expect(stepped.cells).toEqual(cells(5, 5, [[4, 2]]))
})
it('defaults boundaries to wrap when the node does not specify a boundary condition', () => {
const current = cells(3, 3, [
[2, 2],
[2, 0],
[0, 2]
])
expect(stepCaCells(current, settings())[0][0]).toBe(true)
})
it('supports fixed boundaries and legacy wrap=false nodes', () => {
const current = cells(3, 3, [
[2, 2],
[2, 0],
[0, 2]
])
expect(stepCaCells(current, settings({ simulation: { grid: { boundary: 'fixed' } } }))[0][0]).toBe(false)
expect(stepCaCells(current, settings({ simulation: { grid: { wrap: false } } }))[0][0]).toBe(false)
})
it('supports mirror boundaries by reflecting out-of-bounds neighbour samples', () => {
const current = cells(3, 3, [
[0, 0],
[1, 0],
[0, 1]
])
expect(stepCaCells(current, settings({ simulation: { grid: { boundary: 'fixed' } } }))[0][0]).toBe(true)
expect(stepCaCells(current, settings({ simulation: { grid: { boundary: 'mirror' } } }))[0][0]).toBe(false)
})
})