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('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) }) })