# Adaptation Splicer > An isolated splicer — mark a radial section of your cell, splice an organelle there, and build up a specimen's full 360° loadout where placement matters and the rim has a finite space budget. --- .. toc:: ### Mark, splice, adapt This is the **Petri Dish Evolution** splicer lifted out of the game so the placement mechanics can be built and felt on their own. Pick a **specimen**, drag the **MARK SECTION** range to choose where on the 360° body to grow a part, pick a **preferred** organelle, and **Generate**. Each Generate is a two-stage roll — **33% your pick · 33% a wildcard · 33% nothing**, then a tier roll (**none / poor / decent / great**). A successful splice attaches the part **at that placement**: it shows on the cell as a directional wedge + organelle, and that position carries the specimen's accountability for its full state. **Placement matters** — a jaw grown at the front is a front-facing jaw. Every directional adaptation **occupies its arc** of the rim, and the rim has a finite **space budget**: marking over an occupied section is blocked, and once the rim is full you can't splice more there. The eyespot is omnidirectional (it doesn't use rim space), and **Actinosphaerium** wears a full baseline spike crown yet can still grow more on top — spikes on every other specimen stay within their marked section. Sprites render **free** (the catalogue's directional schematic) by default so you can iterate at no cost; flip **Use Gemini** to request a real image-to-image sprite (needs an API key — it degrades gracefully to the free render without one). .. exec::docs.adaptation_splicer.adaptation_splicer :code: false ### Source ```python # File: lib/splicer.py """Standalone Adaptation Splicer page (`/adaptation-splicer`). An isolated, drivable surface for developing the cell-splicing mechanics WITHOUT playing the full Petri Dish Evolution game. It runs on an OPEN-LIST placement model (assets/splicer.js → window.SPL): a specimen's adaptations are an open list, each occupying a radial arc of the rim, capped by the rim's space budget — so a specimen can be continuously adapted up to its allotted space, previously-placed adaptations show as directional wedges, and occupied sections are blocked. Pick a specimen, mark a 360° section, pick a preferred part, and Generate: a two-stage roll (33% intended / 33% wildcard / 33% nothing, then a tier roll none/poor/decent/great) runs SERVER-side (button-triggered — the reliable pattern) and the placement is applied clientside. Sprites are FREE schematic renders by default; flip the Gemini switch to request a real image-to-image sprite (needs GEMINI_IMAGE_API_KEY; degrades gracefully to the free render when absent). The live game (lib/evolution/*, assets/evolution_game.js, assets/placement.js) is deliberately untouched by this module. """ import json import os import random import urllib.request from dash import dcc, html, clientside_callback, callback, Input, Output, State, no_update import dash_mantine_components as dmc from dash_iconify import DashIconify from lib.adaptations import ADAPTATIONS, PART_ORDER, to_store as _adapt_store from lib.ai_brain import MODELS as _AI_MODELS AGENT_MODEL_OPTIONS = [{"value": k, "label": v} for k, v in _AI_MODELS.items()] from lib.movement import SPECIMENS, to_store as _mv_store from lib.diet import to_store as _diet_store from lib.evolution.config import ADAPT_ODDS, TIER_ODDS ACCENT = "#3399ff" ACCENT2 = "#e8590c" _DEF_SPEC = "euglena" _ROLE_COLOR = {"speed": "blue", "sense": "grape", "defense": "orange", "attack": "red", "feed": "green", "grip": "teal"} def _pick(odds): r = random.random() acc = 0.0 for k, v in odds.items(): acc += v if r <= acc: return k return list(odds)[-1] # Tier odds skewed by the marked arc WIDTH: more room → Decent/Great more likely # (still chance-based, never guaranteed). Lerps base TIER_ODDS → a generous # distribution as width goes 15° → 140°. _TIER_GENEROUS = {"none": 0.06, "poor": 0.16, "decent": 0.40, "great": 0.38} def _tier_odds_for_width(width): t = max(0.0, min(1.0, (float(width) - 15.0) / (140.0 - 15.0))) return {k: TIER_ODDS[k] + (_TIER_GENEROUS[k] - TIER_ODDS[k]) * t for k in TIER_ODDS} def _gemini_sprite(marked, part, tier, spec): """Image-to-image evolve via Gemini. Returns a data-URI PNG, or None on any failure / missing key (so the splicer falls back to the free render). Never raises.""" key = os.environ.get("GEMINI_IMAGE_API_KEY") or "" if not key or not marked or "," not in marked: return None try: model = os.environ.get("GEMINI_IMAGE_MODEL", "gemini-2.5-flash-image") b64 = marked.split(",", 1)[1] prompt = ( f"Evolve this microscopic cell silhouette: grow a {part} organelle " f"({tier} quality) at the orange-marked section, keeping the existing " f"cell body and any other features. Clean black line-art on a pure " f"white background, top-down view, single cell." ) payload = {"contents": [{"parts": [ {"text": prompt}, {"inline_data": {"mime_type": "image/png", "data": b64}}, ]}]} url = (f"https://generativelanguage.googleapis.com/v1beta/models/" f"{model}:generateContent?key={key}") req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read()) for cand in data.get("candidates", []): for p in cand.get("content", {}).get("parts", []): inl = p.get("inline_data") or p.get("inlineData") if inl and inl.get("data"): return "data:image/png;base64," + inl["data"] except Exception: return None return None def _chip(part_id): a = ADAPTATIONS[part_id] return dmc.Chip( dmc.Group([DashIconify(icon=a["icon"], width=15), dmc.Text(a["label"], size="xs")], gap=5), value=part_id, size="xs", variant="outline", color="brand", radius="sm", ) def splicer_component(): specimens = [{"value": s["value"], "label": s["label"]} for s in SPECIMENS] init_loadout = {"spec": _DEF_SPEC, "eyespot": {"tier": "none"}, "adaptations": []} host = html.Div( html.Div(id="spl-stage", style={"position": "absolute", "inset": 0}), id="spl-host", className="petri-dish", style={"position": "relative", "width": "100%", "maxWidth": 320, "aspectRatio": "1 / 1", "borderRadius": "50%", "overflow": "hidden", "margin": "0 auto"}, ) left = dmc.Stack([ host, dmc.Stack([ dmc.Progress(id="spl-capacity", value=0, color="brand", size="lg", radius="sm"), dmc.Text("", id="spl-capacity-text", size="xs", c="dimmed", style={"fontFamily": "monospace"}), ], gap=4), dmc.Paper( [dmc.Text("SPECIMEN STATS", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}, mb=6), html.Div(id="spl-stats")], withBorder=True, radius="md", p="sm", style={"borderColor": "rgb(51 142 234 / 24%)", "background": "light-dark(#f5f9ff, #16202e)"}, ), ], gap="sm", style={"flex": "1 1 340px", "maxWidth": 380}) controls = dmc.Stack([ dmc.Select(id="spl-spec", label="Specimen", value=_DEF_SPEC, allowDeselect=False, data=specimens, leftSection=DashIconify(icon="mdi:microscope")), dmc.Stack([ dmc.Text("MARK SECTION (on the cell)", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.Text("Click the cell to set a start edge, move to sweep the arc, click again to set it. " "A bigger arc raises the odds of a Decent / Great roll (still chance-based).", size="xs", c="dimmed"), dmc.Text("", id="spl-mark-label", size="xs", fw=600, style={"fontFamily": "monospace"}), dmc.Text("", id="spl-readout", size="sm", fw=700, style={"fontFamily": "monospace", "minHeight": 18}), dmc.Group([ dmc.Badge("min 15°", color="red", variant="light", size="xs"), dmc.Badge("60°+ favors Decent", color="yellow", variant="light", size="xs"), dmc.Badge("140°+ favors Great", color="teal", variant="light", size="xs"), ], gap=6), ], gap=6), dmc.Stack([ dmc.Text("ADAPTATION (preference)", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.ChipGroup(dmc.Group([_chip(p) for p in PART_ORDER], gap=6), id="spl-pref", value="flagellum", multiple=False), ], gap=6), dmc.Group([ dmc.Button("Generate", id="spl-generate", color="brand", leftSection=DashIconify(icon="tabler:dna-2")), dmc.Switch(id="spl-gemini", label="Use Gemini", checked=False, size="sm", onLabel="AI", offLabel="free"), ], justify="space-between", align="center"), dmc.Text("", id="spl-outcome", size="sm", fw=600, c=ACCENT, style={"minHeight": 20}), dmc.Divider(label="test in the game", labelPosition="center"), dmc.Text("AI agents (any pairing — two Claudes, two Geminis, or a mix):", size="10px", c="dimmed"), dmc.Group([ dmc.Select(id="spl-model-player", value="claude-haiku-4-5", data=AGENT_MODEL_OPTIONS, allowDeselect=False, size="xs", style={"flex": 1}, label="You", **{"aria-label": "Your AI agent model"}), dmc.Select(id="spl-model-opp", value="gemini-2.5-flash", data=AGENT_MODEL_OPTIONS, allowDeselect=False, size="xs", style={"flex": 1}, label="Rival", **{"aria-label": "Rival AI agent model"}), ], gap="xs", grow=True), dmc.Button("Launch in game", id="spl-launch", color="grape", variant="light", fullWidth=True, leftSection=DashIconify(icon="tabler:player-play")), dmc.Text("Sends this specimen + its adaptations straight into Petri Dish Evolution, skipping " "the incubation-chamber spawn — so you can feel how a configuration plays.", size="10px", c="dimmed"), ], gap="md", style={"flex": "1 1 320px", "maxWidth": 420}) preview = dmc.Paper( [dmc.Text("TIER PREVIEW (poor · decent · great)", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}, mb="xs"), html.Div(id="spl-preview-cards", style={"display": "grid", "gridTemplateColumns": "repeat(3, 1fr)", "gap": "12px"})], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)"}, ) component = dmc.Stack([ dmc.Paper( [dmc.Text("ADAPTATION SPLICER", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text("Mark a section of YOUR cell, pick a preferred organelle, and Generate. A roll " "(33% your pick · 33% a wildcard · 33% nothing, then a tier none/poor/decent/great) " "splices the part at that placement — placement matters and carries into the cell's " "360° state. Each directional adaptation occupies its arc; the rim has a finite space " "budget, and occupied sections are blocked. Sprites are free schematic renders; flip " "‘Use Gemini’ for a real image-to-image sprite.", 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([left, controls], align="flex-start", gap="xl", wrap="wrap"), preview, # stores + hidden remove-bridge button dcc.Store(id="spl-loadout", data=init_loadout), dcc.Store(id="spl-adapt", data=_adapt_store()), dcc.Store(id="spl-mv", data=_mv_store()), dcc.Store(id="spl-diet", data=_diet_store()), dcc.Store(id="spl-marked"), dcc.Store(id="spl-result"), dcc.Store(id="spl-stage-sink"), dcc.Store(id="spl-preview-sink"), dcc.Store(id="spl-section", data=[150, 210]), # committed [a0, a0+width] (wrap-safe) html.Button(id="spl-remove-btn", style={"display": "none"}), html.Button(id="spl-section-btn", style={"display": "none"}), ], gap="md") # --- reset the loadout when the specimen changes (also inits on load) --- clientside_callback( "function(spec){ return (window.SPL ? window.SPL.fresh(spec) " ": {spec: spec, eyespot: {tier: 'none'}, adaptations: []}); }", Output("spl-loadout", "data"), Input("spl-spec", "value"), ) # --- render the 360 stage + placed list + capacity + mark status --- clientside_callback( """ function(lo, spec, section, pref, mv, diet) { if (!window.SPL) { return ['', 0, '', '', true]; } lo = lo || window.SPL.fresh(spec); section = (section && section.length === 2) ? section : [150, 210]; const c = window.__splCtx = window.__splCtx || {phase: 'idle'}; c.stageId = 'spl-stage'; c.readoutId = 'spl-readout'; c.sectionBtnId = 'spl-section-btn'; c.lo = lo; c.spec = spec; c.pref = pref; if (c.phase !== 'armed') { c.section = section; } // don't clobber an in-progress sweep window.SPL.attachStage('spl-host'); window.SPL.refresh(); const stats = document.getElementById('spl-stats'); if (stats) { stats.innerHTML = window.SPL.statsCardHTML(lo, spec, mv, diet, true); } const st = window.SPL.markStatus(lo, spec, section, pref); const cap = window.SPL.capacityInfo(lo); const side = window.__sideLabel ? window.__sideLabel((section[0] + section[1]) / 2) : ''; const w = Math.round(section[1] - section[0]); const label = 'section ' + w + '° · ' + side + (st.ok ? ' — ready' : ' — ' + st.reason); const full = (cap.count >= cap.max || cap.freeDeg <= 0); const capText = 'space ' + cap.pct + '% · ' + cap.count + '/' + cap.max + ' parts · free ' + Math.round(cap.freeDeg) + '°' + (full ? ' · FULL' : ''); return ['', cap.pct, capText, label, !st.ok]; } """, Output("spl-stage-sink", "data"), Output("spl-capacity", "value"), Output("spl-capacity-text", "children"), Output("spl-mark-label", "children"), Output("spl-generate", "disabled"), Input("spl-loadout", "data"), Input("spl-spec", "value"), Input("spl-section", "data"), Input("spl-pref", "value"), State("spl-mv", "data"), State("spl-diet", "data"), ) # --- commit a finalized on-SVG selection into the section store (bridge) --- clientside_callback( "function(n){ const NU = window.dash_clientside.no_update; " "if (!n || !window.__splPendingSection) { return NU; } " "const s = window.__splPendingSection; window.__splPendingSection = null; return s; }", Output("spl-section", "data", allow_duplicate=True), Input("spl-section-btn", "n_clicks"), prevent_initial_call=True, ) # --- tier-preview cards (catalog-matched directional previews) --- clientside_callback( """ function(pref, meta) { const host = document.getElementById('spl-preview-cards'); if (!host || !window.CV || !meta) { return ''; } const part = (meta.parts || {})[pref] || {}; const vmap = {}; (part.variants || []).forEach(v => { vmap[v.tier] = v; }); const tc = window.__tierColor || (() => '#3399ff'); const render = window.CV.renderAdaptPreview || window.CV.renderPart; host.innerHTML = ['poor', 'decent', 'great'].map(function (t) { const v = vmap[t] || {}; const mult = (v.mult != null) ? (v.mult >= 0 ? '+' : '') + v.mult.toFixed(1) : ''; const cost = (v.cost != null) ? v.cost.toFixed(2) : ''; return '
' + '
' + t.toUpperCase() + '
' + '
' + render(pref, t) + '
' + '
' + (v.name || '') + '
' + '
stat ' + mult + ' · cost ' + cost + '
' + '
'; }).join(''); return ''; } """, Output("spl-preview-sink", "data"), Input("spl-pref", "value"), State("spl-adapt", "data"), ) # --- keep the marked PNG current (only rasterized while Gemini is ON) --- clientside_callback( """ async function(spec, section, gemini) { if (!gemini) { return null; } try { const c = document.createElement('canvas'); c.width = 384; c.height = 384; const x = c.getContext('2d'); x.fillStyle = '#ffffff'; x.fillRect(0, 0, 384, 384); const im = new Image(); im.src = '/assets/petri/' + spec + '.svg'; await (im.decode ? im.decode().catch(() => {}) : new Promise(r => { im.onload = r; im.onerror = r; })); x.drawImage(im, 92, 92, 200, 200); const cx = 192, cy = 192; const a0 = (section[0] - 90) * Math.PI / 180, a1 = (section[1] - 90) * Math.PI / 180; x.beginPath(); x.moveTo(cx, cy); x.arc(cx, cy, 150, a0, a1); x.closePath(); x.fillStyle = 'rgba(232,89,12,0.35)'; x.fill(); x.strokeStyle = '#e8590c'; x.lineWidth = 3; x.stroke(); return c.toDataURL('image/png'); } catch (e) { return null; } } """, Output("spl-marked", "data"), Input("spl-spec", "value"), Input("spl-section", "data"), Input("spl-gemini", "checked"), ) # --- THE ROLL (server-side, button-triggered) + optional Gemini sprite --- @callback( Output("spl-result", "data"), Input("spl-generate", "n_clicks"), State("spl-spec", "value"), State("spl-section", "data"), State("spl-pref", "value"), State("spl-gemini", "checked"), State("spl-marked", "data"), prevent_initial_call=True, ) def _spl_roll(n, spec, section, pref, gemini, marked): if not n: return no_update r1 = _pick(ADAPT_ODDS) if r1 == "nothing": return {"result": "nothing", "n": n} part = pref if r1 == "intended" else random.choice([p for p in PART_ORDER if p != pref]) width = (section[1] - section[0]) if (section and len(section) == 2) else 60 tier = _pick(_tier_odds_for_width(width)) if tier == "none": return {"result": r1, "part": part, "tier": "none", "fizzle": True, "n": n} sprite = _gemini_sprite(marked, part, tier, spec) if gemini else None return {"result": r1, "part": part, "tier": tier, "sprite": sprite, "n": n} # --- apply the rolled result to the loadout (clientside placement) --- clientside_callback( """ function(result, lo, spec, section) { const NU = window.dash_clientside.no_update; if (!result || !window.SPL) { return [NU, NU]; } const L = window.SPL.LABEL; if (result.result === 'nothing') { return [NU, 'Nothing took — the splice fizzled (33% odds).']; } if (result.fizzle) { return [NU, (result.result === 'wildcard' ? 'Wildcard ' : '') + (L[result.part] || result.part) + ' tried to form but rolled none — fizzled.']; } lo = lo || window.SPL.fresh(spec); const res = window.SPL.add(lo, result.part, result.tier, section, result.sprite || null); if (!res.placed) { return [NU, 'No room to place ' + (L[result.part] || result.part) + '.']; } const verb = result.result === 'wildcard' ? 'Wildcard! ' : ''; const where = window.SPL.isOmni(result.part, spec) ? ' (omnidirectional)' : ' at ' + Math.round((section[0] + section[1]) / 2) + '°'; const tag = result.sprite ? ' · Gemini sprite' : ''; return [res.lo, verb + (L[result.part] || result.part) + ' · ' + result.tier + ' spliced' + where + tag + '.']; } """, Output("spl-loadout", "data", allow_duplicate=True), Output("spl-outcome", "children"), Input("spl-result", "data"), State("spl-loadout", "data"), State("spl-spec", "value"), State("spl-section", "data"), prevent_initial_call=True, ) # --- remove a placed adaptation (raw onclick → hidden button → here) --- clientside_callback( """ function(n, lo) { const NU = window.dash_clientside.no_update; if (!n || !window.SPL || !lo) { return NU; } const id = window.__splRemoveTarget; window.__splRemoveTarget = null; if (!id) { return NU; } if (id === 'eyespot') { const out = JSON.parse(JSON.stringify(lo)); out.eyespot = {tier: 'none'}; return out; } return window.SPL.remove(lo, id); } """, Output("spl-loadout", "data", allow_duplicate=True), Input("spl-remove-btn", "n_clicks"), State("spl-loadout", "data"), prevent_initial_call=True, ) # --- Launch this config straight into the game (skips the spawn screen) --- # Hands off via localStorage (the game's pde-launch-check Interval consumes it), # then navigates. Translate the open list → the game's v2 loadout for now. clientside_callback( """ function(n, lo, spec, pModel, oModel) { const NU = window.dash_clientside.no_update; if (!n || !window.SPL) { return NU; } lo = lo || window.SPL.fresh(spec); const payload = {slug: spec, v2: window.SPL.toV2(lo), loadout: lo, eye: (lo.eyespot && lo.eyespot.tier) || 'none', models: {player: pModel || 'claude-haiku-4-5', opp: oModel || 'gemini-2.5-flash'}, // legacy field kept so an old game build still picks a sane provider agent: (String(pModel || '').indexOf('gemini') === 0 ? 'gemini' : 'claude')}; try { localStorage.setItem('pde_launch', JSON.stringify(payload)); } catch (e) {} return '/petri-dish-evolution'; } """, Output("url", "pathname", allow_duplicate=True), Input("spl-launch", "n_clicks"), State("spl-loadout", "data"), State("spl-spec", "value"), State("spl-model-player", "value"), State("spl-model-opp", "value"), prevent_initial_call=True, ) return component ``` :defaultExpanded: false :withExpandedButton: true --- *Source: /adaptation-splicer*