129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
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 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`)
|
||
|
|
}
|
||
|
|
}
|