Files
glitch_automata_lab/backend/admin/src/caEngineSteps/naga.ts
T

421 lines
13 KiB
TypeScript
Raw Normal View History

2026-07-06 13:11:48 +02:00
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<string, Moykle>
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<string, Moykle>()
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<string>()
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<string>()
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