Inspiration

Every agentic web demo I'd seen was an agent doing a chore for someone. You hand over the wheel, look away, and hope. That framing treats the human as an obstacle to route around.

Co-op games already solve this. In Portal 2's co-op campaign, the level design makes you need the other player. You can't brute-force past a partner — you have to talk to them.

So I built that, with an AI agent as player two. Not an assistant. A teammate who is genuinely stuck without you.

What it does

You control the human. An AI agent controls the robot.

Coloured pressure plates hold coloured doors open. Step off the plate and the door slams. The plate that opens your door is only reachable by the robot — and vice versa.

That's not decorative. A solver verifies it: on every level, the human cannot reach their goal if the robot never moves.

The agent gets six WebMCP tools — read the board, move the robot, hint, reset, change level, and speak to you.

What it cannot do is move your character. There is no tool for that. I never wrote one. So when it needs you on a plate, it has no mechanism except to ask and wait.

That missing tool is the whole design. It's what turns "watch an AI solve a puzzle" into "solve a puzzle with an AI."

Three levels, ending with one where you must walk into the robot's lane, hold its door with your body, then clear out before it can pass.

How we built it

Vanilla JavaScript. No framework, no build step, no dependencies. Five static files on Vercel.

levels.js — level data, movement rules, and a breadth-first solver over the joint state space: every combination of (human position, robot position). It runs unmodified in Node, so before designing a single pixel I could ask each candidate level two questions — is it solvable, and is it solvable with the robot frozen. I needed a yes and a no. Anything solvable single-handed got thrown out.

game.js — engine, renderer, input, and a built-in agent that drives the same public API the tools call. It recomputes the joint solution after every move, then executes only the robot's half. When the next optimal move is yours, it stops, marks your tile, and asks.

webmcp.js — six tools with schemas and descriptions written for a model, not a developer. Registration is defensive across five API shapes with a 10-second retry, since the surface is still moving between browsers.

The decision I'd defend hardest: the tool descriptions carry the game rules. get_game_state returns an ASCII board, a legend, and a plain-English rule string. An agent that's never seen this game plays it correctly first try, because the page taught it how.

Challenges we ran into

Making the agent a partner, not a cutscene. My first version computed the joint solution and executed all of it — moving both characters, finishing the level while I watched. Technically impressive, completely pointless. The fix: compute the joint plan, execute only your half. That's the difference between a demo and a game.

One CSS line that silently broke everything. I had .intro { position:fixed; inset:0; z-index:400; display:grid; } and closed the overlay with element.hidden = true. But display:grid overrides the hidden attribute. The overlay never went away — it sat at z-index:400 swallowing every click. The game underneath ran perfectly the whole time. Nothing reached it. I lost hours debugging game logic that had nothing wrong with it.

No way to test any of it. That bug got through because I was verifying code by reading it. No Puppeteer, no Playwright, not even jsdom available. So I wrote a minimal DOM shim from scratch — enough createElement, getElementById, classList and event dispatch to load the real files and click things programmatically. It caught a second bug in minutes: the restart button threw LS is not defined from an implicit global. Two real bugs in the first five minutes of having tests, in code I'd convinced myself was correct.

Accomplishments that we're proud of

The missing tool. The most important decision in this project is a function I never wrote. Restraint in a tool surface turned out more expressive than another capability.

Provable co-dependency. "You need each other" is easy to claim, hard to guarantee. The test suite searches from the human's start with the robot frozen and asserts the goal is unreachable — on every level. Design intent, verified by machine.

41 passing assertions on a hackathon project, using a DOM shim I had to write myself.

It degrades gracefully. Built-in agent, hands-free auto-solve, or a real WebMCP agent. The first two need no special browser — whatever a judge is running, it works.

What we learned

Semantic tools beat pixel-guessing by more than I expected. A screenshot agent would have to infer that an amber tile is a door, that an outlined square is a plate, and that they're causally linked. get_game_state just says so. Watching an agent play correctly first try made the WebMCP argument concrete in a way reading the spec hadn't.

Write tool descriptions for the model, not yourself. Rewriting them as instructions to a collaborator — "you control ONLY the robot, never the human" — measurably improved behaviour.

Test the wiring, not just the logic. Both bugs that cost me time lived in the seam between JavaScript and the DOM. The pure functions were fine. It was the glue that broke.

What's next for Lockstep

Asymmetric information — tiles only one of you can read, so the conversation carries real information instead of just coordination.

Latency as a mechanic — timed levels where the agent's thinking time becomes part of the puzzle.

A level editor with the solver attached, rejecting any level solvable alone at design time. Community levels with a machine-checked co-op guarantee.

Two-agent mode — the tool surface doesn't care who's holding it.

Beyond the game. Agent handles reach and speed, human holds the actions that need a person, both negotiating in the same view. That's the shape of any high-stakes agentic workflow. Lockstep is the smallest legible version of it.

Built With

Share this project:

Updates

Submission history