Adding first working version
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user