import type { BoundaryCondition, CaClass, Cells, JsonObject, SceneParams } from './types.js' export const DEFAULT_BOUNDARY_CONDITION: BoundaryCondition = 'wrap' export const DEFAULT_GRID_SIZE = 24 export function isPlainObject(value: unknown): value is JsonObject { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } export function statesEqual(left: CaClass['states'] | undefined, right: CaClass['states'] | undefined) { if (left === undefined || right === undefined) return false if (Array.isArray(left) || Array.isArray(right)) { if (!Array.isArray(left) || !Array.isArray(right)) return false return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort()) } return left === right } export function supportsCaClass(runtime: { supported_classes?: CaClass[]; supportedClasses?: CaClass[] }, caClass: CaClass | undefined) { if (!caClass) return false const supportedClasses = runtime.supported_classes ?? runtime.supportedClasses ?? [] return supportedClasses.some( (supportedClass) => supportedClass.dimensions === caClass.dimensions && statesEqual(supportedClass.states, caClass.states) && neighborhoodsCompatible(supportedClass, caClass) ) } function neighborhoodsCompatible(supportedClass: CaClass, caClass: CaClass) { if (!supportedClass.neighborhoodId || !caClass.neighborhoodId) return true return supportedClass.neighborhoodId === caClass.neighborhoodId } export function getCaClassFromParams(params: JsonObject | undefined): CaClass | undefined { const caClass = params?.caClass if (!isPlainObject(caClass)) return undefined const dimensions = typeof caClass.dimensions === 'number' ? caClass.dimensions : undefined const states = caClass.states if (!dimensions || (typeof states !== 'number' && !Array.isArray(states))) return undefined return { neighborhoodId: typeof caClass.neighborhoodId === 'string' ? caClass.neighborhoodId : undefined, dimensions, states } } export function numericGridSize(value: unknown, dimensions: number, fallbackSize = DEFAULT_GRID_SIZE) { const grid = isPlainObject(value) ? value : {} const size = Array.isArray(grid.size) ? grid.size : [] const x = typeof size[0] === 'number' ? size[0] : fallbackSize const y = dimensions >= 2 && typeof size[1] === 'number' ? size[1] : 1 const z = dimensions >= 3 && typeof size[2] === 'number' ? size[2] : 1 return [x, y, z] as const } export function seededRandom(seed: string) { let state = 2166136261 for (let index = 0; index < seed.length; index += 1) { state ^= seed.charCodeAt(index) state = Math.imul(state, 16777619) } return () => { state += 0x6d2b79f5 let t = state t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } 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' } export function getBoundaryCondition(params: SceneParams | undefined): BoundaryCondition { return resolveBoundaryCondition(params?.simulation?.grid) } export function resolveBoundaryCondition(grid: JsonObject | undefined): BoundaryCondition { if (isBoundaryCondition(grid?.boundary)) return grid.boundary if (grid?.wrap === false) return 'fixed' return DEFAULT_BOUNDARY_CONDITION } export function wrapIndex(index: number, size: number) { return ((index % size) + size) % size } export function mirrorIndex(index: number, size: number) { if (size <= 1) return 0 const period = (size - 1) * 2 const wrapped = wrapIndex(index, period) return wrapped <= size - 1 ? wrapped : period - wrapped } export function resolveBoundaryIndex(index: number, size: number, boundary: BoundaryCondition) { if (size <= 0) return null if (boundary === 'wrap') return wrapIndex(index, size) if (boundary === 'mirror') return mirrorIndex(index, size) return index >= 0 && index < size ? index : null } export function cloneCells(cells: Cells): Cells { return cells.map((row) => [...row]) } export function assertCellsShape(cells: Cells, label = 'cells') { const width = cells[0]?.length ?? 0 for (const row of cells) { if (row.length !== width) throw new Error(`${label} must be rectangular`) for (const value of row) { if (typeof value !== 'boolean') throw new Error(`${label} must contain boolean cell states`) } } } export function assertSameCellsShape(reference: Cells, next: Cells, label = 'engine output') { assertCellsShape(reference, 'input cells') assertCellsShape(next, label) if (next.length !== reference.length || (next[0]?.length ?? 0) !== (reference[0]?.length ?? 0)) { throw new Error(`${label} must preserve the input grid dimensions`) } }