state
The Manabrew Protocol
Manabrew separates the engine (the rules) from the client (the UI). Any engine that speaks this protocol can drive the a Manabrew client, and any client that implements it can host a Manabrew compatible engine.
What we hope to achieve by putting effort and love into the spec, is that more people and projects start adopting it. We see huge potential in a truly open-standard for online card gaming, and we dream of a day where UIs and Engines for all sorts of games will be able to be inter-changed for a superior community driven ecosystem.
- Witches of the Hill
The protocol ensures:
Section titled “The protocol ensures:”- 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.
Enforcing the rules is up to the particular engine implementing the protocol.
Message families
Section titled “Message families”The engine → client channel carries four separate kinds of message:
prompt
An AgentPrompt - a call to action. The engine has paused and needs a decision from a
specific player.
engineDirective
An EngineDirective - an out-of-band notice for one player, never paired with a prompt and
never part of the game state. It reports the player’s action recording and an open vote to
repeat recorded actions. Each directive is the complete current status of its kind, so the
client keeps only the latest one.
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: concede
(legal at any time), startRecording and stopRecording (legal while the player holds priority
with an empty stack), requestRepeat (asks to repeat the recorded actions iterations times,
under the same condition), and repeatVote (answers an open repeat vote).
Prompts deliberately carry no gameView: the client already has the latest
state from the most recent StateUpdate message.
The prompt envelope
Section titled “The prompt envelope”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; // each prompt type defines the shape of its input}Answering a prompt
Section titled “Answering a prompt”type ClientToServerMessage = | { kind: "response"; promptId: number; action: PromptOutput } | { kind: "directive"; directive: DirectiveInput }; // out-of-band, never paired with a promptIt is important promptId is always the same as the last promptId emitted by the engine.
// engine → client{ "promptId": 7, "decidingPlayerId": "player-0", "input": { "type": "chooseNumber", ... } }
// client → engine{ "kind": "response", "promptId": 7, "action": { "type": "chooseNumber", "output": { "type": "numberDecision", "chosenNumber": 3 } } }Errors
Section titled “Errors”type ProtocolError = { code: ProtocolErrorCode; message: string; promptId?: number };
type ProtocolErrorCode = | "stalePrompt" | "wrongPlayer" | "wrongPromptType" | "unknownActionId" | "invalidShape";Display events
Section titled “Display events”DisplayEvent is retained in the Rust and TypeScript protocol packages for
independent implementations. The reference runtime does not currently emit it,
and the active StateUpdate transport does not carry display events.
type DisplayEvent = | { kind: "cardPlayed"; cardId: string; cardName: string; setCode: string; playerId: string; } | { kind: "turnChanged"; activePlayerId: string; activePlayerName: string; turnNumber: number; };#[serde( tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]pub enum DisplayEvent { CardPlayed { card_id: String, card_name: String, set_code: String, player_id: String, }, TurnChanged { active_player_id: String, active_player_name: String, turn_number: u32, },}The prompt catalogue
Section titled “The prompt catalogue”Every prompt the engine can send. Each links to its arguments, response shape, and a wire example.
Choices & information
- chooseNumber - pick a number in a range.
- chooseCards - select cards from a set.
- chooseColor - choose one or more colors.
- chooseBoolean - a yes/no decision.
- chooseFromSelection - pick spell modes.
- revealCards - acknowledge revealed cards.
- scry - sort cards into destination zones.
- reorder - put cards in a chosen order.
- diceRolled - acknowledge a dice roll.
Priority & costs
- chooseAction - take an action or pass priority.
- payManaCost - pay a mana cost interactively.
Mulligan
- mulligan - keep or mulligan the opening hand.
- mulliganPutBack - London-mulligan put-back.
Combat & targeting
- chooseAttackers - declare attackers.
- chooseBlockers - declare blockers.
- chooseDamageAssignmentOrder - order multiple blockers.
- chooseCombatDamageAssignment - assign combat damage.
- chooseBoardTargets - choose targets on the board.
End
- gameOver - the game has ended.
License
Section titled “License”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.