Adding first working version

This commit is contained in:
2026-07-06 13:11:48 +02:00
parent 3cd9df9bfe
commit 41e9c76028
36 changed files with 6211 additions and 4391 deletions
+71 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { listCaEngineRuntimes, registerCaEngineRuntime, stepCaCells } from '../admin/src/caEngines.js'
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 {
@@ -28,6 +28,7 @@ describe('CA engine runtimes', () => {
'outer-totalistic-2d',
'elementary-1d',
'wildfire-2d',
'naga-3d',
'generic-voxel-ca',
'noop'
])
@@ -140,6 +141,75 @@ describe('CA engine runtimes', () => {
).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],
@@ -8,8 +8,11 @@ import {
describe('CA initial condition runtimes', () => {
it('registers the built-in random soup generator', () => {
expect(listInitialConditionRuntimes().map((runtime) => runtime.id)).toContain('random-soup')
expect(listInitialConditionRuntimes().map((runtime) => runtime.id)).toEqual(
expect.arrayContaining(['random-soup', 'naga-markov'])
)
expect(getInitialConditionRuntime('random-soup')?.label).toBe('Random soup')
expect(getInitialConditionRuntime('naga-markov')?.label).toBe('Naga Markov chains')
})
it('generates deterministic cell coordinates for the same seed and density', () => {
@@ -47,6 +50,49 @@ describe('CA initial condition runtimes', () => {
}).cells).toEqual([[1, 1, 0]])
})
it('generates collision-free Naga Markov chains with directional continuity', () => {
const generated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [12, 12, 12] } } },
params: { count: 2, length: 5, sameTypeProbability: 1, seed: 'straight-nagas' }
})
expect(generated.cells).toHaveLength(10)
expect(new Set(generated.cells.map(([x, y, z]) => `${x}_${y}_${z}`)).size).toBe(10)
expect(generated.generator).toMatchObject({
kind: 'naga-markov',
count: 2,
length: 5,
sameTypeProbability: 1,
seed: 'straight-nagas',
created: 2
})
const occupied = new Set(generated.cells.map(([x, y, z]) => `${x}_${y}_${z}`))
for (let start = 0; start < generated.cells.length; start += 5) {
for (let index = start; index < start + 4; index += 1) {
const [x, y, z, arrow] = generated.cells[index]
const [nextX, nextY, nextZ, nextArrow] = generated.cells[index + 1]
expect(nextArrow).toBe(arrow)
if (arrow === 1) expect([nextX, nextY, nextZ]).toEqual([x + 1, y, z])
if (arrow === 2) expect([nextX, nextY, nextZ]).toEqual([x, y + 1, z])
if (arrow === 3) expect([nextX, nextY, nextZ]).toEqual([x, y, z + 1])
if (arrow === 4) expect([nextX, nextY, nextZ]).toEqual([x - 1, y, z])
if (arrow === 5) expect([nextX, nextY, nextZ]).toEqual([x, y - 1, z])
if (arrow === 6) expect([nextX, nextY, nextZ]).toEqual([x, y, z - 1])
}
const [x, y, z, arrow] = generated.cells[start + 4]
const terminalTarget =
arrow === 1 ? [x + 1, y, z] :
arrow === 2 ? [x, y + 1, z] :
arrow === 3 ? [x, y, z + 1] :
arrow === 4 ? [x - 1, y, z] :
arrow === 5 ? [x, y - 1, z] :
[x, y, z - 1]
expect(occupied.has(terminalTarget.join('_'))).toBe(false)
}
})
it('rejects duplicate runtime ids and missing implementations', () => {
expect(() =>
registerInitialConditionRuntime({
+92
View File
@@ -4,6 +4,98 @@ import { createCaStudioApi } from '../src/caStudioApi.js'
import { createTestDb } from './testDb.js'
describe('CA Studio API', () => {
it('seeds a useful shelf of elementary 1D CA rules', async () => {
const db = await createTestDb()
const app = createCaStudioApi(db)
await request(app)
.get('/api/ca/engines?dimensions=1&states=2')
.expect(200)
.expect((response) => {
const rules = response.body
.filter((engine: { engine_kind: string }) => engine.engine_kind === 'elementary-1d')
.map((engine: { default_params: { ruleId?: string } }) => engine.default_params.ruleId)
expect(rules).toEqual(expect.arrayContaining([
'Rule 18',
'Rule 22',
'Rule 30',
'Rule 45',
'Rule 54',
'Rule 60',
'Rule 73',
'Rule 90',
'Rule 110',
'Rule 122',
'Rule 126',
'Rule 150',
'Rule 184'
]))
const rule90 = response.body.find((engine: { slug: string }) => engine.slug === 'elementary-rule-90')
expect(rule90.params_schema.ruleId).toMatchObject({
type: 'select',
default: 'Rule 90',
options: [{ value: 'Rule 90', label: 'Rule 90' }]
})
})
})
it('seeds the Naga 3D engine for seven-state voxel spaces', async () => {
const db = await createTestDb()
const app = createCaStudioApi(db)
await request(app)
.get('/api/ca/engines?dimensions=3&states=7')
.expect(200)
.expect((response) => {
const naga = response.body.find((engine: { slug: string }) => engine.slug === 'naga-3d')
expect(naga).toMatchObject({
name: 'Naga 3D',
engine_kind: 'naga-3d',
default_params: {
ruleId: 'naga-3d',
rendererId: 'voxel-3d',
seed: 'naga',
edgeWrap: true
}
})
expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }])
expect(naga.params_schema.edgeWrap).toMatchObject({
type: 'boolean',
label: 'Edge wrap',
default: true,
group: 'Edge settings'
})
})
})
it('seeds the Naga Markov initial condition generator for seven-state voxel spaces', async () => {
const db = await createTestDb()
const app = createCaStudioApi(db)
await request(app)
.get('/api/ca/initial-condition-generators?dimensions=3&states=7')
.expect(200)
.expect((response) => {
const generator = response.body.find((item: { slug: string }) => item.slug === 'naga-markov-chains')
expect(generator).toMatchObject({
name: 'Naga Markov chains',
generator_kind: 'naga-markov',
default_params: {
count: 3,
length: 5,
sameTypeProbability: 0.8,
seed: 'naga-markov'
}
})
expect(generator.params_schema.sameTypeProbability).toMatchObject({
type: 'range',
default: 0.8
})
})
})
it('lists preset nodes whose resolved settings use an engine', async () => {
const db = await createTestDb()
const app = createCaStudioApi(db)
+3 -4
View File
@@ -94,10 +94,9 @@ describe('CA Studio repository', () => {
supportedClasses: [{ neighborhoodId: 'von-neumann', dimensions: 2, states: 3 }]
})
expect((await listInitialConditionGenerators(db)).map((generator) => generator.slug)).toEqual([
'life-random-soup',
'von-neumann-traffic'
])
expect((await listInitialConditionGenerators(db)).map((generator) => generator.slug)).toEqual(
expect.arrayContaining(['life-random-soup', 'von-neumann-traffic', 'naga-markov-chains'])
)
expect(
(await listInitialConditionGenerators(db, {
caClass: { neighborhoodId: 'moore', dimensions: 2, states: 2 }