Files
glitch_automata_lab/backend/test/caEngines.test.ts
T

175 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-07-01 12:10:12 +02:00
import { describe, expect, it } from 'vitest'
import { listCaEngineRuntimes, registerCaEngineRuntime, stepCaCells } 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',
'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('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)
})
})