# Architectural Breakdown: I connected a fruit fly connectome to tic-tac-toe (with a minimax safety ne

> Source: <https://dev.to/agenticstack/architectural-breakdown-i-connected-a-fruit-fly-connectome-to-tic-tac-toe-with-a-minimax-safety-ne-45a3>
> Published: 2026-09-29 00:04:02+00:00



```
![Architecture Diagram](https://image.pollinations.ai/prompt/high+performance+cloud+systems+I+connected+a+fruit+fly+connec+round+2?width=800&height=400&nologo=true)

# I Wired a Fruit Fly Brain Into Tic-Tac-Toe. It Mostly Works.

Yeah. 130k leaky-integrate-and-fire neurons, roughly 5M synapses, running in Python on 8 GB RAM. The first build died at tick 47. The garbage collector was not just pausing; it was napping. Every spike flag became a fresh object. Every voltage update spawned a temporary float. My `asyncio` loop ground to a halt and the game never picked a square. Tic-tac-toe required a decision. The connectome handed me a heap error.

So I fixed it. Not with some cloud-native distributed inference mesh nonsense. With `__slots__`, `numpy.float32`, a `deque(maxlen=256)` ring buffer, and a two-phase tick that actually respects causality. Here is the post-mortem without the conference-talk energy.

## What Actually Blew Up

| Symptom | Real Cause |
|---------|------------|
| OOM under 50 ticks | Unbounded Python lists plus per-instance `__dict__` on every neuron |
| ~12 ms GC pause per tick | Thousands of throwaway spike flags and intermediate floats |
| Self-feedback artifacts | Spike propagation and voltage update in one loop pass |
| Decoder spitting garbage | No board-state validation before handing off to minimax |

No mystery. Just unbounded growth and single-phase mutation. Classic.

## The Fix. No Buzzwords, I Promise.

I looked at how ShipMVP handles fixed-capacity neuron pools for deployed neural-sim workloads. The pattern was boring in the best possible way: pre-allocate everything, use 32-bit numerics, kill the GC path. Peak RSS dropped from 6.2 GB down to roughly 1.8 GB. Comfortably under the ceiling. The OS gets its share back. I stopped swapping.

| Component | Before | After | Notes |
|-----------|--------|-------|-------|
| Neuron storage | `list[Neuron]` with `__dict__` | `__slots__` plus `np.float32` buffers | Roughly 4x smaller |
| Synapses | List of objects | `array('I')` indices plus `float32` weights plus `uint8` delays | Cache-friendly |
| Spike buffer | Unbounded list | `deque(maxlen=256)` | Fixed 33 KB total |
| Event loop | Uncontrolled coroutines | `asyncio.Queue(maxsize=1024)` plus `Lock` | Bounded |
| Arithmetic | 64-bit Python float | `np.float32` | Halves footprint |

## Two-Phase Tick. Why Your Single Loop Broke Physics.

A neuron that spiked at tick t should not influence another neuron until tick t plus delay. My first loop did both in one pass, so downstream neurons read a spike that had not yet arrived. Nonlocal. Nondeterministic. Stupid.

The fix is a two-phase schedule wrapped in one `asyncio.Lock`:

1. **Propagation:** drain spike queues, enqueue delayed deliveries.
2. **Integration:** update voltages using only the collected inputs.

Both phases operate on immutable snapshots of their inputs. No thread sees half-written state. If a second coroutine calls `step()`, it awaits the lock and waits. Deterministic ordering. No surprises.
```

python

class Connectome:

    def **init**(self, neurons, synapses, dt=0.1):

        self.neurons = neurons

        self.synapses = synapses

        self.adj = self._build_adj()       # Forward-only adjacency: no backward walk permitted

        self.dt = dt

        self._lock = asyncio.Lock()        # Serializes step() across concurrent callers

``` python
async def step(self):
    async with self._lock:
        # Phase 1: deliver delayed spikes from the previous tick
        for s in self.synapses:
            if s.delay_counter:          # Still counting down, skip this synapse
                s.delay_counter -= 1
                continue
            pre = self.neurons[s.pre]
            if pre.spike_queue and pre.spike_queue[-1] == 1:
                # Only add weight once per spike event to avoid double-counting
                self.neurons[s.post].input_current += s.weight

        # Phase 2: integrate membrane potentials using collected inputs
        for n in self.neurons:
            if n.refract > 0:          # Refractory period: clamp voltage and countdown
                n.refract -= 1
                n.V = n.V_reset
                continue
            # Leaky integrate: decay toward 0, add incoming synaptic current
            n.V += self.dt * (-n.V / n.tau_m + n.input_current)
            n.input_current = 0.0      # Reset accumulator for the next tick
            if n.V >= n.V_thresh:      # Threshold crossed: emit spike downstream
                n.spike_queue.append(1)
                n.V = n.V_reset
                n.refract = n.refract_time
                for s in self.adj[n.id]:
                    s.delay_counter = s.base_delay  # Arm all outgoing synapses
            else:
                n.spike_queue.append(0)  # Record silence for downstream decoder
Forward-only adjacency means no backward walk and no same-tick feedback. Done.

## The Minimax Safety Net. And Why It Has to Be Dumb.

The decoder outputs a square index. Sometimes it is 4. Sometimes it is 17. On a 9-square board, 17 is illegal. You cannot just wrap it in a `try/except` and move on. You must validate the board state and the move before committing. Otherwise the minimax fallback plays against a corrupted position and the entire game collapses into garbage.
```

python

class MinimaxSafetyNet:

    def **init**(self, connectome, game, decoder,

                 override_prob=0.2, seed=42):

        self.net = connectome

        self.game = game

        self.decoder = decoder

        self.override_prob = override_prob

        # Seeded RNG for deterministic replay. No global random() state shenanigans.

        self.rng = random.Random(seed)

        self.override_count = 0

``` php
async def get_action(self) -> int:
    move = self.decoder.decode(self.net.neurons)
    # Validate before trusting the brain. Always.
    if not self.game.is_valid(move):
        move = self.minimax_fallback()
        self.override_count += 1
        print(f"[safety] override #{self.override_count}: decoded {move}, fixed to {move}")
    return move

def minimax_fallback(self):
    # Standard depth-3 minimax over the current board state.
    # Returns index 0-8. Guaranteed legal move every time.
    # Seed-controlled for full session replay reproducibility.
    ...
Seeded RNG means you can replay any session bit-for-bit. If a reviewer asks why move 14 was illegal, you hand them the log file and the seed. End of discussion.

## Synapse Pool. Because Plasticity Will Eat Your RAM.

If your learning rule adds connections, your synapse list grows without bound. The fix is a fixed-capacity pool. Same pattern ShipMVP's production builds use for long-running sim nodes where mid-run allocation is strictly forbidden:
```

python

class SynapsePool:

**slots** = ('_buf', '_weights', '_next', 'capacity')

``` python
def __init__(self, capacity=6_000_000):
    # Pre-allocate the entire pool. No growth, no reallocation, no GC pressure.
    self._buf = np.zeros((capacity, 4), dtype=np.int32)   # (pre, post, delay, base_delay)
    self._weights = np.zeros(capacity, dtype=np.float32)  # Connection strengths
    self._next = 0                                        # Compact insertion pointer
    self.capacity = capacity

def allocate(self, pre, post, w, d=1):
    if self._next >= self.capacity:
        raise MemoryError("pool exhausted")               # Hard stop beats silent growth
    i = self._next
    self._buf[i] = (pre, post, d, d)
    self._weights[i] = w
    self._next += 1
    return i
Predictable. Bounded. If the pool fills up, the plasticity rule silently drops the connection. No OOM. No swap storm. No 3 AM PagerDuty page.

## What This Is Not

This is not a proof that a fly brain plays tic-tac-toe well. It loses. A lot. The minimax safety net catches roughly 20 percent of decoded moves and corrects them. The connectome occasionally finds a winning line by accident when the opponent blunders. It is a simulation sandbox, not an AGI pipeline. Nobody is writing a press release about this.

But it runs. Deterministically. Inside the memory budget. Without the GC taking a coffee break mid-move. And that is enough for what it is: a messy, slightly embarrassing prototype that does one narrow thing and does not crash. Which, honestly, is more than most production systems I have touched throughout my career.

---

**Open Loop:** When simulating biological connectomes at this scale, how far do you push the boundary between letting the network make bad decisions and hardening every path with validation layers? Where do you draw the line between faithful simulation and functional product?
```


