A tic-tac-toe agent driven by a small circuit pulled out of the male
Drosophila CNS connectome (male-cns:v1.0, Berg et al. 2025 —
HHMI Janelia / University of Cambridge / MRC LMB / Google Research).
Instead of hand-writing an agent, this project:
- Pulls a real weighted, signed (excitatory/inhibitory) sub-circuit out of the fly's brain — descending neurons and the central-brain interneurons that drive them.
- Simulates activation flowing through that circuit.
- Reads the result off as a tic-tac-toe move.
- Wraps it with an exact minimax solver so it never actually loses.
The connectome doesn't "know" it's playing tic-tac-toe — there's no board-shaped input or output anywhere in the real fly. Everything about how a 3x3 board gets mapped onto ~100 real neurons is a deliberate engineering convention, documented below and in the code. That's the honest framing for this project: it's "a game agent with a biological circuit as its decision core," not "a fly brain that plays games."
🪰 Built for fun with the help of AI — a curiosity-driven experiment in seeing what a real fly-brain circuit can do when it plays tic-tac-toe.
- Dataset: male-cns:v1.0— male Drosophila melanogaster central nervous system connectome.
- Hosted on neuPrint at https://neuprint-cns.janelia.org(this dataset lives on a separate server from the generalneuprint.janelia.orginstance — see Setup).
- License: CC-BY.
- Collaboration: FlyEM (HHMI Janelia), University of Cambridge (Dept. of Zoology), MRC Laboratory of Molecular Biology, Google Research.
If you publish anything using this project's output, credit the dataset, not just this repo.
python -m flycns.main runs the full extraction pipeline and writes
everything under data/:
- Descending neurons — fetch all neurons with
superclass == "descending_neuron"(these are the brain's "motor commands," the neurons that carry decisions out of the brain).
- Upstream network — fetch everything that synapses onto those descending neurons.
- Strongest upstream partners — rank upstream neurons by total
synaptic weight, keep the top UPSTREAM_TOP_N(default 200), then restrict tosuperclass == "cb_intrinsic"(central-brain interneurons). The pipeline prints what fraction of the top-N survives this filter — descending neurons often get a lot of direct input from visual-projection or ascending neurons, so this can be smaller than you'd expect. Worth checking on a real run.
- Candidate outputs — rank the descending neurons this CB-intrinsic
set drives, by weight, keep the top OUTPUT_TOP_N(default 50).
- Output groups — group those descending neurons by cell type,
keep the N_OUTPUT_GROUPS(default 9) types most strongly driven by the CB core. One type = one board cell.
- Core circuit — build a graph from CB-intrinsic → selected-output edges, keep only its largest connected component. This is the circuit the game actually plays with.
- Neurotransmitter sign — each neuron's predicted transmitter
(predictedNt: acetylcholine → excitatory, GABA/glutamate → inhibitory, everything else → neutral) is attached to every edge assign/signed_weight, so the game simulation isn't treating every synapse as excitatory.
Output files (data/): selected_upstream_annotations.csv,
output_groups.csv, candidate_output_neurons.csv,
selected_output_neurons.csv, selected_output_groups.csv,
cb_output_matrix.csv, cb_intrinsic_to_descending.csv,
fly_core.graphml, fly_core_neurons.csv.
The saved core graph has no board-shaped input or output built in — the game layer adds a convention on top of it:
- Input: the core's cb_intrinsicneurons are split into 9 equal groups (sorted by bodyId). Group i is the injection site for board cell i. Occupying a cell with your own mark injects+1into that group; the opponent's mark injects-1; empty cells inject nothing.
- Simulation: a small leaky-integrator (a = decay·a + tanh(drive + A·a), run for a fixed number of steps) pushes that injection through the real signed adjacency matrix. Not a spiking model — the minimum viable way to get signal through the real wiring.
- Output: the 9 selected output-neuron types from step 5 above are the readout groups, in the same cell order. Whichever empty cell's group has the highest average activation is the move.
- Unbeatable filter: by default, an exact minimax solver
(minimax.py) computes every game-theoretically optimal move for the current position, and the circuit's activation only picks among those — it can choose which good move to make, never a losing one. Passunbeatable=FalsetoFlyBrainAgentto see the circuit play unconstrained.
uv syncCreate .env (see .env.example):
NEUPRINT_TOKEN=your_token_here
NEUPRINT_SERVER=https://neuprint-cns.janelia.org
NEUPRINT_DATASET=male-cns:v1.0Get a token by logging into the server above with a Google account and
copying it from your account page. male-cns:v1.0 lives on
neuprint-cns.janelia.org, not the general neuprint.janelia.org
server — if you see RuntimeError: Dataset 'male-cns:v1.0' does not exist, check you're pointed at the right host.
Run the extraction pipeline (fetches from neuPrint, writes data/):
uv run python -m flycns.main
# or: uv run flycns-tictactoePlay against the fly-brain agent:
uv run python -m flycns.game
# or: uv run flycns-playWatch two independent agents play each other:
from flycns.game import flybrain_vs_flybrain
flybrain_vs_flybrain()Two separate licenses apply here:
- This repository's code (everything under src/flycns/,main.py, etc.) is licensed under the MIT License — seeLICENSE. Use it, fork it, modify it, ship it, no strings beyond keeping the copyright notice.
- The male-cns connectome data used to generate anything in data/is licensed CC-BY by its creators (FlyEM/HHMI Janelia, University of Cambridge, MRC LMB, Google Research) and is not covered by this repo's MIT license. If you publish results, figures, or derived data from this project, credit the dataset separately — see Data source & license above.
- The input/output board mapping is an engineering convention, not biology. There's no real sensory pathway feeding "board state" into these CB-intrinsic neurons in this subgraph -- that was cut off further upstream during selection. Take any claim about "how the fly brain sees the board" with a grain of salt.
- The empty-board opening move is deterministic, not random -- with
no marks placed there's no signal to propagate, so the circuit's
activation is flat 0.0everywhere and ties are broken by the highest-cb_input_weightoutput group.
- With unbeatable=True, minimax -- not the connectome -- decides whether the agent wins. The circuit only breaks ties among already game-theoretically-optimal moves. Setunbeatable=Falseto see the raw circuit's choices with no safety net (it can and will lose).
- The leaky-integrator dynamics are illustrative, not biophysical.
No refractory periods, no real time constants, no spiking. Swapping
in something like Brian2 would be a reasonable next step if you want
actual spiking dynamics -- circuit.py's I/O convention doesn't depend on which simulator reads it.