Skip to content

The Manabrew Protocol

Manabrew separates the engine (the rules) from the client (the UI). Any engine that speaks this protocol can drive the Manabrew client, and any client that implements it can host the Manabrew engine. This section documents the protocol so you can implement it for your own engine.

  • A transport-agnostic JSON message format — a client written against this spec can talk to any conforming engine.
  • Complete enough to drive interactive play: priority, targeting, combat, cost payment, library manipulation, dice, and the opening-hand procedure.
  • Card behaviour. The protocol carries already-resolved decisions; it does not say how the engine decides what’s legal or how spells resolve.
  • Card data. Oracle text and costs are rendered by the engine from its own database; image URLs, rulings, and printing metadata are fetched separately (e.g. from Scryfall), not transmitted here.
  • Replay or persistence formats.

The engine → client channel carries three separate kinds of message. They are never conflated:

state

A StateUpdate carrying the full gameView — the only carrier of game state. The client re-renders the board from it.

display

[⚠️ Work in Progress] A DisplayEvent — a transient, potentially very frequent UI display information. Carries no authoritative state. This could be used to sync up the fields, as well as the cursors positions of the players for multiplayer “livelyness”.

prompt

An AgentPrompt — a call to action. The engine has paused and needs a decision from a specific player. Carries no game state.

Prompts deliberately carry no gameView: the client already has the latest state from the most recent StateUpdate message.

Every prompt is wrapped in an AgentPrompt:

interface AgentPrompt {
promptId: number; // correlate the response back to this prompt
decidingPlayerId: string; // who must answer
sourceCardId?: string; // the card that caused this prompt, if any
input: PromptInput; // the discriminated union below
}

input is a discriminated union tagged by a type field. Each variant is one kind of decision — mulligan, chooseNumber, chooseCards, payManaCost, chooseAttackers, and so on. The pages in this section document one variant each.

The client replies with a PromptOutput — also a discriminated union tagged by type — echoing the promptId so the engine can match it. Each prompt page lists the exact response shape the engine expects.

// engine → client
{ "promptId": 7, "decidingPlayerId": "player-0", "input": { "type": "chooseNumber", ... } }
// client → engine
{ "promptId": 7, "output": { "type": "numberDecision", "chosenNumber": 3 } }

Every prompt the engine can send. Each links to its arguments, response shape, and a wire example.

Choices & information

Priority & costs

Mulligan

Combat & targeting

End

This protocol specification — every page under /protocol/ — is licensed Creative Commons Attribution 4.0 International (CC-BY-4.0), deliberately separate from the reference implementation, so that anyone may describe or implement the same wire format without depending on this repository.

The reference implementation (the rest of the project) is AGPL-3.0-or-later. Independent re-implementations of this protocol under any license are explicitly invited.