import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js' import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js' export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6 export type NagaSymbol = NagaArrow | null export type NagaPosition = readonly [number, number, number] export type NagaUniverse = Map export interface FollowResult { changes: number emits: number comment: string } export interface EvolveResult extends FollowResult {} interface NagaTopology { boundary: BoundaryCondition size: readonly [number, number, number] } export class Moykle { position: NagaPosition forward: NagaSymbol constructor(position: NagaPosition, arrow: NagaSymbol) { this.position = position this.forward = arrow } } export class Universe { readonly cells = new Map() get(position: NagaPosition) { return this.cells.get(addr(position))?.forward ?? null } set(moykle: Moykle) { this.cells.set(addr(moykle.position), moykle) } } function isArrow(value: unknown): value is NagaArrow { return value === 1 || value === 2 || value === 3 || value === 4 || value === 5 || value === 6 } function addPosition(left: NagaPosition, right: NagaPosition): NagaPosition { return [left[0] + right[0], left[1] + right[1], left[2] + right[2]] } function subtractPosition(left: NagaPosition, right: NagaPosition): NagaPosition { return [left[0] - right[0], left[1] - right[1], left[2] - right[2]] } function resolvePosition(position: NagaPosition, topology: NagaTopology): NagaPosition | null { const x = resolveBoundaryIndex(position[0], topology.size[0], topology.boundary) const y = resolveBoundaryIndex(position[1], topology.size[1], topology.boundary) const z = resolveBoundaryIndex(position[2], topology.size[2], topology.boundary) if (x === null || y === null || z === null) return null return [x, y, z] } function translatePosition(position: NagaPosition, offset: NagaPosition, topology: NagaTopology) { return resolvePosition(addPosition(position, offset), topology) } function reverseTranslatePosition(position: NagaPosition, offset: NagaPosition, topology: NagaTopology) { return resolvePosition(subtractPosition(position, offset), topology) } export function opposite(symbol: NagaArrow): NagaArrow { return (((symbol + 2) % 6) + 1) as NagaArrow } export function fVec(symbol: NagaSymbol | 0): NagaPosition | null { if (symbol === 1) return [1, 0, 0] if (symbol === 2) return [0, 1, 0] if (symbol === 3) return [0, 0, 1] if (symbol === 4) return [-1, 0, 0] if (symbol === 5) return [0, -1, 0] if (symbol === 6) return [0, 0, -1] return null } export function addr(position: NagaPosition) { return `${position[0]}_${position[1]}_${position[2]}` } export function isHead(universe: NagaUniverse, moykle: Moykle, _time: number, topology: NagaTopology) { let adjacentsThatPointToMoykle = 0 if (moykle.forward === null) return false for (let i = 1 as NagaArrow; i <= 6; i = (i + 1) as NagaArrow) { const offset = fVec(i) if (!offset) continue const adjacentPosition = translatePosition(moykle.position, offset, topology) if (!adjacentPosition) continue const adjacent = universe.get(addr(adjacentPosition)) if (!adjacent || adjacent.forward === null) continue if (adjacent.forward !== opposite(i) && i !== moykle.forward) { adjacentsThatPointToMoykle += 1 } } return adjacentsThatPointToMoykle === 0 } export function getAllHeads(universe: NagaUniverse, _time: number, topology: NagaTopology) { const heads: Moykle[] = [] for (const moykle of universe.values()) { let apprentice = 0 let master = 0 if (moykle.forward === null) continue for (let i = 1 as NagaArrow; i <= 6; i = (i + 1) as NagaArrow) { const offset = fVec(i) if (!offset) continue const adjacentPosition = translatePosition(moykle.position, offset, topology) if (!adjacentPosition) continue const adjacent = universe.get(addr(adjacentPosition)) if (!adjacent || adjacent.forward === null) continue if (adjacent.forward === opposite(i)) master += 1 if (i === moykle.forward) apprentice += 1 } if (apprentice <= 1 && master === 0) heads.push(moykle) } return heads } export function moykleNumber(universe: NagaUniverse, moykle: Moykle, _time: number, topology: NagaTopology) { let pointsToOccupied = 0 let pointedAtBy = 0 for (let i = 1 as NagaArrow; i <= 6; i = (i + 1) as NagaArrow) { const offset = fVec(i) if (!offset) continue const adjacentPosition = translatePosition(moykle.position, offset, topology) if (!adjacentPosition) continue const adjacent = universe.get(addr(adjacentPosition)) if (!adjacent || adjacent.forward === null) continue if (adjacent.forward === opposite(i)) pointedAtBy += 1 if (i === moykle.forward) pointsToOccupied += 1 } return [pointsToOccupied, pointedAtBy] as const } export function createNagaRandom(seed: string | number = 100) { return seededRandom(String(seed)) } export function evolve(universe: NagaUniverse, time: number, topology: NagaTopology, random = createNagaRandom()): EvolveResult { const headList = getAllHeads(universe, time, topology) if (headList.length === 0) throw new Error('Cannot evolve, no heads') const selected = headList[Math.floor(random() * headList.length)] ?? headList[0] return follow(universe, selected, time, topology, 0) } export function follow(universe: NagaUniverse, head: Moykle, time: number, topology: NagaTopology, _depth: number): FollowResult { let changes = 0 let emits = 0 let current = head let tail = head let tailSearching = true const tailVisited = new Set() while (tailSearching) { const currentAddress = addr(current.position) if (tailVisited.has(currentAddress)) { tailSearching = false tail = current break } tailVisited.add(currentAddress) if (current.forward === null) break const offset = fVec(current.forward) if (!offset) break const nextPosition = translatePosition(current.position, offset, topology) if (!nextPosition) break const next = universe.get(addr(nextPosition)) if (next && isArrow(next.forward)) { current = next tailSearching = true } else { tailSearching = false tail = current } } if (tail.forward === null) { return { changes, emits, comment: `T:${time} | Tail has no symbol to move.` } } const tailOffset = fVec(tail.forward) if (!tailOffset) { return { changes, emits, comment: `T:${time} | Tail symbol has no direction.` } } const placePosition = reverseTranslatePosition(head.position, tailOffset, topology) if (!placePosition) { return { changes, emits, comment: `T:${time} | Tail would move outside fixed boundary.` } } const placeAddress = addr(placePosition) if (!universe.has(placeAddress)) { universe.set(placeAddress, new Moykle(placePosition, null)) } const place = universe.get(placeAddress) if (!place) throw new Error(`Unable to create naga place cell at ${placeAddress}`) if (head.forward === opposite(tail.forward)) { let comment = `T:${time} | Emission! ${place.position.join(',')} implied contradiction.` let kinkSearching = true current = universe.get(addr(head.position)) ?? head const kinkVisited = new Set() while (kinkSearching) { const currentAddress = addr(current.position) if (kinkVisited.has(currentAddress)) break kinkVisited.add(currentAddress) if (current.forward === null) break const currentOffset = fVec(current.forward) if (!currentOffset) break const nextPosition = translatePosition(current.position, currentOffset, topology) if (!nextPosition) break const next = universe.get(addr(nextPosition)) if (next && isArrow(next.forward)) { current = next const kinkPlacePosition = reverseTranslatePosition(current.position, tailOffset, topology) if (!kinkPlacePosition) { kinkSearching = false break } const kinkPlace = universe.get(addr(kinkPlacePosition)) kinkSearching = Boolean(kinkPlace && isArrow(kinkPlace.forward)) } else { kinkSearching = false } } const kinkPlacePosition = reverseTranslatePosition(current.position, tailOffset, topology) if (!kinkPlacePosition) { return { changes, emits, comment: `T:${time} | Kink would move outside fixed boundary.` } } const kinkPlaceAddress = addr(kinkPlacePosition) if (!universe.has(kinkPlaceAddress)) { universe.set(kinkPlaceAddress, new Moykle(kinkPlacePosition, null)) } const kinkPlace = universe.get(kinkPlaceAddress) if (!kinkPlace) throw new Error(`Unable to create naga kink place cell at ${kinkPlaceAddress}`) const temp = kinkPlace.forward kinkPlace.forward = tail.forward tail.forward = temp emits += 1 changes += 1 comment += ` Kink at ${kinkPlacePosition.join(',')}, swapped ${kinkPlace.forward} for ${temp}.` return { changes, emits, comment } } const comment = `T:${time} | Take tail symbol ${tail.forward} from ${tail.position.join(',')}, put into ${place.position.join(',')} in exchange for ${place.forward}` const temp = place.forward place.forward = tail.forward tail.forward = temp changes += 1 return { changes, emits, comment } } function gridSize(settings: SceneParams, cells: Cells) { const size = settings.simulation?.grid?.size const width = Array.isArray(size) && typeof size[0] === 'number' ? size[0] : cells[0]?.length ?? 24 const height = Array.isArray(size) && typeof size[1] === 'number' ? size[1] : cells.length || 24 const depth = Array.isArray(size) && typeof size[2] === 'number' ? size[2] : 24 return [Math.max(1, width), Math.max(1, height), Math.max(1, depth)] as const } function coordinateSymbol(coordinate: number[]) { const state = coordinate[3] ?? 1 return isArrow(state) ? state : 1 } function universeFromCoordinates(coordinates: number[][] | undefined) { const universe: NagaUniverse = new Map() for (const coordinate of coordinates ?? []) { const [x, y, z = 0] = coordinate if (!Number.isInteger(x) || !Number.isInteger(y) || !Number.isInteger(z)) continue const position: NagaPosition = [x, y, z] universe.set(addr(position), new Moykle(position, coordinateSymbol(coordinate))) } return universe } function seedUniverseFromCells(cells: Cells) { const universe: NagaUniverse = new Map() for (let y = 0; y < cells.length; y += 1) { for (let x = 0; x < (cells[y]?.length ?? 0); x += 1) { if (!cells[y][x]) continue const position: NagaPosition = [x, y, 0] universe.set(addr(position), new Moykle(position, 1)) } } return universe } function coordinatesFromUniverse(universe: NagaUniverse, size: readonly [number, number, number]) { const coordinates: number[][] = [] for (const moykle of universe.values()) { const [x, y, z] = moykle.position if (moykle.forward === null) continue if (x < 0 || y < 0 || z < 0 || x >= size[0] || y >= size[1] || z >= size[2]) continue coordinates.push([x, y, z, moykle.forward]) } return coordinates } function projectCoordinatesToCells(coordinates: number[][], cells: Cells) { const next = cells.map((row) => row.map(() => false)) for (const coordinate of coordinates) { const [x, y] = coordinate if (!Number.isInteger(x) || !Number.isInteger(y)) continue if (y < 0 || x < 0 || y >= next.length || x >= (next[y]?.length ?? 0)) continue next[y][x] = true } return next } function ensureSimulation(settings: SceneParams) { settings.simulation ??= {} settings.simulation.initialCondition ??= {} return settings.simulation as SceneParams['simulation'] & JsonObject } export function stepNaga3d(cells: Cells, settings: SceneParams) { const simulation = ensureSimulation(settings) const size = gridSize(settings, cells) const coordinates = simulation.initialCondition?.cells const universe = Array.isArray(coordinates) && coordinates.length > 0 ? universeFromCoordinates(coordinates) : seedUniverseFromCells(cells) const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0 const seed = typeof simulation.seed === 'string' ? simulation.seed : 'naga' const topology: NagaTopology = { boundary: resolveBoundaryCondition(simulation.grid), size } try { evolve(universe, tick, topology, createNagaRandom(`${seed}:${tick}`)) } catch (error) { if (!(error instanceof Error) || error.message !== 'Cannot evolve, no heads') throw error } const nextCoordinates = coordinatesFromUniverse(universe, size) simulation.initialCondition = { ...simulation.initialCondition, type: 'cells', cells: nextCoordinates } simulation.nagaTick = tick + 1 return projectCoordinatesToCells(nextCoordinates, cells) } export const Addr = addr export const Evolve = evolve export const Follow = follow export const FVec = fVec export const Moykle_number = moykleNumber export const Opposite = opposite