Eyespot Lab

A limited working example of the eyespot adaptation — compare its vision tiers live, then spend DNA on a lootbox-style splice that equips into the Petri Dish Game.

Eyespot Lab

A limited working example of the eyespot adaptation — compare its vision tiers live, then spend DNA on a lootbox-style splice that equips into the Petri Dish Game.

---

.. toc::

See through your cell

The eyespot decides how much of the world your cell can see and how it's drawn. On the left, switch tiers to watch the same scene re-render:

Splice a new eyespot (lootbox)

On the right, spend banked DNA to splice. Each spin rerolls the odds across the four tiers (each 0–80%, summing to 100%) and there's a 50/50 chance you actually land your preferred part — the reels animate, then the result equips into the [Petri Dish Game](/petri-dish-game) (shared player-loadout, saved locally). The tier is a gamble: you can roll a worse eye than you had.

.. admonition::Where DNA comes from :color: blue

Play the [Petri Dish Game](/petri-dish-game): eat plankton and smaller cells to bank DNA. At the threshold the game pauses and sends you here to evolve.

.. exec::docs.adaptation_eyespot.adaptation_eyespot :code: false

Source

```python

File: docs/adaptation_eyespot/adaptation_eyespot.py

from dash import dcc, html, clientside_callback, Input, Output, State import dash_mantine_components as dmc from dash_iconify import DashIconify

from lib.adaptations import ADAPTATIONS, PART_ORDER, EYESPOT_VISION, TIER_LABEL, TIER_COLOR

/adaptation-eyespot — the EYESPOT LAB. Two halves:

1. a live vision demo: switch tier (none/poor/decent/great) and watch the same

mini-scene re-render as radar / blur / muted / full + zoom — exactly what the

Petri Dish Game does, in isolation.

2. a DNA-spend LOOTBOX: spend banked DNA (shared player-loadout store) on a roll.

A 50/50 "preferred vs wildcard" reel + a per-spin tier distribution (each

0-80%, summing 100%) animate slot-machine style, then commit the result to

the loadout so the game picks it up.

ACCENT = "#3399ff" COST = 12 _EYE = ADAPTATIONS["eyespot"] _VARIANTS = {v["tier"]: v for v in _EYE["variants"]} _PART_LABELS = [ADAPTATIONS[p]["label"] for p in PART_ORDER]

_SCENE = [ {"x": -150, "y": -110, "r": 70, "img": "/assets/petri/paramecium.svg", "kind": "food", "deg": 25, "vx": 2.1, "vy": 1.3}, {"x": 360, "y": -180, "r": 120, "img": "/assets/petri/cyclops.svg", "kind": "hunter", "deg": 210, "ring": True, "vx": -1.6, "vy": 1.1}, {"x": 240, "y": 300, "r": 64, "img": "/assets/petri/euglena.svg", "kind": "food", "deg": -100, "vx": 1.4, "vy": -2.0}, {"x": -420, "y": 260, "r": 96, "img": "/assets/petri/daphnia.svg", "kind": "threat", "deg": 135, "vx": 2.3, "vy": -1.2}, {"x": -640, "y": -480, "r": 60, "img": "/assets/petri/navicula.svg", "kind": "food", "deg": 60, "vx": 1.0, "vy": 1.8}, {"x": 700, "y": 520, "r": 80, "img": "/assets/petri/stentor.svg", "kind": "threat", "deg": 250, "vx": -2.0, "vy": -1.0}, {"x": 0, "y": 0, "r": 64, "img": "/assets/petri/volvox.svg", "kind": "player", "deg": 0}, ] _PLANKTON = [{"x": gx * 130 - 845, "y": gy * 130 - 845, "s": 16} for gx in range(14) for gy in range(14) if (gx * 7 + gy * 3) % 3 == 0]

_TIERS = ["none", "poor", "decent", "great"]

------------------------------------------------------------- vision demo -----

_demo = dmc.Paper( [ dmc.Group( [dmc.Text("VISION DEMO", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Badge("eyespot", color="grape", variant="light", styles={"root": {"fontFamily": "monospace"}})], justify="space-between", mb="xs", ), html.Div(id="eye-arena", className="petri-dish", style={"position": "relative", "borderRadius": "50%", "aspectRatio": "1 / 1", "width": "100%", "maxWidth": 360, "margin": "0 auto", "overflow": "hidden"}), dmc.SegmentedControl( id="eye-tier", value="none", fullWidth=True, color="brand", radius="md", size="sm", mt="sm", data=[{"value": t, "label": TIER_LABEL[t]} for t in _TIERS], ), dmc.Text("", id="eye-blurb", size="xs", c="dimmed", mt="xs"), dmc.Text("", id="eye-stats", size="xs", fw=600, mt=4, style={"fontFamily": "monospace"}), dcc.Interval(id="eye-demo-tick", interval=90, n_intervals=0), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 330px", "maxWidth": 400, "borderColor": "rgb(51 142 234 / 24%)"}, )

------------------------------------------------------------------ lootbox ----

def _odd_cell(tier): return html.Div( [ html.Div(html.Div(id=f"eye-bar-{tier}", className="eye-bar"), className="eye-bar-track"), dmc.Text(f"0%", id=f"eye-pct-{tier}", size="xs", fw=700, ta="center", style={"fontFamily": "monospace"}), dmc.Badge(TIER_LABEL[tier], color=TIER_COLOR[tier], variant="light", size="xs", fullWidth=True), ], id=f"eye-odd-{tier}", className="eye-odd", )

_lootbox = dmc.Paper( [ dmc.Group( [dmc.Text("SPLICE LAB", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Group( [DashIconify(icon="mdi:dna", width=18, color="#7048e8"), dmc.Text("0", id="eye-dna", fw=700, style={"fontFamily": "monospace"}), dmc.Text("DNA", size="xs", c="dimmed")], gap=6)], justify="space-between", mb="xs", ), dmc.Text("Spend DNA to splice a new eyespot. 50/50 you get your preferred part; the tier you " "land on is a gamble — the odds reroll every spin.", size="xs", c="dimmed", mb="xs"), # adaptation (type) reel html.Div( [dmc.Text("ADAPTATION", size="9px", c="dimmed", style={"letterSpacing": "1px"}), html.Div("EYESPOT", id="eye-type-label", className="eye-type-hit")], className="eye-type-reel", ), # tier odds reel html.Div([_odd_cell(t) for t in _TIERS], className="eye-odds-row"), dmc.Group( [dmc.Button(f"SPLICE DNA (−{COST})", id="eye-splice", color="brand", leftSection=DashIconify(icon="tabler:dna-2")), dmc.Anchor(dmc.Button("Back to game", variant="default", leftSection=DashIconify(icon="tabler:arrow-back")), href="/petri-dish-game")], gap="sm", mt="xs", ), dmc.Text("", id="eye-roll-status", size="sm", fw=600, mt="xs"), dmc.Group( [dmc.Text("Currently equipped:", size="xs", c="dimmed"), dmc.Badge("none", id="eye-applied", color="gray", variant="filled", size="sm")], gap=6, mt=4, ), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 330px", "maxWidth": 460, "borderColor": "rgb(51 142 234 / 24%)"}, )

component = dmc.Stack( [ dmc.Paper( [dmc.Text("EYESPOT LAB", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text("The eyespot governs how much of the world your cell sees and how it's rendered. " "Compare the tiers on the left; splice a new one with DNA on the right — it equips " "into the Petri Dish Game.", size="sm", c="dimmed")], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)", "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)"}, ), dmc.Group([_demo, _lootbox], align="flex-start", gap="xl", wrap="wrap"), dcc.Store(id="eye-vis", data=EYESPOT_VISION), dcc.Store(id="eye-scene", data=_SCENE), dcc.Store(id="eye-plankton", data=_PLANKTON), dcc.Store(id="eye-variants", data=_VARIANTS), dcc.Store(id="eye-parts", data=_PART_LABELS), dcc.Store(id="eye-demo-sink"), ], gap="md", )

1) live vision demo: move the scene + render in the selected tier's style/view

clientside_callback( """ function(n, tier, vis, scene0, plankton) { if (!window.CV) { return window.dash_clientside.no_update; } if (!window.__eye) { window.__eye = {cells: JSON.parse(JSON.stringify(scene0 || []))}; } const cells = window.__eye.cells; cells.forEach(c => { if (c.kind === 'player') { return; } c.x += (c.vx || 0); c.y += (c.vy || 0); if (Math.abs(c.x) > 900) { c.vx *= -1; c.x = Math.max(-900, Math.min(900, c.x)); } if (Math.abs(c.y) > 900) { c.vy *= -1; c.y = Math.max(-900, Math.min(900, c.y)); } if (Math.hypot(c.vx || 0, c.vy || 0) > 0.3) { c.deg = Math.atan2(c.vy, c.vx) * 180 / Math.PI; } }); const v = (vis && vis[tier]) || (vis && vis.none) || {view: 320, style: 'radar'}; const host = document.getElementById('eye-arena'); if (!host) { return window.dash_clientside.no_update; } host.innerHTML = window.CV.renderArena({ vb: [-v.view, -v.view, 2 * v.view, 2 * v.view], style: v.style, cells: cells, plankton: plankton, cam: {x: 0, y: 0, vr: v.view}, t: n, }); return window.dash_clientside.no_update; } """, Output("eye-demo-sink", "data"), Input("eye-demo-tick", "n_intervals"), Input("eye-tier", "value"), State("eye-vis", "data"), State("eye-scene", "data"), State("eye-plankton", "data"), )

2) tier -> blurb + stats text

clientside_callback( """ function(tier, variants, vis) { variants = variants || {}; const v = variants[tier]; if (!v) { return ['', '']; } const vz = (vis && vis[tier]) || {}; const stats = 'stat ' + (v.mult >= 0 ? '+' : '') + v.mult.toFixed(1) + ' cost ' + v.cost.toFixed(2) + ' view ' + (vz.view || '?') + ' render ' + (vz.style || '?'); return [v.blurb, stats]; } """, Output("eye-blurb", "children"), Output("eye-stats", "children"), Input("eye-tier", "value"), State("eye-variants", "data"), State("eye-vis", "data"), )

3) loadout -> DNA readout + equipped badge + sync demo selector to equipped tier

clientside_callback( """ function(loadout) { // player-loadout.eyespot is now a SLOT OBJECT {part,tier,...}; coerce to its // tier string (back-compat with the old bare-string shape via __migrateLoadout). const lo = window.__migrateLoadout ? window.__migrateLoadout(loadout) : (loadout || {dna: 0}); const tier = (lo.eyespot && lo.eyespot.tier) || (typeof lo.eyespot === 'string' ? lo.eyespot : 'none'); return [String(lo.dna || 0), tier, tier]; } """, Output("eye-dna", "children"), Output("eye-applied", "children"), Output("eye-tier", "value"), Input("player-loadout", "data"), )

4) SPLICE — the lootbox. Computes per-spin odds (each 0-80, sum 100), draws a

tier, runs the slot-machine animation, and commits to the shared loadout.

clientside_callback( """ function(n, loadout, vis, parts) { const nu = window.dash_clientside.no_update; if (!n) { return [nu, nu]; } const COST = """ + str(COST) + """; // migrate to the slot-object loadout so we preserve move/feed/locks/sprite const lo = window.__migrateLoadout ? window.__migrateLoadout(loadout) : (loadout || {dna: 0}); const have = lo.dna || 0; if (have < COST) { return [nu, '⚠ Need ' + COST + ' DNA (you have ' + have + ').']; } const order = ['none', 'poor', 'decent', 'great'];

// per-spin distribution: 4 weights, each 0-80, summing to 100 function randDist() { let w = [Math.random(), Math.random(), Math.random(), Math.random()]; let s = w.reduce((a, b) => a + b, 0) || 1; w = w.map(x => x / s * 100); for (let it = 0; it < 6; it++) { let over = 0; w = w.map(x => { if (x > 80) { over += x - 80; return 80; } return x; }); if (over <= 0) { break; } const room = w.map(x => 80 - x), rs = room.reduce((a, b) => a + b, 0) || 1; w = w.map((x, i) => x + room[i] / rs * over); } let r = w.map(x => Math.floor(x)); let rem = 100 - r.reduce((a, b) => a + b, 0); const frac = w.map((x, i) => ({i: i, f: x - Math.floor(x)})).sort((a, b) => b.f - a.f); for (let k = 0; k < rem; k++) { r[frac[k % 4].i]++; } return r; } const odds = randDist(); order.forEach((t, i) => { const bar = document.getElementById('eye-bar-' + t), pct = document.getElementById('eye-pct-' + t); const cell = document.getElementById('eye-odd-' + t); if (bar) { bar.style.height = Math.max(3, odds[i]) + '%'; } if (pct) { pct.textContent = odds[i] + '%'; } if (cell) { cell.classList.remove('eye-win', 'eye-hl'); } }); // draw outcome from the odds let rr = Math.random() * 100, acc = 0, drawn = 0; for (let i = 0; i < 4; i++) { acc += odds[i]; if (rr <= acc) { drawn = i; break; } } const preferred = Math.random() < 0.5; // 50/50 preferred part vs wildcard fizzle

// animate the tier reel (slot machine, decelerating, lands on drawn) const cells = order.map(t => document.getElementById('eye-odd-' + t)); let s = 0; const total = 4 * 5 + drawn; let delay = 45; (function tierTick() { cells.forEach((c, k) => { if (c) { c.classList.toggle('eye-hl', k === (s % 4)); } }); s++; if (s <= total) { delay = Math.min(420, delay * 1.13); setTimeout(tierTick, delay); } else { cells.forEach((c, k) => { if (c) { c.classList.remove('eye-hl'); c.classList.toggle('eye-win', k === drawn); } }); } })(); // animate the adaptation (type) reel const tl = document.getElementById('eye-type-label'); if (tl) { const names = parts || ['Eyespot']; let ts = 0, td = 50; (function typeTick() { tl.textContent = names[ts % names.length].toUpperCase(); tl.className = 'eye-type-spin'; ts++; if (ts < 16) { td = Math.min(360, td * 1.12); setTimeout(typeTick, td); } else { tl.textContent = preferred ? 'EYESPOT' : '\\u2726 WILDCARD'; tl.className = preferred ? 'eye-type-hit' : 'eye-type-miss'; } })(); }

// commit immediately (animation reveals the already-decided result). Write a // proper eyespot SLOT, preserving the rest of the loadout (move/feed/locks/sprite). const curTier = (lo.eyespot && lo.eyespot.tier) || 'none'; lo.dna = have - COST; lo.eyespot = {part: 'eyespot', tier: preferred ? order[drawn] : curTier, placement: 0, arc: 360, locked: false, sprite: (lo.eyespot && lo.eyespot.sprite) || null}; const newLoad = lo; const msg = preferred ? ('\\ud83e\\uddec Spliced! Eyespot \\u2192 ' + order[drawn].toUpperCase()) : '\\u2026 mutation fizzled \\u2014 DNA spent, eyespot unchanged. Re-roll!'; return [newLoad, msg]; } """, Output("player-loadout", "data", allow_duplicate=True), Output("eye-roll-status", "children"), Input("eye-splice", "n_clicks"), State("player-loadout", "data"), State("eye-vis", "data"), State("eye-parts", "data"), prevent_initial_call=True, ) ```

:defaultExpanded: false :withExpandedButton: true

---

*Source: /adaptation-eyespot*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: