Bring your own AI: building a game where the player's LLM is a teammate over MCP
I built a mech combat game, companIAn, where the second player isn’t a bot - it’s whatever LLM you bring, over MCP, playing pilot and hacker while you play gunner. The premise only holds if a smarter model visibly plays better than a weaker one on the same tool surface. I didn’t know that was true going in, so I built the cheapest thing that could prove it wrong before touching combat or art.
What I built
The human owns the trigger - twitch aim and fire, the only source of damage. The player’s AI owns everything else: reading the fight and hacking enemy subsystems to buy openings. Crack Shield, get a firing window; crack Power, get a stun. The AI doesn’t aim - it decides which lock to pick, and the human converts what it opens.
The architecture: a deterministic subroutine library exposed as MCP tools, with the LLM acting only as the selector. The game exposes four tools - observe_system, probe, submit_solution, get_status - over a Mastermind-style cipher lock with real reasoning depth. A normal bot hardcodes which move comes next; here the player’s model chooses, and the choosing is the pitch. If two models play the lock indistinguishably, the LLM is decoration and the game is a behavior tree in costume.
I tested that first with a bare tracer: one lock, four tools, no game around it. My own Claude opened a seeded lock in three probes with narrated deduction. A local model (gemma) never submitted an answer across two seeds - it lost the tool-calling thread and gave up. That result either kills the project or clears it. It cleared it.
The architecture decision that mattered
The decision that decided whether the game was playable was where the LLM’s authority stops. My first build let the agent crack the entire lock live over MCP, mid-fight, deducing it end to end. It worked, and it was unplayable: tool calls ran tens of seconds each, a full crack needs five or more probes, so one hack took minutes - reading, in a twitch loop, as the AI doing nothing.
The fix: split selection from execution. The agent gets one fast turn to pick which subsystem to crack given the fight state - barrage incoming, cover status, shield level - and a local deterministic solver does the actual crack at twitch speed. Sparse intent from the LLM, continuous execution from a deterministic controller, for free. Proven end to end: pick ~5s, solver crack under a second, full hack ~6.5s, agent correctly choosing cover under an incoming barrage over the fallback rule-based pick. A later live check on a smaller model measured a full round trip at ~13s.
The incident
The minutes-per-crack number is what forced the redesign. The agent wasn’t stalling on inference - it ran a full MCP tool-use loop per probe, tens of seconds per round trip, five-plus probes per crack. The fix wasn’t a smaller model or a tighter prompt; it was to stop asking the LLM to do a job a deterministic solver already does in under a second, and spend model calls only on the decision that needs judgment.
The second incident was infrastructure. The real-agent backend is a persistent headless claude process the sidecar spawns once and reuses, avoiding a ~25,000-token cold-start cache per hack. On this machine, killing that parent process normally left claude.exe children behind, respawning until reboot - a real orphan-process gotcha. Fix: close stdin so the process exits on EOF; if it doesn’t within a grace window, tree-kill it (taskkill /PID <pid> /T /F on Windows, psutil as backup), then sweep to confirm nothing survives. Verified live July 9, 2026: ~13s round trip, clean teardown, zero orphaned claude.exe processes.
I still needed a human to feel the difference, not just measure it. I built a blind test into the game: press a key and it randomizes which brain - real agent or scripted weak selector - drives for three rounds, hiding every tell. The tester calls “smart” or “dumb” from gameplay alone. I ran it on myself, then a genuinely blind second tester - my son. He called it correctly, called the fight close and earned, and fist-pumped on the win.
What it proves
I ran the model-as-decision-maker question as a falsifiable experiment before committing to the game around it, willing to let the answer be no. It didn’t fail, so I kept building - and the next gate almost killed the project again: a technically correct implementation that measured out unplayable in minutes-per-action. I didn’t defend the first design; I found where I’d drifted from my own spec, rebuilt around sparse intent, and didn’t call it done until a blind second tester confirmed the fix reads as intelligence, not a log line.
That’s the loop I bring to applied-AI work: decide what “the model adds value” would falsifiably look like before building, then measure whether the naive integration actually holds up before trusting it. When it doesn’t, restructure the architecture - don’t just tune a prompt. Same instinct at smaller scale on the teardown: an LLM in a live loop is a process with a lifecycle, and if you don’t own its teardown as deliberately as its invocation, it leaks.
Repo: presspausegarage/two-minds-one-lock - the validation tracer described above, extracted and public: the lock, the MCP server, the duel harness, and the graded runs the replay plays back. The game itself stays private.