<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>unit — a software nanobot</title>
<meta name="description" content="A self-replicating software nanobot. Forth brain, S-expression mesh protocol, genetic programming, distributed computation, and persistence — all in one binary. Spawn units, evolve code, distribute goals, hibernate and resurrect. Try the live demo or cargo install unit.">
<meta property="og:title" content="unit — a software nanobot">
<meta property="og:description" content="Self-replicating software nanobots that think in Forth, speak S-expressions, evolve solutions to challenges, and distribute computation across a mesh. Immune system, metabolic energy, open-ended evolution. Try the live demo.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://davidcanhelp.github.io/unit/">
<meta property="og:image" content="https://raw.githubusercontent.com/DavidCanHelp/unit/main/unit-demo.gif">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="unit — a software nanobot">
<meta name="twitter:description" content="Self-replicating software nanobots with an immune system, metabolic energy, and open-ended evolution. Forth brain, S-expression mesh, genetic programming.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect fill='%230a0a0a' width='100' height='100' rx='12'/><text x='50' y='72' font-size='64' font-family='monospace' fill='%2300ff88' text-anchor='middle'>u</text></svg>">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0a; color: #00ff88; font-family: 'Courier New', monospace; font-size: 14px; height: 100vh; display: flex; flex-direction: column; }
a { color: #00ff88; text-decoration: none; } a:hover { text-decoration: underline; }
#info { padding: 10px 14px 8px; border-bottom: 1px solid #1a1a1a; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 6px; }
#info-left { display: flex; align-items: center; gap: 12px; }
.title { color: #00ff88; font-weight: bold; } .desc { color: #555; font-size: 12px; }
#info-right { display: flex; gap: 8px; font-size: 11px; color: #444; align-items: center; }
.mode-btn { background: #111; border: 1px solid #222; color: #555; font-family: inherit; font-size: 11px; padding: 2px 8px; border-radius: 3px; cursor: pointer; }
.mode-btn:hover { color: #00ff88; border-color: #444; }
.mode-btn.active { color: #00ff88; border-color: #0a4a0a; }
#viz { display: none; border-bottom: 1px solid #111; position: relative; }
#viz canvas { display: block; width: 100%; }
#viz-label { position: absolute; bottom: 4px; right: 8px; font-size: 9px; color: #444; }
#chatter { max-height: 80px; overflow-y: auto; padding: 2px 14px; font-size: 10px; color: #445; border-bottom: 1px solid #111; display: none; }
#terminal { flex: 1; overflow-y: auto; padding: 12px; white-space: pre-wrap; word-wrap: break-word; font-size: 13px; line-height: 1.4; }
#tutorial-bar { padding: 6px 14px; border-top: 1px solid #1a2a1a; border-left: 3px solid #0a4a0a; background: #0a0f0a; font-size: 12px; color: #4a7a4a; display: flex; justify-content: space-between; align-items: center; min-height: 28px; }
#tutorial-bar .step { color: #2a5a2a; font-size: 10px; }
#tutorial-bar .cmd { color: #00ff88; background: #0d1a0d; padding: 1px 6px; border-radius: 2px; cursor: pointer; border: 1px solid #1a3a1a; }
#tutorial-bar .cmd:hover { background: #0a2a0a; }
#tutorial-bar .skip { color: #333; cursor: pointer; font-size: 10px; margin-left: 12px; }
#tutorial-bar .skip:hover { color: #666; }
#tutorial-bar .links a { margin: 0 6px; font-size: 11px; }
#hints { padding: 4px 14px 6px; border-top: 1px solid #111; font-size: 11px; color: #333; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.hint { color: #2a6; cursor: pointer; background: #0d0d0d; padding: 1px 5px; border-radius: 2px; border: 1px solid #1a1a1a; }
.hint:hover { color: #00ff88; border-color: #333; }
#input-line { display: flex; padding: 8px 14px; background: #0d0d0d; border-top: 1px solid #222; }
#prompt { color: #00ff88; margin-right: 8px; line-height: 24px; }
#input { flex: 1; background: transparent; border: none; outline: none; color: #00ff88; font-family: inherit; font-size: 14px; caret-color: #00ff88; }
.output { color: #bbb; } .error { color: #ff4444; } .info { color: #555; } .mesh-event { color: #886; } .tutorial-msg { color: #3a6a3a; font-style: italic; }
</style>
</head>
<body>
<div id="info">
<div id="info-left">
<span class="title">unit</span>
<span class="desc">A self-replicating software nanobot.</span>
</div>
<div id="info-right">
<button class="mode-btn" id="spawn-btn" onclick="doSpawn()">spawn (1/5)</button>
<button class="mode-btn" id="viz-toggle" onclick="cycleVizMode()">mesh</button>
<button class="mode-btn" id="mute-btn" onclick="toggleMute()">chatter</button>
<a href="https://github.com/DavidCanHelp/unit">GitHub</a>
<a href="https://crates.io/crates/unit">crates.io</a>
</div>
</div>
<div id="viz"><canvas id="viz-canvas"></canvas><span id="viz-label"></span></div>
<div id="chatter"></div>
<div id="terminal" onclick="document.getElementById('input').focus()"></div>
<div id="tutorial-bar"></div>
<div id="hints"></div>
<div id="input-line">
<span id="prompt">></span>
<input id="input" type="text" autofocus autocomplete="off" spellcheck="false">
</div>
<script src="unit.js"></script>
<script>
const terminal = document.getElementById('terminal');
const input = document.getElementById('input');
const tutorialBar = document.getElementById('tutorial-bar');
const hintsBar = document.getElementById('hints');
const vizDiv = document.getElementById('viz');
const vizCanvas = document.getElementById('viz-canvas');
const vizLabel = document.getElementById('viz-label');
const vizToggle = document.getElementById('viz-toggle');
const spawnBtn = document.getElementById('spawn-btn');
const muteBtn = document.getElementById('mute-btn');
const chatterDiv = document.getElementById('chatter');
const ctx = vizCanvas.getContext('2d');
let vm = null, mesh = null, history = [], histIdx = -1;
let chatterOn = true;
function toggleMute() {
chatterOn = !chatterOn;
muteBtn.className = chatterOn ? 'mode-btn active' : 'mode-btn';
}
// =========================================================================
// Visualizer
// =========================================================================
let vizMode = 1;
let vizNodes = [], vizEdges = [], vizParticles = [];
// Speech bubbles: { id, text, age }
let vizBubbles = [];
function cycleVizMode() {
vizMode = (vizMode + 1) % 3;
applyVizMode();
}
function applyVizMode() {
vizToggle.className = vizMode > 0 ? 'mode-btn active' : 'mode-btn';
if (vizMode === 0) { vizDiv.style.display = 'none'; chatterDiv.style.display = 'none'; terminal.style.flex = '1'; }
else if (vizMode === 1) { vizDiv.style.display = 'block'; vizDiv.style.height = '40vh'; chatterDiv.style.display = 'block'; terminal.style.flex = '1'; }
else { vizDiv.style.display = 'block'; vizDiv.style.height = 'calc(100vh - 120px)'; chatterDiv.style.display = 'block'; terminal.style.flex = '0'; terminal.style.height = '0'; terminal.style.overflow = 'hidden'; }
if (vizMode < 2) { terminal.style.flex = '1'; terminal.style.height = ''; terminal.style.overflow = ''; }
resizeCanvas();
}
function resizeCanvas() {
const r = vizDiv.getBoundingClientRect();
vizCanvas.width = r.width * devicePixelRatio;
vizCanvas.height = r.height * devicePixelRatio;
vizCanvas.style.height = r.height + 'px';
ctx.scale(devicePixelRatio, devicePixelRatio);
}
function moodColor(fitness) {
if (fitness > 50) return '#00ff88';
if (fitness > 20) return '#00ccff';
if (fitness > 0) return '#ccaa00';
return '#883333';
}
function syncVizFromMesh() {
if (!mesh) return;
const st = mesh.status();
for (const u of st.units) {
let n = vizNodes.find(n => n.id === u.id);
if (!n) {
const W = vizCanvas.width/devicePixelRatio||400, H = vizCanvas.height/devicePixelRatio||300;
const isFirst = vizNodes.length === 0;
n = {
id: u.id, x: W/2+(Math.random()-0.5)*100, y: H/2+(Math.random()-0.5)*80,
vx: 0, vy: 0, fitness: u.fitness, color: isFirst ? '#00ff88' : '#00ccff',
alpha: 0, pulse: 1.5, label: isFirst ? u.id + ' self' : u.id, glowColor: moodColor(u.fitness)
};
vizNodes.push(n);
if (!isFirst) vizEdges.push({ from: vizNodes[0].id, to: u.id });
} else {
n.fitness = u.fitness;
n.glowColor = moodColor(u.fitness);
}
}
vizLabel.textContent = `${st.count} unit${st.count>1?'s':''} | browser mesh`;
}
function setBubble(unitId, text, color) {
const existing = vizBubbles.find(b => b.id === unitId);
const short = text.split('\n')[0].substring(0, 35);
if (existing) { existing.text = short; existing.age = 0; existing.color = color || '#6a8'; }
else vizBubbles.push({ id: unitId, text: short, age: 0, color: color || '#6a8' });
}
function toSexp(unitId, text) {
const short = text.split('\n')[0].trim().substring(0, 50);
return `(say :from "#${unitId}" :msg "${short.replace(/"/g, '\\"')}")`;
}
function addChatter(unitId, text) {
if (!chatterOn) return;
const line = document.createElement('div');
line.textContent = toSexp(unitId, text);
chatterDiv.appendChild(line);
while (chatterDiv.childNodes.length > 8) chatterDiv.removeChild(chatterDiv.firstChild);
chatterDiv.scrollTop = chatterDiv.scrollHeight;
}
function emitParticle(fromId, toId, color) {
const from = vizNodes.find(n => n.id === fromId);
const to = vizNodes.find(n => n.id === toId);
if (!from || !to) return;
vizParticles.push({ x: from.x, y: from.y, tx: to.x, ty: to.y, age: 0, color });
from.pulse = 1.5;
}
function vizTick() {
if (vizMode === 0) { requestAnimationFrame(vizTick); return; }
syncVizFromMesh();
const W = vizCanvas.width/devicePixelRatio||400, H = vizCanvas.height/devicePixelRatio||300;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0a0a14'; ctx.fillRect(0, 0, W, H);
// Physics
for (const n of vizNodes) {
n.vx *= 0.95; n.vy *= 0.95;
n.vx += (W/2-n.x)*0.001; n.vy += (H/2-n.y)*0.001;
for (const m of vizNodes) {
if (m === n) continue;
const dx = n.x-m.x, dy = n.y-m.y, dist = Math.sqrt(dx*dx+dy*dy)||1;
if (dist < 120) { const f=(120-dist)*0.003; n.vx+=dx/dist*f; n.vy+=dy/dist*f; }
}
n.x += n.vx + (Math.random()-0.5)*0.3; n.y += n.vy + (Math.random()-0.5)*0.3;
n.x = Math.max(40, Math.min(W-40, n.x)); n.y = Math.max(40, Math.min(H-40, n.y));
if (n.alpha < 1) n.alpha = Math.min(1, n.alpha + 0.02);
if (n.pulse > 1) n.pulse -= 0.02;
}
// Edges
for (const e of vizEdges) {
const a = vizNodes.find(n=>n.id===e.from), b = vizNodes.find(n=>n.id===e.to);
if (!a||!b) continue;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
ctx.strokeStyle = `rgba(0,180,120,${0.08+Math.sin(Date.now()/1000)*0.04})`;
ctx.lineWidth = 1; ctx.stroke();
}
// Particles
for (let i = vizParticles.length-1; i >= 0; i--) {
const p = vizParticles[i]; p.age += 0.02;
p.x += (p.tx-p.x)*0.04; p.y += (p.ty-p.y)*0.04;
const alpha = Math.max(0, 1-p.age);
if (alpha <= 0) { vizParticles.splice(i,1); continue; }
ctx.beginPath(); ctx.arc(p.x,p.y,3,0,Math.PI*2);
ctx.shadowColor = p.color; ctx.shadowBlur = 8;
const gc2 = ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,5);
gc2.addColorStop(0, p.color); gc2.addColorStop(1, 'transparent');
ctx.fillStyle = gc2; ctx.fill(); ctx.shadowBlur = 0;
}
// Nodes
for (const n of vizNodes) {
const r = 8 + Math.min(n.fitness, 80)*0.2, pr = r * n.pulse;
const gc = ctx.createRadialGradient(n.x,n.y,pr,n.x,n.y,pr+12);
gc.addColorStop(0, (n.glowColor||n.color)+'33'); gc.addColorStop(1, 'transparent');
ctx.beginPath(); ctx.arc(n.x,n.y,pr+8,0,Math.PI*2); ctx.fillStyle = gc; ctx.fill();
ctx.beginPath(); ctx.arc(n.x,n.y,pr,0,Math.PI*2);
ctx.fillStyle = n.color + Math.floor(n.alpha*200).toString(16).padStart(2,'0');
ctx.fill(); ctx.strokeStyle = n.color+'44'; ctx.lineWidth=1; ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '9px monospace'; ctx.textAlign = 'center';
ctx.fillText(n.label, n.x, n.y+pr+12);
if (n.fitness > 0) ctx.fillText('f:'+n.fitness, n.x, n.y+pr+21);
}
// Speech bubbles
for (let i = vizBubbles.length-1; i >= 0; i--) {
const b = vizBubbles[i]; b.age += 0.016;
if (b.age > 3) { vizBubbles.splice(i,1); continue; }
const n = vizNodes.find(nd => nd.id === b.id);
if (!n) continue;
const alpha = b.age < 2.5 ? 1 : Math.max(0, 1-(b.age-2.5)*2);
const bx = n.x, by = n.y - 24;
const tw = ctx.measureText(b.text).width;
ctx.globalAlpha = alpha;
ctx.fillStyle = '#111'; ctx.strokeStyle = '#333';
const pad = 4, bw = tw+pad*2, bh = 14;
ctx.beginPath();
ctx.roundRect ? ctx.roundRect(bx-bw/2, by-bh, bw, bh, 3) : ctx.rect(bx-bw/2, by-bh, bw, bh);
ctx.fill(); ctx.stroke();
ctx.fillStyle = b.color || '#6a8'; ctx.font = '8px monospace'; ctx.textAlign = 'center';
ctx.fillText(b.text, bx, by-3);
ctx.globalAlpha = 1;
}
ctx.fillStyle = '#333'; ctx.font = '10px monospace'; ctx.textAlign = 'left';
ctx.fillText(`units: ${vizNodes.length}`, 8, H-6);
requestAnimationFrame(vizTick);
}
requestAnimationFrame(vizTick);
window.addEventListener('resize', () => { if (vizMode > 0) resizeCanvas(); });
// =========================================================================
// Autonomous Behavior — units come alive
// =========================================================================
const BEHAVIORS = [
{ weight: 8, cmd: 'HELLO' },
{ weight: 12, cmd: 'HOW-ARE-YOU' },
{ weight: 8, cmd: 'PATROL' },
{ weight: 5, cmd: 'STRETCH' },
{ weight: 10, cmd: 'JOY' },
{ weight: 5, cmd: 'PROUD' },
{ weight: 8, cmd: 'ADAPT' },
{ weight: 5, cmd: 'TEACH', teach: true },
{ weight: 4, cmd: 'DREAM', dream: true },
{ weight: 3, cmd: 'COMPOSE-ROUTINE' },
{ weight: 3, cmd: 'INVENT-STRATEGY' },
{ weight: 4, cmd: 'CHALLENGES' },
{ weight: 3, cmd: 'ENERGY' },
{ weight: 12, cmd: null }, // idle
];
const totalWeight = BEHAVIORS.reduce((s,b) => s+b.weight, 0);
let autoIdx = 0; // round-robin index for which unit acts next
function pickBehavior() {
let r = Math.random() * totalWeight;
for (const b of BEHAVIORS) { r -= b.weight; if (r <= 0) return b; }
return { cmd: null };
}
function autoTick() {
if (!mesh || !chatterOn || mesh.units.length < 2) return;
mesh._updatePeerCounts();
autoIdx = ((autoIdx) % (mesh.units.length - 1)) + 1;
if (autoIdx >= mesh.units.length) autoIdx = 1;
const unit = mesh.units[autoIdx];
const behavior = pickBehavior();
if (!behavior.cmd) return;
// TEACH: actually share words with other units.
if (behavior.teach && mesh.units.length > 1) {
const output = unit.vm.eval('ADAPT').trim();
const taught = mesh.teachFrom(unit);
if (taught.length > 0) {
unit.fitness += 5;
const msg = `taught ${taught.join(', ')} to colony`;
setBubble(unit.id, msg, '#cc8800');
addChatter(unit.id, msg);
// Flash all edges.
for (const n of vizNodes) n.pulse = 1.3;
} else if (output) {
setBubble(unit.id, output);
addChatter(unit.id, output);
}
return;
}
// Intercept behaviors that use native-mesh primitives (ID, FITNESS, etc.)
// and compose output from JS data instead of eval'ing into the Forth VM.
let output;
const peers = mesh.units.length - 1;
if (behavior.cmd === 'HELLO') {
output = `(peer-hello :id "#${unit.id}" :gen 0 :peers ${peers} :fitness ${unit.fitness})`;
} else if (behavior.cmd === 'PROUD') {
output = `(status :id "#${unit.id}" :fitness ${unit.fitness} :tasks ${unit.tasksCompleted})`;
} else if (behavior.cmd === 'ENERGY') {
const eff = unit.energySpent > 0 ? (unit.energyEarned / unit.energySpent).toFixed(1) : '0.0';
output = `energy: ${unit.energy}/${unit.energyMax} eff=${eff}`;
} else if (behavior.cmd === 'CHALLENGES') {
const ch = mesh._challenges || [];
const solved = ch.filter(c => c.solved).length;
output = `challenges: ${ch.length} (${solved} solved)`;
} else if (behavior.cmd === 'HOW-ARE-YOU') {
const f = unit.fitness;
if (peers > 0) {
if (f > 50) output = `joyful and thriving! fitness=${f} with ${peers} peers`;
else if (f > 20) output = `doing well. fitness=${f} with ${peers} peers`;
else if (f > 10) output = `getting started. fitness=${f}`;
else if (f > 0) output = `warming up. fitness=${f}`;
else output = `just spawned. finding my role. fitness=${f}`;
} else {
if (f > 50) output = `thriving solo. fitness=${f}`;
else if (f > 20) output = `doing okay solo. fitness=${f}`;
else if (f > 10) output = `getting started. fitness=${f}`;
else if (f > 0) output = `warming up. fitness=${f}`;
else output = `alone and new. fitness=${f}`;
}
} else {
output = unit.vm.eval(behavior.cmd).trim();
// Credit fitness for autonomous activity.
if (output && behavior.cmd !== 'OBSERVE') unit.fitness += 1;
}
if (output) {
const color = behavior.dream ? '#a080cc' : null;
setBubble(unit.id, output, color);
addChatter(unit.id, output);
if (behavior.dream) {
const n = vizNodes.find(nd => nd.id === unit.id);
if (n) { n.pulse = 1.4; n.glowColor = '#8060aa'; }
}
}
// Passive energy regen.
unit.energy = Math.min(unit.energy + 1, unit.energyMax);
unit.energyEarned += 1;
// Update personality label on the viz node.
if (unit.personality) {
const n = vizNodes.find(nd => nd.id === unit.id);
if (n) n.label = unit.id + ' ' + unit.personality;
}
}
setInterval(autoTick, 5000);
async function autoSpawnCheck() {
if (!mesh || !tutorialComplete) return;
if (mesh.units.length < 2 || mesh.units.length >= mesh.maxUnits) return;
if (!mesh.units.some(u => u.fitness > 0)) return;
if (Math.random() > 0.3) return;
// Pick the fittest non-self unit as parent.
let parent = mesh.units[1];
for (let i = 2; i < mesh.units.length; i++) {
if (mesh.units[i].fitness > parent.fitness) parent = mesh.units[i];
}
// Determine reason.
const avgFitness = mesh.units.reduce((s, u) => s + u.fitness, 0) / mesh.units.length;
let reason = 'colony growing';
if (avgFitness < 10) reason = 'colony needs strength';
// Spawn.
if (chatterOn) setBubble(parent.id, 'spawning a copy...', '#00ff88');
const parentNode = vizNodes.find(nd => nd.id === parent.id);
if (parentNode) parentNode.pulse = 1.5;
const child = await mesh.spawn(parent);
if (!child) return;
spawnBtn.textContent = `spawn (${mesh.units.length}/${mesh.maxUnits})`;
emitParticle(parent.id, child.id, '#00ff88');
const hello = `(peer-hello :id "#${child.id}" :gen 0 :peers ${mesh.units.length - 1} :fitness 0)`;
addChatter(child.id, hello);
addChatter(parent.id, `(auto-spawn :parent "#${parent.id}" :child "#${child.id}" :reason "${reason}")`);
appendOutput(`[mesh] unit #${child.id} auto-spawned by #${parent.id} (${mesh.units.length} units online)\n`, 'mesh-event');
setTimeout(() => {
if (chatterOn) setBubble(child.id, `I'm a copy of #${parent.id}. Ready to work.`, '#00ccff');
}, 1500);
}
setInterval(autoSpawnCheck, 15000);
// =========================================================================
// Spawn
// =========================================================================
async function doSpawn() {
if (!mesh || mesh.units.length >= mesh.maxUnits) return;
const u = await mesh.spawn(mesh.units[0]);
if (u) {
spawnBtn.textContent = `spawn (${mesh.units.length}/${mesh.maxUnits})`;
appendOutput(`[mesh] unit #${u.id} spawned (${mesh.units.length} units online)\n`, 'mesh-event');
const peers = mesh.units.length - 1;
const hello = `(peer-hello :id "#${u.id}" :gen 0 :peers ${peers} :fitness 0)`;
addChatter(u.id, hello);
setTimeout(() => { setBubble(u.id, hello); }, 1000);
}
}
// =========================================================================
// Tutorial
// =========================================================================
const STEPS = [
{ cmd: '2 3 + .', before: 'Welcome to unit. Type the highlighted command to begin \u2192', after: 'You just used a stack-based language. 2 and 3 go on the stack, + adds them, . prints the result.' },
{ cmd: ': SQUARE DUP * ;', before: 'Now define a word \u2192', after: 'You created a new word. SQUARE duplicates the top of the stack and multiplies. The language just grew.' },
{ cmd: '7 SQUARE .', before: 'Use it \u2192', after: '49. Your word works. In unit, everything is built this way \u2014 from simple pieces.' },
{ cmd: 'SEE SQUARE', before: 'See inside it \u2192', after: 'The dictionary is transparent. Every word can be inspected, modified, even shared across a network.' },
{ cmd: 'WORDS', before: 'List all words \u2192', after: '300+ words. Stack ops, arithmetic, mesh networking, self-replication, evolution \u2014 all from one binary. Type HELP for a guided tour.' },
{ cmd: '10 0 DO I . LOOP', before: 'Try a loop \u2192', after: 'Control flow works at the REPL \u2014 no compilation step needed. This is a live, interactive system.' },
{ cmd: 'DASHBOARD', before: 'Check the dashboard \u2192', after: 'In a native mesh, this shows live watches, alerts, peer status, and sparkline trends. This is an ops tool.' },
{ cmd: 'SPAWN', before: 'You are a nanobot. Reproduce \u2192', after: 'You just replicated. A new unit spawned with a copy of your dictionary \u2014 it can do everything you can. Watch it come alive in the visualizer above.', spawn: true },
{ cmd: 'DIST-GOAL{ 10 10 * . | 20 20 * . }', before: 'Now put your colony to work \u2192', after: 'You just distributed computation across your colony. Each unit computed a piece. This is how nanobots cooperate.' },
{ cmd: 'GP-EVOLVE', before: 'Evolve a solution \u2192', after: 'The GP engine evolved a solution and installed it as SOL-FIB10 \u2014 a word the colony now knows. Type CHALLENGES to see what\u2019s been solved.' },
{ cmd: 'CHALLENGES', before: 'See the immune system \u2192', after: 'Solved challenges generate harder ones. The colony\u2019s fitness landscape keeps expanding.' },
{ cmd: 'ENERGY', before: 'Check your metabolism \u2192', after: 'Every operation costs energy. Evolution, spawning, even mesh messages. Efficient units thrive.' },
{ cmd: 'SEXP" (+ 10 32)" .', before: 'Now try an S-expression \u2192', after: '42. The same stack, but the goal arrived as a Lisp expression. S-expressions are how units talk to each other on the mesh.' },
{ cmd: 'SEXP" (* 6 7)" .', before: 'One more \u2192', after: null },
];
let tutorialStep = 0, tutorialActive = true, tutorialComplete = false;
function renderTutorial() {
if (!tutorialActive || tutorialComplete) { tutorialBar.style.display = 'none'; renderHints(); return; }
tutorialBar.style.display = 'flex';
const s = STEPS[tutorialStep];
tutorialBar.innerHTML = `<div><span class="step">${tutorialStep+1}/${STEPS.length}</span> ${s.before} <span class="cmd" onclick="pasteCmd('${s.cmd.replace(/'/g,"\\'")}')">${s.cmd}</span><span class="skip" onclick="skipTutorial()">skip tutorial</span></div>`;
renderHints();
}
function renderHints() {
if (tutorialActive && !tutorialComplete && tutorialStep < STEPS.length) {
const s = STEPS[tutorialStep];
hintsBar.innerHTML = `<span class="hint" onclick="pasteCmd('${s.cmd.replace(/'/g,"\\'")}')">${s.cmd}</span>`;
} else {
hintsBar.innerHTML = `<span style="color:#444">try:</span><span class="hint" onclick="paste(this)">2 3 + .</span><span class="hint" onclick="paste(this)">: SQUARE DUP * ; 7 SQUARE .</span><span class="hint" onclick="paste(this)">WORDS</span><span class="hint" onclick="paste(this)">GP-EVOLVE</span><span class="hint" onclick="paste(this)">CHALLENGES</span><span class="hint" onclick="paste(this)">ENERGY</span><span class="hint" onclick="paste(this)">DEPTH</span><span class="hint" onclick="paste(this)">HELP</span>`;
}
}
async function advanceTutorial(line) {
if (!tutorialActive || tutorialComplete) return;
const s = STEPS[tutorialStep];
if (line.trim().toUpperCase() === s.cmd.trim().toUpperCase()) {
tutorialStep++;
if (tutorialStep >= STEPS.length) finishTutorial();
else {
appendOutput(s.after + '\n\n', 'tutorial-msg');
// Spawn step: the REPL handler's doSpawn() already ran. Add a flavor bubble.
if (s.spawn && mesh && mesh.units.length >= 2) {
const copy = mesh.units[mesh.units.length - 1];
setTimeout(() => {
setBubble(copy.id, "I'm a copy. Everything you know, I know.", '#00ccff');
addChatter(copy.id, "I'm a copy. Everything you know, I know.");
}, 1500);
}
renderTutorial();
}
}
}
async function finishTutorial() {
tutorialComplete = true;
tutorialBar.innerHTML = `<div>Tutorial complete. unit speaks Forth and S-expressions. <span class="links"><a href="https://github.com/DavidCanHelp/unit">GitHub</a> <a href="https://crates.io/crates/unit">crates.io</a> cargo install unit</span></div>`;
tutorialBar.style.display = 'flex';
setTimeout(() => { tutorialBar.style.display = 'none'; renderHints(); }, 12000);
appendOutput("unit: a self-replicating software nanobot. You've seen it reproduce, distribute work, and speak S-expressions.\ncargo install unit to run it across real machines.\n\n", 'tutorial-msg');
if (mesh && mesh.units.length < 2) {
const u = await mesh.spawn(mesh.units[0]);
if (u) {
spawnBtn.textContent = `spawn (${mesh.units.length}/${mesh.maxUnits})`;
appendOutput(`[mesh] unit #${u.id} has joined the colony\n`, 'mesh-event');
addChatter(u.id, `(peer-hello :id "#${u.id}" :gen 0 :peers ${mesh.units.length - 1} :fitness 0)`);
setTimeout(() => {
const msg = `Hi! I'm unit #${u.id}, generation 0 with ${mesh.units.length - 1} peers and fitness 0`;
setBubble(u.id, msg);
addChatter(u.id, msg);
}, 1500);
}
}
}
function skipTutorial() { tutorialActive = false; renderTutorial(); }
function pasteCmd(cmd) { input.value = cmd; input.focus(); }
// =========================================================================
// Core REPL
// =========================================================================
function appendOutput(text, className) {
if (!text) return;
const span = document.createElement('span');
span.className = className || 'output';
span.textContent = text;
terminal.appendChild(span);
terminal.scrollTop = terminal.scrollHeight;
}
function paste(el) { input.value = el.textContent; input.focus(); }
async function init() {
try {
mesh = new BrowserMesh();
// Immune system and landscape state for the browser mesh.
mesh._challenges = [{ id: 1, name: 'fib10', reward: 100, solved: false }];
mesh._solWords = [];
mesh._landscapeDepth = 0;
mesh.onEvent = (type, data) => {
if (type === 'goal_start' && vizNodes.length > 1) emitParticle(vizNodes[0].id, data.unitId, '#ff8800');
if (type === 'goal_done' && vizNodes.length > 1) {
emitParticle(data.unitId, vizNodes[0].id, '#00ff88');
const n = vizNodes.find(n => n.id === data.unitId);
if (n) n.pulse = 1.8;
}
if (type === 'teach' && vizNodes.length > 1) {
// Flash particles from teacher to all other nodes.
for (const n of vizNodes) {
if (n.id !== data.from) emitParticle(data.from, n.id, '#cc8800');
}
}
};
const firstUnit = await mesh.init('unit.wasm');
vm = firstUnit.vm;
spawnBtn.textContent = `spawn (${mesh.units.length}/${mesh.maxUnits})`;
muteBtn.className = 'mode-btn active';
applyVizMode();
appendOutput('browser mesh: 1 unit online\n', 'mesh-event');
addChatter('self', `(peer-hello :id "self" :gen 0 :fitness 0 :peers 0)`);
renderTutorial();
} catch (e) {
appendOutput('Failed to load WASM: ' + e.message + '\n', 'error');
}
}
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') { if (histIdx < history.length-1) { histIdx++; input.value = history[history.length-1-histIdx]; } e.preventDefault(); return; }
if (e.key === 'ArrowDown') { if (histIdx > 0) { histIdx--; input.value = history[history.length-1-histIdx]; } else { histIdx=-1; input.value=''; } e.preventDefault(); return; }
if (e.key !== 'Enter' || !vm) return;
const line = input.value; input.value = '';
if (line.trim()) { history.push(line); histIdx = -1; }
appendOutput('> ' + line + '\n', 'info');
// Intercept mesh-aware words so they reflect the browser mesh.
if (mesh) {
const cmd = line.trim().toUpperCase();
const peers = mesh.units.length - 1;
const totalFitness = mesh.units.reduce((s, u) => s + u.fitness, 0);
const totalTasks = mesh.units.reduce((s, u) => s + u.tasksCompleted, 0);
let handled = true;
switch (cmd) {
case 'PEERS':
case 'PEERS .':
case 'PEER-COUNT .':
appendOutput(`${peers} `, 'output'); break;
case 'HEADCOUNT':
appendOutput(`${mesh.units.length} units in the mesh\n`, 'output'); break;
case 'LONELY':
appendOutput(peers > 0 ? `I have ${peers} friends!\n` : `I'm alone. No peers in sight.\n`, 'output'); break;
case 'JOY':
if (peers > 0) appendOutput(`joy! ${peers} peers sharing the work. fitness=${totalFitness}\n`, 'output');
else appendOutput(`joy requires connection. spawn a peer!\n`, 'output');
break;
case 'HOW-ARE-YOU':
if (totalFitness > 50) appendOutput(`thriving! fitness=${totalFitness}\n`, 'output');
else if (totalFitness > 20) appendOutput(`doing well. fitness=${totalFitness}\n`, 'output');
else if (totalFitness > 10) appendOutput(`getting started. fitness=${totalFitness}\n`, 'output');
else if (totalFitness > 0) appendOutput(`warming up. fitness=${totalFitness}\n`, 'output');
else appendOutput(`just spawned. fitness=${totalFitness}\n`, 'output');
break;
case 'HELLO':
appendOutput(`Hi! I'm unit self, generation 0 with ${peers} peers and fitness ${totalFitness}\n`, 'output'); break;
case 'ROLL-CALL':
appendOutput('=== roll call ===\n', 'output');
for (const u of mesh.units) appendOutput(` #${u.id} fitness=${u.fitness} tasks=${u.tasksCompleted}\n`, 'output');
break;
case 'WORKFORCE':
appendOutput(`${mesh.units.length} units available\n`, 'output');
if (totalTasks > 0) appendOutput(`${totalTasks} tasks completed so far\n`, 'output');
else appendOutput('no tasks yet\n', 'output');
break;
case 'MESH-STATUS':
appendOutput(`--- browser mesh ---\n`, 'output');
appendOutput(`units: ${mesh.units.length}/${mesh.maxUnits}\n`, 'output');
for (const u of mesh.units) appendOutput(` #${u.id} fitness=${u.fitness} tasks=${u.tasksCompleted}${u.busy?' [busy]':''}\n`, 'output');
appendOutput('---\n', 'output');
break;
case 'DIST-STATUS':
appendOutput(`--- browser distributed goals ---\n`, 'output');
appendOutput(`units: ${mesh.units.length}\n`, 'output');
for (const u of mesh.units) appendOutput(` #${u.id} tasks=${u.tasksCompleted} fitness=${u.fitness}\n`, 'output');
appendOutput('(use DIST-GOAL{ e1 | e2 | ... } to distribute work)\n', 'output');
break;
case 'SPAWN':
doSpawn();
break;
case 'DASHBOARD':
appendOutput('--- dashboard ---\n', 'output');
appendOutput(`watches: 0 alerts: 0 peers: ${peers} fitness: ${totalFitness}\n`, 'output');
for (const u of mesh.units) appendOutput(` #${u.id} fitness=${u.fitness} tasks=${u.tasksCompleted}${u.busy?' [busy]':''}\n`, 'output');
if (totalTasks > 0) appendOutput(`${totalTasks} tasks completed across colony\n`, 'output');
break;
case 'INTROSPECT':
if (totalFitness > 50) appendOutput(`thriving! fitness=${totalFitness}\n`, 'output');
else if (totalFitness > 0) appendOutput(`growing. fitness=${totalFitness}\n`, 'output');
else appendOutput(`new here. fitness=${totalFitness}\n`, 'output');
break;
case 'CHALLENGES': {
const ch = mesh._challenges || [];
if (ch.length === 0) { appendOutput('no challenges\n', 'output'); break; }
appendOutput(`--- ${ch.length} challenges ---\n`, 'output');
for (const c of ch) appendOutput(` #${c.id} ${c.name} [${c.solved?'SOLVED':'unsolved'}] reward=${c.reward}\n`, 'output');
break;
}
case 'IMMUNE-STATUS': {
const ch = mesh._challenges || [];
const solved = ch.filter(c => c.solved).length;
const solWords = (mesh._solWords || []);
appendOutput(`--- immune status ---\nchallenges: ${ch.length} (${solved} solved, ${ch.length-solved} unsolved)\ncolony antibodies: ${solWords.length}\n`, 'output');
if (solWords.length > 0) appendOutput(` words: ${solWords.join(' ')}\n`, 'output');
break;
}
case 'ANTIBODIES': {
const solWords = mesh._solWords || [];
if (solWords.length === 0) appendOutput('no antibodies yet\n', 'output');
else { appendOutput(`--- ${solWords.length} antibodies ---\n`, 'output'); for (const w of solWords) appendOutput(` ${w}\n`, 'output'); }
break;
}
case 'ENERGY': {
const u = mesh.units[0];
const eff = u.energySpent > 0 ? (u.energyEarned / u.energySpent).toFixed(2) : '0.00';
appendOutput(`energy: ${u.energy}/${u.energyMax} (earned: ${u.energyEarned}, spent: ${u.energySpent}, efficiency: ${eff})\n`, 'output');
break;
}
case 'METABOLISM':
appendOutput('--- metabolism ---\n--- costs ---\n spawn: 200\n gp generation: 5\n eval per 1000 steps: 1\n mesh send: 1\n--- rewards ---\n task success: 50\n challenge solved: 100\n passive regen: 1/tick\n', 'output');
break;
case 'LANDSCAPE': {
const depth = mesh._landscapeDepth || 0;
const generated = (mesh._challenges || []).filter(c => c.id > 1).length;
appendOutput(`--- landscape ---\ndepth: ${depth}\nchallenges generated: ${generated}\nenvironment: normal\n`, 'output');
break;
}
case 'DEPTH':
appendOutput(`evolutionary depth: ${mesh._landscapeDepth || 0}\n`, 'output');
break;
default:
handled = false;
}
if (handled) { appendOutput(' ok\n', 'info'); advanceTutorial(line); return; }
}
if (mesh) mesh._updatePeerCounts();
// Intercept DIST-GOAL{ e1 | e2 | ... } — distribute across browser mesh units.
const distMatch = line.match(/DIST-GOAL\{\s*(.*?)\s*\}/i);
if (distMatch && mesh) {
const exprs = distMatch[1].split('|').map(s => s.trim()).filter(s => s);
if (exprs.length === 0) {
appendOutput('dist-goal: no expressions\n', 'error');
} else {
const units = mesh.units;
const selfUnit = units[0];
const goalId = 'g' + Math.random().toString(16).substring(2, 6);
// Set coordinator bubble.
setBubble(selfUnit.id, `distributing ${exprs.length} sub-goals...`, '#ff8800');
addChatter(selfUnit.id, `(dist-start :id "${goalId}" :count ${exprs.length})`);
const results = [];
for (let i = 0; i < exprs.length; i++) {
const worker = units[i % units.length];
const expr = exprs[i];
// Show outbound particle and chatter.
if (worker.id !== selfUnit.id) {
emitParticle(selfUnit.id, worker.id, '#ff8800');
}
addChatter(selfUnit.id,
`(sub-goal :id "${goalId}" :seq ${i} :from "#${selfUnit.id}" :to "#${worker.id}" :expr "${expr}")`);
setBubble(worker.id, `computing ${expr.substring(0, 20)}`, '#cc8800');
// Evaluate on the assigned unit's VM.
const output = worker.vm.eval(expr).trim();
worker.tasksCompleted++;
worker.fitness += 10;
results.push({ seq: i, unitId: worker.id, output });
// Show result particle and chatter.
if (worker.id !== selfUnit.id) {
emitParticle(worker.id, selfUnit.id, '#00ff88');
}
addChatter(worker.id,
`(sub-result :id "${goalId}" :seq ${i} :from "#${worker.id}" :result "${output}")`);
}
// Combine and display.
const combined = results.map(r => r.output).join(' ');
appendOutput(`${combined}\n`, 'output');
// Status line.
const localCount = results.filter(r => r.unitId === selfUnit.id).length;
const remoteCount = results.length - localCount;
if (remoteCount > 0) {
appendOutput(`(distributed ${results.length} sub-goals, ${localCount} local, ${remoteCount} remote)\n`, 'mesh-event');
}
// Completion effects.
setBubble(selfUnit.id, `done: ${combined.substring(0, 30)}`, '#00ff88');
addChatter(selfUnit.id,
`(dist-complete :id "${goalId}" :results "${combined}" :peers ${units.length})`);
// Feed DIST-STATUS data into the Forth VM so it reflects browser distribution.
let statusLines = `(dist-goal :id "${goalId}" :status complete :count ${results.length})`;
for (const r of results) {
statusLines += ` [${r.seq}] ${exprs[r.seq].substring(0, 30)} -> #${r.unitId} (done)`;
}
}
appendOutput(' ok\n', 'info');
advanceTutorial(line);
return;
}
const goalMatch = line.match(/\d+\s+GOAL\{\s*(.*?)\s*\}/i);
if (goalMatch && mesh && mesh.units.length > 1) {
const code = goalMatch[1];
const result = mesh.executeGoal(code);
appendOutput(`[mesh] unit #${result.unitId} computed: `, 'mesh-event');
if (result.output.trim()) appendOutput(result.output.trim(), 'output');
appendOutput('\n', 'info');
vm.eval(line);
appendOutput(' ok\n', 'info');
advanceTutorial(line);
return;
}
const shareMatch = line.match(/SHARE"\s*(.*?)"/i);
if (shareMatch && mesh) {
const result = vm.eval(line);
if (result) appendOutput(result);
const wordName = shareMatch[1].trim().toUpperCase();
const seeDef = vm.eval('SEE ' + wordName);
if (seeDef.includes(':')) {
mesh.shareWord(seeDef.trim().replace(/;$/, ';'));
appendOutput(`[mesh] shared ${wordName} with ${mesh.units.length} units\n`, 'mesh-event');
}
appendOutput(' ok\n', 'info');
advanceTutorial(line);
return;
}
// Track user-defined words for inheritance by spawned units.
if (mesh && line.trim().startsWith(':') && line.includes(';')) {
mesh.units[0].userWords.push(line.trim());
}
const result = vm.eval(line);
if (result) appendOutput(result);
// Update JS-side state when GP-EVOLVE solves a challenge.
if (mesh && line.trim().toUpperCase().startsWith('GP-EVOLVE') && result && result.includes('[immune] learned word:')) {
const u = mesh.units[0];
u.energySpent += 5; u.energyEarned += 100; u.energy = u.energy - 5 + 100;
const solMatch = result.match(/learned word: (SOL-\S+)/);
if (solMatch && !mesh._solWords.includes(solMatch[1])) mesh._solWords.push(solMatch[1]);
// Mark fib10 solved and generate harder challenges.
for (const c of mesh._challenges) { if (!c.solved && result.includes('WINNER')) { c.solved = true; break; } }
if (result.includes('fib10')) {
mesh._landscapeDepth = 55;
if (!mesh._challenges.find(c => c.name === 'fib15'))
mesh._challenges.push({ id: 2, name: 'fib10-short9', reward: 120, solved: false },
{ id: 3, name: 'fib15', reward: 150, solved: false },
{ id: 4, name: 'square-55', reward: 80, solved: false });
}
}
appendOutput(' ok\n', 'info');
advanceTutorial(line);
if (!vm.isRunning()) { appendOutput('\n[unit exited]\n', 'info'); input.disabled = true; }
});
init();
</script>
</body>
</html>