Skip to content

Commit b22702e

Browse files
committed
wip
1 parent bf58206 commit b22702e

18 files changed

Lines changed: 6748 additions & 4916 deletions

File tree

essays/TEL/alternate.html

Lines changed: 832 additions & 768 deletions
Large diffs are not rendered by default.

essays/TEL/index.html

Lines changed: 1294 additions & 1245 deletions
Large diffs are not rendered by default.

essays/TEL/main.css

Lines changed: 1966 additions & 1848 deletions
Large diffs are not rendered by default.

essays/TEL/main.js

Lines changed: 801 additions & 799 deletions
Large diffs are not rendered by default.

essays/TEL/style.css

Lines changed: 454 additions & 218 deletions
Large diffs are not rendered by default.

experiments/Pentagon_Lattice_Geometry/ca.js

Lines changed: 250 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

56115
export 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
}

experiments/Pentagon_Lattice_Geometry/index.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ <h1>Multi-Sheeted Pentagon Lattice Lab</h1>
267267
<option value="diffusion">Diffusion average</option>
268268
<option value="sheetflow">Sheet-flow (anyonic)</option>
269269
<option value="xor">XOR neighbors</option>
270+
<option value="langton">Langton's Ant</option>
270271
</select>
271272
</label>
272273
<label id="caLifeRuleLabel">
@@ -306,6 +307,52 @@ <h1>Multi-Sheeted Pentagon Lattice Lab</h1>
306307
<input id="caThreshold" max="5" min="1" type="range" value="1" />
307308
<span class="rangeval" id="caThresholdVal">1</span>
308309
</label>
310+
<div id="caLangtonBlock" style="display: none">
311+
<label>
312+
Turn ruleset:
313+
<input id="caTurnString" style="width: 120px" type="text" value="RL" />
314+
</label>
315+
<label>
316+
Preset turmite:
317+
<select id="caTurmitePreset">
318+
<option value="">— custom —</option>
319+
<option selected value="RL">RL (classic Langton)</option>
320+
<option value="LR">LR (mirror Langton)</option>
321+
<option value="RLR">RLR (chaotic)</option>
322+
<option value="LLRR">LLRR (symmetric growth)</option>
323+
<option value="RRLL">RRLL</option>
324+
<option value="LRRL">LRRL</option>
325+
<option value="RLLR">RLLR</option>
326+
<option value="RRLR">RRLR</option>
327+
<option value="LLRRL">LLRRL</option>
328+
<option value="RRLLR">RRLLR</option>
329+
<option value="LRRRRRLLR">LRRRRRLLR (highway)</option>
330+
<option value="RNL">RNL (with straight)</option>
331+
<option value="RUL">RUL (with reverse)</option>
332+
<option value="RSL">RSL (with stay)</option>
333+
<option value="RLLRLLRRRL">RLLRLLRRRL (snowflake)</option>
334+
</select>
335+
</label>
336+
<label style="display: flex; align-items: center; gap: 6px">
337+
<input id="caAntMirror" type="checkbox" />
338+
Mirror turns (chirality flip)
339+
</label>
340+
<div style="margin-top: 6px">
341+
<button id="caAntSwarm">Seed ant swarm</button>
342+
<input
343+
id="caAntCount"
344+
max="8"
345+
min="1"
346+
style="width: 50px"
347+
title="Number of ants in swarm"
348+
type="number"
349+
value="4"
350+
/>
351+
</div>
352+
<div class="alg-block" id="caAntStats" style="margin-top: 6px; font-size: 11px">
353+
ants: <span id="caAntInfo"></span>
354+
</div>
355+
</div>
309356
<div style="margin-top: 8px">
310357
<button id="caPlay">▶ Play</button>
311358
<button id="caStep">Step</button>

0 commit comments

Comments
 (0)