Working but slow

This commit is contained in:
2026-07-06 23:11:42 +02:00
parent 09c1fba8e6
commit f93bcfb8d4
15 changed files with 4443 additions and 4205 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js'
import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js'
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
@@ -389,7 +389,7 @@ export function stepNaga3d(cells: Cells, settings: SceneParams) {
? universeFromCoordinates(coordinates)
: seedUniverseFromCells(cells)
const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0
const seed = typeof simulation.seed === 'string' ? simulation.seed : 'naga'
const seed = resolveRngSeed(simulation, 'naga')
const topology: NagaTopology = {
boundary: resolveBoundaryCondition(simulation.grid),
size
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js'
import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { Cells, SceneParams } from '../types.js'
import { emptyCellsLike } from './cellGrid.js'
@@ -17,7 +17,7 @@ export function stepWildfire2d(cells: Cells, settings: SceneParams) {
const width = cells[0]?.length ?? 0
const boundary = resolveBoundaryCondition(settings.simulation?.grid)
const spreadProbability = parseProbability(settings.simulation?.spreadProbability, 1)
const seed = typeof settings.simulation?.seed === 'string' ? settings.simulation.seed : 'wildfire'
const seed = resolveRngSeed(settings.simulation, 'wildfire')
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
+1 -1
View File
@@ -95,7 +95,7 @@ registerCaEngineRuntime(
id: 'naga-3d',
label: 'Naga 3D',
supportedClasses: [{ dimensions: 3, states: 7 }],
defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', seed: 'naga', edgeWrap: true },
defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', rng_seed: 'naga', edgeWrap: true },
step: stepNaga3d
},
{ replace: true }
@@ -36,10 +36,27 @@ function clampProbability(value: unknown, fallback: number) {
return Math.max(0, Math.min(1, typeof value === 'number' && Number.isFinite(value) ? value : fallback))
}
function resolveIcSeed(params: JsonObject, fallback: string) {
if (typeof params.ic_seed === 'string' && params.ic_seed.trim()) return params.ic_seed
if (typeof params.seed === 'string' && params.seed.trim()) return params.seed
return fallback
}
function randomNormal(random: () => number) {
const left = Math.max(Number.EPSILON, random())
const right = Math.max(Number.EPSILON, random())
return Math.sqrt(-2 * Math.log(left)) * Math.cos(2 * Math.PI * right)
}
function sampleNagaLength(meanLength: number, lengthStdDev: number, maxLength: number, random: () => number) {
if (lengthStdDev <= 0) return clampInteger(meanLength, 5, 1, maxLength)
return clampInteger(meanLength + randomNormal(random) * lengthStdDev, meanLength, 1, maxLength)
}
function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const density = clampDensity(params.density)
const seed = typeof params.seed === 'string' ? params.seed : 'studio-seed'
const random = seededRandom(seed)
const icSeed = resolveIcSeed(params, 'studio-seed')
const random = seededRandom(icSeed)
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const cells: number[][] = []
@@ -57,7 +74,7 @@ function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: {
kind: 'random-soup',
density,
seed
ic_seed: icSeed
}
}
}
@@ -83,11 +100,13 @@ function inBounds(x: number, y: number, z: number, width: number, height: number
function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const maxLength = Math.max(1, width * height * depth)
const count = clampInteger(params.count, 3, 1, Math.max(1, width * height * depth))
const length = clampInteger(params.length, 5, 1, Math.max(1, width * height * depth))
const meanLength = clampInteger(params.meanLength ?? params.length, 5, 1, maxLength)
const lengthStdDev = Math.max(0, Math.min(maxLength, typeof params.lengthStdDev === 'number' && Number.isFinite(params.lengthStdDev) ? params.lengthStdDev : 0))
const sameTypeProbability = clampProbability(params.sameTypeProbability, 0.8)
const seed = typeof params.seed === 'string' ? params.seed : 'naga-markov'
const random = seededRandom(seed)
const icSeed = resolveIcSeed(params, 'naga-markov')
const random = seededRandom(icSeed)
const occupied = new Set<string>()
const cells: number[][] = []
const maxAttempts = count * 300
@@ -100,6 +119,7 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
let y = Math.floor(random() * height)
let z = Math.floor(random() * depth)
let arrow = randomNagaArrow(random)
const length = sampleNagaLength(meanLength, lengthStdDev, maxLength, random)
const proposed: number[][] = []
const proposedKeys = new Set<string>()
let valid = true
@@ -164,9 +184,10 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: {
kind: 'naga-markov',
count,
length,
meanLength,
lengthStdDev,
sameTypeProbability,
seed,
ic_seed: icSeed,
created
}
}
+6
View File
@@ -72,6 +72,12 @@ export function seededRandom(seed: string) {
}
}
export function resolveRngSeed(simulation: JsonObject | undefined, fallback: string) {
if (typeof simulation?.rng_seed === 'string' && simulation.rng_seed.trim()) return simulation.rng_seed
if (typeof simulation?.seed === 'string' && simulation.seed.trim()) return simulation.seed
return fallback
}
export function isBoundaryCondition(value: unknown): value is BoundaryCondition {
return value === 'wrap' || value === 'mirror' || value === 'fixed'
}
+39 -22
View File
@@ -72,7 +72,7 @@ interface IcgConfig {
dimensions: number
states: number
density: number
seed: string
icSeed: string
}
interface EngineConfig {
@@ -324,7 +324,7 @@ function defaultIcgConfig(): IcgConfig {
dimensions: 2,
states: 2,
density: 0.28,
seed: 'studio-seed'
icSeed: 'studio-seed'
}
}
@@ -353,16 +353,16 @@ function icgParamsSchema(config: IcgConfig): JsonObject {
max: 1,
step: 0.01
},
seed: {
ic_seed: {
type: 'string',
label: 'Seed',
default: config.seed
label: 'IC seed',
default: config.icSeed
}
}
}
function icgDefaultParams(config: IcgConfig): JsonObject {
return { density: config.density, seed: config.seed }
return { density: config.density, ic_seed: config.icSeed }
}
function ruleLabel(ruleId: string) {
@@ -1644,8 +1644,8 @@ function IcgConfigFields({
</label>
</div>
<label>
Seed
<input value={config.seed} onChange={(event) => onChange((current) => ({ ...current, seed: event.target.value }))} />
IC seed
<input value={config.icSeed} onChange={(event) => onChange((current) => ({ ...current, icSeed: event.target.value }))} />
</label>
</div>
)
@@ -1817,7 +1817,11 @@ function configFromIcg(generator: InitialConditionGenerator): IcgConfig {
dimensions: supportedClass?.dimensions ?? 2,
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
density: typeof generator.default_params.density === 'number' ? generator.default_params.density : 0.28,
seed: typeof generator.default_params.seed === 'string' ? generator.default_params.seed : 'studio-seed'
icSeed: typeof generator.default_params.ic_seed === 'string'
? generator.default_params.ic_seed
: typeof generator.default_params.seed === 'string'
? generator.default_params.seed
: 'studio-seed'
}
}
@@ -2168,7 +2172,8 @@ function LibraryEditor({
const [selectedEngineId, setSelectedEngineId] = React.useState('')
const [icgDensity, setIcgDensity] = React.useState(0.28)
const [icgNagaCount, setIcgNagaCount] = React.useState(3)
const [icgNagaLength, setIcgNagaLength] = React.useState(5)
const [icgNagaMeanLength, setIcgNagaMeanLength] = React.useState(5)
const [icgNagaLengthStdDev, setIcgNagaLengthStdDev] = React.useState(0)
const [icgNagaSameTypeProbability, setIcgNagaSameTypeProbability] = React.useState(0.8)
const [icgSeed, setIcgSeed] = React.useState('studio-seed')
const [nodeName, setNodeName] = React.useState('Untitled preset')
@@ -2664,24 +2669,28 @@ function LibraryEditor({
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
if (typeof generator?.default_params.density === 'number') setIcgDensity(generator.default_params.density)
if (typeof generator?.default_params.count === 'number') setIcgNagaCount(generator.default_params.count)
if (typeof generator?.default_params.length === 'number') setIcgNagaLength(generator.default_params.length)
if (typeof generator?.default_params.meanLength === 'number') setIcgNagaMeanLength(generator.default_params.meanLength)
else if (typeof generator?.default_params.length === 'number') setIcgNagaMeanLength(generator.default_params.length)
if (typeof generator?.default_params.lengthStdDev === 'number') setIcgNagaLengthStdDev(generator.default_params.lengthStdDev)
if (typeof generator?.default_params.sameTypeProbability === 'number') {
setIcgNagaSameTypeProbability(generator.default_params.sameTypeProbability)
}
if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed)
if (typeof generator?.default_params.ic_seed === 'string') setIcgSeed(generator.default_params.ic_seed)
else if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed)
}
function selectedIcgParams(generator: InitialConditionGenerator) {
if (generator.generator_kind === 'naga-markov') {
return {
count: icgNagaCount,
length: icgNagaLength,
meanLength: icgNagaMeanLength,
lengthStdDev: icgNagaLengthStdDev,
sameTypeProbability: icgNagaSameTypeProbability,
seed: icgSeed
ic_seed: icgSeed
}
}
return { density: Math.max(0, Math.min(1, icgDensity)), seed: icgSeed }
return { density: Math.max(0, Math.min(1, icgDensity)), ic_seed: icgSeed }
}
function applySelectedIcg() {
@@ -3204,12 +3213,6 @@ function LibraryEditor({
</label>
{selectedIcg?.generator_kind === 'naga-markov' ? (
<>
<div className="root-config-grid">
<label>
Length
<input min={1} step={1} type="number" value={icgNagaLength} onChange={(event) => setIcgNagaLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
</div>
<div className="icg-controls">
<label>
Naga count
@@ -3217,6 +3220,20 @@ function LibraryEditor({
</label>
<strong>{icgNagaCount}</strong>
</div>
<div className="icg-controls">
<label>
Mean length
<input max={64} min={1} step={1} type="range" value={icgNagaMeanLength} onChange={(event) => setIcgNagaMeanLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
<strong>{icgNagaMeanLength}</strong>
</div>
<div className="icg-controls">
<label>
Length std. dev.
<input max={32} min={0} step={1} type="range" value={icgNagaLengthStdDev} onChange={(event) => setIcgNagaLengthStdDev(Math.max(0, Number(event.target.value) || 0))} />
</label>
<strong>{icgNagaLengthStdDev}</strong>
</div>
<div className="icg-controls">
<label>
Same type probability
@@ -3235,7 +3252,7 @@ function LibraryEditor({
</div>
)}
<label>
Seed
IC seed
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
</label>
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
+52 -44
View File
@@ -30,7 +30,6 @@ interface SceneBundle {
bounds: THREE.LineSegments
camera: THREE.PerspectiveCamera
controls: OrbitControls
glowMesh: THREE.InstancedMesh | null
grid: THREE.GridHelper
mesh: THREE.InstancedMesh | null
mountNode: HTMLDivElement
@@ -60,23 +59,59 @@ function disposeMesh(scene: THREE.Scene, mesh: THREE.InstancedMesh | null) {
}
function createVoxelMaterial(primaryColor: THREE.Color) {
return new THREE.MeshStandardMaterial({
color: primaryColor,
emissive: primaryColor.clone().multiplyScalar(0.58),
emissiveIntensity: 0.48,
metalness: 0.04,
roughness: 0.68,
vertexColors: true
})
}
return new THREE.ShaderMaterial({
defines: {
USE_INSTANCING_COLOR: ''
},
uniforms: {
ambientStrength: { value: 0.56 },
diffuseStrength: { value: 0.58 },
glowStrength: { value: 0.34 },
lightDirection: { value: new THREE.Vector3(0.45, 0.82, 0.36).normalize() },
primaryColor: { value: primaryColor }
},
vertexShader: `
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
function createVoxelGlowMaterial() {
return new THREE.MeshBasicMaterial({
blending: THREE.AdditiveBlending,
depthWrite: false,
opacity: 0.26,
transparent: true,
vertexColors: true
void main() {
vColor = instanceColor;
vNormal = normalize(mat3(modelMatrix * instanceMatrix) * normal);
vec4 worldPosition = modelMatrix * instanceMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`,
fragmentShader: `
uniform float ambientStrength;
uniform float diffuseStrength;
uniform float glowStrength;
uniform vec3 lightDirection;
uniform vec3 primaryColor;
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
float diffuse = max(dot(normal, normalize(lightDirection)), 0.0);
float reverseFill = max(dot(normal, normalize(vec3(-0.45, 0.42, -0.68))), 0.0);
float rim = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.15);
float faceLift = pow(max(abs(normal.y), 0.0), 0.7) * 0.08;
vec3 color = max(vColor, primaryColor * 0.12);
vec3 lit = color * (ambientStrength + diffuse * diffuseStrength + reverseFill * 0.16 + faceLift);
vec3 glow = color * (0.18 + rim * glowStrength);
vec3 finalColor = lit + glow;
gl_FragColor = vec4(finalColor, 1.0);
}
`,
toneMapped: true
})
}
@@ -123,18 +158,6 @@ function createVoxelMesh(size: number, options: VoxelRendererOptions) {
return mesh
}
function createVoxelGlowMesh(size: number, options: VoxelRendererOptions) {
const spacing = WORLD_SIZE / size
const edge = spacing * (1 - clamp(options.cellGap, 0.02, 0.45)) * 1.18
const geometry = new THREE.BoxGeometry(edge, edge, edge)
const material = createVoxelGlowMaterial()
const mesh = new THREE.InstancedMesh(geometry, material, Math.max(1, size ** 3))
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
mesh.castShadow = false
mesh.receiveShadow = false
return mesh
}
const CATEGORICAL_STATE_COLORS = [
'#70f45f',
'#f0c94a',
@@ -218,17 +241,12 @@ function colorForCell(
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.mesh = createVoxelMesh(size, options)
bundle.glowMesh = createVoxelGlowMesh(size, options)
bundle.scene.add(bundle.glowMesh)
bundle.scene.add(bundle.mesh)
const mesh = bundle.mesh
const glowMesh = bundle.glowMesh
const dummy = new THREE.Object3D()
const color = new THREE.Color()
const glowColor = new THREE.Color()
const spacing = WORLD_SIZE / size
const origin = -WORLD_SIZE / 2 + spacing / 2
const statePalette = createStatePalette(cells, options)
@@ -244,22 +262,14 @@ function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number,
colorForCell(bundle, cell, size, ruleId, statePalette, color)
mesh.setMatrixAt(instanceIndex, dummy.matrix)
mesh.setColorAt(instanceIndex, color)
glowColor.copy(color).lerp(new THREE.Color('#ffffff'), 0.16).multiplyScalar(1.18)
glowMesh.setMatrixAt(instanceIndex, dummy.matrix)
glowMesh.setColorAt(instanceIndex, glowColor)
instanceIndex += 1
}
mesh.count = instanceIndex
glowMesh.count = instanceIndex
mesh.instanceMatrix.needsUpdate = true
glowMesh.instanceMatrix.needsUpdate = true
if (mesh.instanceColor) {
mesh.instanceColor.needsUpdate = true
}
if (glowMesh.instanceColor) {
glowMesh.instanceColor.needsUpdate = true
}
}
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
@@ -309,7 +319,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
bounds,
camera,
controls,
glowMesh: null,
grid,
mesh: null,
mountNode: targetNode,
@@ -347,7 +356,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
window.cancelAnimationFrame(bundle.animationFrame)
bundle.resizeObserver.disconnect()
disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.bounds.geometry.dispose()
disposeMaterial(bundle.bounds.material)
bundle.grid.geometry.dispose()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CA Studio Admin</title>
<script type="module" crossorigin src="/admin/assets/index-COp4VUl7.js"></script>
<script type="module" crossorigin src="/admin/assets/index-DowkWVYj.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-D7EhCe8j.css">
</head>
<body>
@@ -0,0 +1,17 @@
UPDATE ca_engines
SET
params_schema =
(params_schema - 'seed') ||
jsonb_build_object(
'rng_seed',
jsonb_build_object(
'type', 'string',
'label', 'RNG seed',
'default', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'),
'group', 'Randomness'
)
),
default_params =
(default_params - 'seed') ||
jsonb_build_object('rng_seed', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'))
WHERE slug = 'naga-3d';
@@ -0,0 +1,55 @@
UPDATE ca_initial_condition_generators
SET
params_schema =
(params_schema - 'seed' - 'length') ||
jsonb_build_object(
'ic_seed',
jsonb_build_object(
'type', 'string',
'label', 'IC seed',
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov')
),
'meanLength',
jsonb_build_object(
'type', 'range',
'label', 'Mean length',
'default', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
'min', 1,
'max', 64,
'step', 1
),
'lengthStdDev',
jsonb_build_object(
'type', 'range',
'label', 'Length std. dev.',
'default', COALESCE(default_params->'lengthStdDev', '0'::jsonb),
'min', 0,
'max', 32,
'step', 1
)
),
default_params =
(default_params - 'seed' - 'length') ||
jsonb_build_object(
'ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov'),
'meanLength', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
'lengthStdDev', COALESCE(default_params->'lengthStdDev', '0'::jsonb)
)
WHERE slug = 'naga-markov-chains';
UPDATE ca_initial_condition_generators
SET
params_schema =
(params_schema - 'seed') ||
jsonb_build_object(
'ic_seed',
jsonb_build_object(
'type', 'string',
'label', 'IC seed',
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed')
)
),
default_params =
(default_params - 'seed') ||
jsonb_build_object('ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed'))
WHERE slug <> 'naga-markov-chains';
+29
View File
@@ -188,6 +188,35 @@ describe('CA engine runtimes', () => {
expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]]))
})
it('uses simulation rng_seed to choose deterministic Naga heads', () => {
const current = cells(6, 6)
const baseParams = {
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [6, 6, 6], boundary: 'wrap' },
initialCondition: {
type: 'cells',
cells: [[1, 2, 2, 1], [4, 2, 2, 1]]
}
}
} satisfies SceneParams
const leftSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'bar' }
}))
const rightSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'alpha' }
}))
expect(leftSeed.settings.simulation?.initialCondition?.cells).toEqual([[4, 2, 2, 1], [0, 2, 2, 1]])
expect(rightSeed.settings.simulation?.initialCondition?.cells).toEqual([[1, 2, 2, 1], [3, 2, 2, 1]])
})
it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => {
const current = cells(5, 5)
const params = settings({
@@ -19,7 +19,7 @@ describe('CA initial condition runtimes', () => {
const input = {
caClass: { dimensions: 2, states: 2 },
settings: { simulation: { grid: { size: [5, 5, 1] } } },
params: { density: 0.35, seed: 'repeatable' }
params: { density: 0.35, ic_seed: 'repeatable' }
}
expect(generateInitialCondition('random-soup', input)).toEqual(generateInitialCondition('random-soup', input))
@@ -29,11 +29,11 @@ describe('CA initial condition runtimes', () => {
const generated = generateInitialCondition('random-soup', {
caClass: { dimensions: 2, states: 2 },
settings: { simulation: { grid: { size: [3, 3, 1] } } },
params: { density: 2, seed: 'full' }
params: { density: 2, ic_seed: 'full' }
})
expect(generated.cells).toHaveLength(9)
expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, seed: 'full' })
expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, ic_seed: 'full' })
})
it('lets custom initial condition runtimes plug into the same contract', () => {
@@ -54,7 +54,7 @@ describe('CA initial condition runtimes', () => {
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' }
params: { count: 2, meanLength: 5, lengthStdDev: 0, sameTypeProbability: 1, ic_seed: 'straight-nagas' }
})
expect(generated.cells).toHaveLength(10)
@@ -62,9 +62,10 @@ describe('CA initial condition runtimes', () => {
expect(generated.generator).toMatchObject({
kind: 'naga-markov',
count: 2,
length: 5,
meanLength: 5,
lengthStdDev: 0,
sameTypeProbability: 1,
seed: 'straight-nagas',
ic_seed: 'straight-nagas',
created: 2
})
@@ -93,6 +94,31 @@ describe('CA initial condition runtimes', () => {
}
})
it('samples Naga Markov chain lengths from deterministic mean and deviation settings', () => {
const generated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [30, 30, 30] } } },
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
})
const repeated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [30, 30, 30] } } },
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
})
expect(generated).toEqual(repeated)
expect(generated.generator).toMatchObject({
kind: 'naga-markov',
count: 8,
meanLength: 7,
lengthStdDev: 3,
ic_seed: 'variable-nagas',
created: 8
})
expect(generated.cells.length).not.toBe(56)
})
it('rejects duplicate runtime ids and missing implementations', () => {
expect(() =>
registerInitialConditionRuntime({
+23 -3
View File
@@ -56,11 +56,17 @@ describe('CA Studio API', () => {
default_params: {
ruleId: 'naga-3d',
rendererId: 'voxel-3d',
seed: 'naga',
rng_seed: 'naga',
edgeWrap: true
}
})
expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }])
expect(naga.params_schema.rng_seed).toMatchObject({
type: 'string',
label: 'RNG seed',
default: 'naga',
group: 'Randomness'
})
expect(naga.params_schema.edgeWrap).toMatchObject({
type: 'boolean',
label: 'Edge wrap',
@@ -84,15 +90,29 @@ describe('CA Studio API', () => {
generator_kind: 'naga-markov',
default_params: {
count: 3,
length: 5,
meanLength: 5,
lengthStdDev: 0,
sameTypeProbability: 0.8,
seed: 'naga-markov'
ic_seed: 'naga-markov'
}
})
expect(generator.params_schema.meanLength).toMatchObject({
type: 'range',
default: 5
})
expect(generator.params_schema.lengthStdDev).toMatchObject({
type: 'range',
default: 0
})
expect(generator.params_schema.sameTypeProbability).toMatchObject({
type: 'range',
default: 0.8
})
expect(generator.params_schema.ic_seed).toMatchObject({
type: 'string',
label: 'IC seed',
default: 'naga-markov'
})
})
})