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