|
|
|
@@ -0,0 +1,420 @@
|
|
|
|
|
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
|