@@ -26,7 +26,20 @@ export const RULE_FAMILIES = ['life', 'parity', 'cyclic', 'majority'];
2626// - "diffusion": next state = round(mean(self + neighbors)).
2727// - "sheetflow": anyonic-style; uses (sheet index of neighbors) mod n.
2828// - "xor": binary; next state = XOR of all neighbor states.
29- export const ALL_RULE_FAMILIES = [ ...RULE_FAMILIES , 'threshold' , 'diffusion' , 'sheetflow' , 'xor' ] ;
29+ // - "langton": Langton's Ant. One or more "ants" walk the lattice; at
30+ // each step an ant turns based on the current cell state,
31+ // flips that cell (cycling through numStates), then moves
32+ // forward to a neighbor. On an n-neighbor tile, "turning"
33+ // means choosing the next/previous edge slot relative to
34+ // the ant's incoming heading.
35+ export const ALL_RULE_FAMILIES = [
36+ ...RULE_FAMILIES ,
37+ 'threshold' ,
38+ 'diffusion' ,
39+ 'sheetflow' ,
40+ 'xor' ,
41+ 'langton' ,
42+ ] ;
3043
3144// Parse a "B/S" string like "B3/S23" into two Sets. For pentagons the
3245// counts live in {0,1,2,3,4,5}.
@@ -52,6 +65,52 @@ export function lifeRuleToString(birth, survive) {
5265 const s = [ ...survive ] . sort ( ( a , b ) => a - b ) . join ( '' ) ;
5366 return `B${ b } /S${ s } ` ;
5467}
68+ // Distinct colours used to render multiple Langton ants.
69+ export const ANT_COLORS = [
70+ '#ff4d6d' ,
71+ '#4dd2ff' ,
72+ '#ffd24d' ,
73+ '#7ee787' ,
74+ '#c792ea' ,
75+ '#ff9e64' ,
76+ '#56b6c2' ,
77+ '#e06c75' ,
78+ ] ;
79+ // Parse a turn ruleset string like "RL", "LLRR", or "RNL" into an array of
80+ // turn operations. Each character maps to a relative rotation and whether
81+ // the ant moves forward this step:
82+ // R = turn right (+1) L = turn left (-1)
83+ // U = reverse (≈180°) N = no turn / straight (0)
84+ // S = stay (don't move) digits 0-9 = explicit rotation amount
85+ export function parseTurnString ( str ) {
86+ const ops = [ ] ;
87+ if ( ! str ) str = 'RL' ;
88+ for ( const ch of str . toUpperCase ( ) ) {
89+ switch ( ch ) {
90+ case 'R' :
91+ ops . push ( { turn : 1 , move : true } ) ;
92+ break ;
93+ case 'L' :
94+ ops . push ( { turn : - 1 , move : true } ) ;
95+ break ;
96+ case 'U' :
97+ ops . push ( { turn : 'reverse' , move : true } ) ;
98+ break ;
99+ case 'N' :
100+ ops . push ( { turn : 0 , move : true } ) ;
101+ break ;
102+ case 'S' :
103+ ops . push ( { turn : 0 , move : false } ) ;
104+ break ;
105+ default : {
106+ const d = parseInt ( ch , 10 ) ;
107+ if ( ! Number . isNaN ( d ) ) ops . push ( { turn : d , move : true } ) ;
108+ }
109+ }
110+ }
111+ if ( ops . length === 0 ) ops . push ( { turn : 1 , move : true } , { turn : - 1 , move : true } ) ;
112+ return ops ;
113+ }
55114
56115export class CA {
57116 constructor ( lattice , opts = { } ) {
@@ -67,6 +126,13 @@ export class CA {
67126 this . state = new Uint8Array ( n ) ;
68127 this . next = new Uint8Array ( n ) ;
69128 this . generation = 0 ;
129+ // Langton's Ant state. Each ant: { tile, edge } where `edge` is the
130+ // raw neighbor slot index the ant is currently heading toward (its
131+ // "facing" direction). Ants are seeded lazily on first step / seeding.
132+ this . ants = [ ] ;
133+ // Programmable turmite ruleset + chirality flag.
134+ this . turnOps = parseTurnString ( opts . turnString ?? 'RL' ) ;
135+ this . antMirror = opts . antMirror ?? false ;
70136 }
71137
72138 setNumStates ( n ) {
@@ -91,10 +157,28 @@ export class CA {
91157 setThreshold ( t ) {
92158 this . threshold = Math . max ( 0 , Math . min ( 5 , t | 0 ) ) ;
93159 }
160+ // Set the Langton/turmite turn ruleset. The number of colours the ant
161+ // cycles through is the length of the ruleset, so grow numStates to match
162+ // (so each colour has a defined turn op and the renderer shows them all).
163+ setTurnString ( str ) {
164+ this . turnOps = parseTurnString ( str ) ;
165+ const need = this . turnOps . length ;
166+ if ( need > this . numStates ) this . setNumStates ( need ) ;
167+ }
168+ // Toggle chirality (mirror all turns left<->right).
169+ setAntMirror ( on ) {
170+ this . antMirror = ! ! on ;
171+ }
172+ // Number of colours an ant cycles a cell through. Bound below by the
173+ // ruleset length (one op per colour) and by 2.
174+ antStateCount ( ) {
175+ return Math . max ( 2 , Math . max ( this . turnOps . length , this . numStates ) ) ;
176+ }
94177
95178 clear ( ) {
96179 this . state . fill ( 0 ) ;
97180 this . generation = 0 ;
181+ this . ants = [ ] ;
98182 }
99183
100184 randomize ( density = 0.3 , seed = null ) {
@@ -123,6 +207,9 @@ export class CA {
123207 if ( tileIdx >= 0 && tileIdx < this . state . length ) {
124208 this . state [ tileIdx ] = value % this . numStates ;
125209 }
210+ // For Langton's Ant, the "seed" is the ant's starting position rather
211+ // than a live cell. Place a single ant at tileIdx facing edge slot 0.
212+ this . placeAnt ( tileIdx , 0 ) ;
126213 }
127214 // Seed a named shape centered on tileIdx (default origin).
128215 seedShape ( shape , tileIdx = 0 ) {
@@ -220,6 +307,75 @@ export class CA {
220307 // For binary-feeling editing: cycle through states 0..numStates-1.
221308 this . state [ idx ] = ( this . state [ idx ] + 1 ) % this . numStates ;
222309 }
310+ // ── Langton's Ant helpers ──────────────────────────────────────────────
311+ // Place (or replace) a single ant. Use addAnt() to keep multiple.
312+ placeAnt ( tileIdx , edge = 0 ) {
313+ if ( tileIdx < 0 || tileIdx >= this . state . length ) {
314+ this . ants = [ ] ;
315+ return ;
316+ }
317+ this . ants = [
318+ {
319+ tile : tileIdx ,
320+ edge : this . firstValidEdge ( tileIdx , edge ) ,
321+ id : 0 ,
322+ color : ANT_COLORS [ 0 ] ,
323+ steps : 0 ,
324+ } ,
325+ ] ;
326+ }
327+ addAnt ( tileIdx , edge = 0 ) {
328+ if ( tileIdx < 0 || tileIdx >= this . state . length ) return ;
329+ const id = this . ants . length ;
330+ this . ants . push ( {
331+ tile : tileIdx ,
332+ edge : this . firstValidEdge ( tileIdx , edge ) ,
333+ id,
334+ color : ANT_COLORS [ id % ANT_COLORS . length ] ,
335+ steps : 0 ,
336+ } ) ;
337+ }
338+ // Seed a small swarm of ants arranged around a tile, each facing a
339+ // different edge so they fan out into distinct patterns.
340+ placeAntSwarm ( tileIdx , count = 4 ) {
341+ this . clear ( ) ;
342+ const t = this . lattice . tiles [ tileIdx ] ;
343+ if ( ! t ) return ;
344+ const nE = t . neighbors . length ;
345+ this . ants = [ ] ;
346+ const k = Math . max ( 1 , Math . min ( count , 8 ) ) ;
347+ for ( let i = 0 ; i < k ; i ++ ) {
348+ const id = i ;
349+ this . ants . push ( {
350+ tile : tileIdx ,
351+ edge : this . firstValidEdge ( tileIdx , Math . round ( ( i * nE ) / k ) ) ,
352+ id,
353+ color : ANT_COLORS [ id % ANT_COLORS . length ] ,
354+ steps : 0 ,
355+ } ) ;
356+ }
357+ }
358+ // Return true if a Langton ant currently occupies this tile.
359+ antOn ( tileIdx ) {
360+ for ( let i = 0 ; i < this . ants . length ; i ++ ) {
361+ if ( this . ants [ i ] . tile === tileIdx ) return true ;
362+ }
363+ return false ;
364+ }
365+ // Find an edge slot at `tile` that has a real neighbor, starting the
366+ // search at `preferred` and wrapping around. Respects activeEdges
367+ // (e.g. pinwheel inactive hypotenuse) when present.
368+ firstValidEdge ( tile , preferred = 0 ) {
369+ const t = this . lattice . tiles [ tile ] ;
370+ const nb = t . neighbors ;
371+ const nE = nb . length ;
372+ for ( let s = 0 ; s < nE ; s ++ ) {
373+ const e = ( ( ( preferred + s ) % nE ) + nE ) % nE ;
374+ if ( t . activeEdges && ! t . activeEdges [ e ] ) continue ;
375+ if ( nb [ e ] !== null ) return e ;
376+ }
377+ return preferred ; // no valid edge; ant will be a no-op
378+ }
223379
224380 step ( ) {
225381 const tiles = this . lattice . tiles ;
@@ -228,6 +384,13 @@ export class CA {
228384 const ns = this . numStates ;
229385 // Number of edges/neighbours varies by polygon type.
230386 const nEdges = ( i ) => tiles [ i ] . neighbors . length ;
387+ // Langton's Ant is an agent-based rule: it mutates `state` in place and
388+ // advances ants, so it doesn't use the double-buffered totalistic path.
389+ if ( this . family === 'langton' ) {
390+ this . stepLangton ( ) ;
391+ this . generation ++ ;
392+ return ;
393+ }
231394 switch ( this . family ) {
232395 case 'life' : {
233396 const { birth, survive } = this . lifeRule ;
@@ -363,6 +526,88 @@ export class CA {
363526 this . next = tmp ;
364527 this . generation ++ ;
365528 }
529+ // One generation of Langton's Ant(s).
530+ //
531+ // Programmable turmite rule (generalised to n-neighbour tiles):
532+ // 1. Read the current cell colour `c` under the ant.
533+ // 2. Look up the turn op for colour `c` in the turn string. The op
534+ // specifies a relative rotation (R=+1, L=-1, U=reverse, N=straight,
535+ // S=stay, digits=explicit) and whether the ant moves this step.
536+ // 3. Flip the cell colour: c -> (c + 1) mod antStateCount().
537+ // 4. Move forward one tile along the new heading (unless op.move is
538+ // false). If that edge has no neighbour, the ant rotates until it
539+ // finds a valid edge (it never leaves the lattice).
540+ //
541+ // All ants are advanced from a snapshot of headings so order within a
542+ // single generation is consistent.
543+ stepLangton ( ) {
544+ if ( this . ants . length === 0 ) return ;
545+ const tiles = this . lattice . tiles ;
546+ const ops = this . turnOps ;
547+ const cycle = this . antStateCount ( ) ;
548+ const moves = [ ] ;
549+ for ( let i = 0 ; i < this . ants . length ; i ++ ) {
550+ const ant = this . ants [ i ] ;
551+ const tile = ant . tile ;
552+ const t = tiles [ tile ] ;
553+ const nb = t . neighbors ;
554+ const nE = nb . length ;
555+ const c = this . state [ tile ] | 0 ;
556+ // Look up the programmable op for this colour (wrap into range).
557+ const op = ops [ ( ( c % ops . length ) + ops . length ) % ops . length ] ;
558+ // Resolve the relative turn into an integer edge rotation.
559+ let turn ;
560+ if ( op . turn === 'reverse' ) {
561+ turn = Math . floor ( nE / 2 ) ;
562+ } else {
563+ turn = op . turn | 0 ;
564+ }
565+ if ( this . antMirror ) turn = - turn ;
566+ // New heading = current edge rotated by `turn`, then snapped to the
567+ // nearest valid (in-lattice, active) edge.
568+ let heading = ( ( ( ant . edge + turn ) % nE ) + nE ) % nE ;
569+ heading = this . firstValidEdge ( tile , heading ) ;
570+ // Flip the cell colour through the rule's colour cycle.
571+ this . state [ tile ] = ( c + 1 ) % cycle ;
572+ ant . steps = ( ant . steps || 0 ) + 1 ;
573+ // If this op says "stay", don't move — just keep the new heading.
574+ if ( op . move === false ) {
575+ moves . push ( { ...ant , tile, edge : heading } ) ;
576+ continue ;
577+ }
578+ // Move forward.
579+ const dest = nb [ heading ] ;
580+ if ( dest === null || dest === undefined ) {
581+ // Stuck: stay put but keep the new heading.
582+ moves . push ( { ...ant , tile, edge : heading } ) ;
583+ continue ;
584+ }
585+ // After arriving at `dest`, choose an incoming heading for the next
586+ // step. We pick the edge slot on `dest` that points back where we
587+ // came from, so "forward" continues roughly straight. Find the slot
588+ // on dest whose neighbour is the tile we just left, then that is the
589+ // ant's "behind"; facing is the opposite-ish slot. Simplest stable
590+ // choice: use the matching back-edge as the new edge baseline.
591+ const dt = tiles [ dest ] ;
592+ const dnb = dt . neighbors ;
593+ let backEdge = 0 ;
594+ for ( let k = 0 ; k < dnb . length ; k ++ ) {
595+ if ( dnb [ k ] === tile ) {
596+ backEdge = k ;
597+ break ;
598+ }
599+ }
600+ // Face roughly forward by stepping past the back-edge (opposite-ish
601+ // slot) so straight-ahead motion continues across the new tile.
602+ const dnE = dnb . length ;
603+ const forwardEdge = this . firstValidEdge (
604+ dest ,
605+ ( ( ( backEdge + Math . floor ( dnE / 2 ) ) % dnE ) + dnE ) % dnE
606+ ) ;
607+ moves . push ( { ...ant , tile : dest , edge : forwardEdge } ) ;
608+ }
609+ this . ants = moves ;
610+ }
366611
367612 // Diagnostics
368613 population ( ) {
@@ -388,4 +633,8 @@ export class CA {
388633 }
389634 return by ;
390635 }
636+ // Expose the per-ant colour list (used by the renderer).
637+ antColors ( ) {
638+ return ANT_COLORS ;
639+ }
391640}
0 commit comments