state
A StateUpdate carrying the full gameView — the only carrier of
game state. The client re-renders the board from it.
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.
The engine → client channel carries four 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.
error
A ProtocolError — the engine rejected the last response. See Errors.
The client → engine channel carries two:
response
A PromptOutput answering the open prompt, echoing its promptId. Strictly paired: one
response per prompt. See Answering a prompt.
directive
A DirectiveInput — out-of-band and fire-and-forget, never paired with a prompt. Currently
only concede, legal at any time.
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 sourceCard?: CardDto; // the complete card that caused this prompt, if any input: PromptInput; // the discriminated union below}sourceCard is carried inside the privately addressed prompt, so it remains
available even when the source is outside the recipient’s visible game state.
Image URLs are client-owned metadata and are not part of CardDto.
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.
Everything the client sends is one union, ClientToServerMessage:
type ClientToServerMessage = | { kind: "response"; promptId: number; action: PromptOutput } | { kind: "directive"; directive: DirectiveInput }; // out-of-band; currently { type: "concede" }A response answers the open prompt: action is a PromptOutput — a
discriminated union tagged by the same type as the prompt family — and
promptId MUST echo the prompt being answered. Each prompt page lists the
exact response shape the engine expects. A directive is fire-and-forget and
never paired with a prompt (conceding is legal at any time).
// engine → client{ "promptId": 7, "decidingPlayerId": "player-0", "input": { "type": "chooseNumber", ... } }
// client → engine{ "kind": "response", "promptId": 7, "action": { "type": "chooseNumber", "output": { "type": "numberDecision", "chosenNumber": 3 } } }The engine validates every response. A promptId that isn’t the open prompt,
an action whose family doesn’t match it, or an echoed action id the prompt
never advertised is rejected:
type ProtocolError = { code: ProtocolErrorCode; message: string; promptId?: number };
type ProtocolErrorCode = | "stalePrompt" | "wrongPlayer" | "wrongPromptType" | "unknownActionId" | "invalidShape";The rejected response is never applied. The engine re-sends the open prompt right after the error, so the client lets the player answer again.
promptId 0 is reserved for engine-synthesized absent-player defaults
(timeouts, disconnects) — clients MUST NOT send it.
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.