Skip to content

The game view

A StateUpdate carries one field, gameView - a complete GameViewDto snapshot of the game. It is the only carrier of authoritative game state: the client throws away its previous view and re-renders the board from each one.

The whole-game snapshot: turn structure, both players, the battlefield, the stack, and game-end state.

interface GameViewDto {
gameId: string;
turn: number;
step: StepKind;
combatAssignments: Array<CombatAssignmentDto>;
activePlayerId: string;
priorityPlayerId: string;
players: Array<PlayerDto>;
zones: Array<ZoneDto>;
stack: Array<StackObjectDto>;
gameOver: boolean;
winnerId: string | null;
monarchId: string | null;
initiativeHolderId: string | null;
dayTime: DayTime;
activePlaneNames?: Array<string>;
}

References: CombatAssignmentDto , DayTime , PlayerDto , StackObjectDto , StepKind , ZoneDto

One player’s scalar state - life, counters, mana pool, statuses. Cards live in the top-level zones list, not on the player.

interface PlayerDto {
id: string;
name: string;
status: PlayerStatus;
isHuman: boolean;
life: number;
maxHandSize: number;
unlimitedHandSize: boolean;
landsPlayedThisTurn: number;
maxLandPlaysPerTurn: number;
unlimitedLandPlays: boolean;
cardsDrawnThisTurn: number;
damagePrevention: number;
isExtraTurn: boolean;
extraTurnCount: number;
controlledBy?: string;
playerKeywords: Array<string>;
commanderCasts: Record<string, number>;
dungeonState?: DungeonStateDto;
activeSchemeNames?: Array<string>;
teamNumber?: number;
counters: { [key in PlayerCounterKind]?: number };
manaPool: { [key in ManaColor]?: number };
commanderDamage: Record<string, number>;
hasCityBlessing: boolean;
hasEnduringStory: boolean;
ringLevel: number;
speed: number;
}

References: DungeonStateDto , ManaColor , PlayerCounterKind , PlayerStatus

Cards arrive as one ZoneDto per (zone, owner) pair. Each zone’s cards are CardViews ordered top-first where the order is public knowledge, and count is the zone’s size.

interface ZoneDto {
zone: ZoneKind;
ownerId: string;
cards: Array<CardView>;
count: number;
}

References: CardView , ZoneKind

type CardView =
| ({ visibility: "visible" } & CardDto)
| { visibility: "hidden"; id: string };

References: CardDto

An engine that computes per-recipient views applies visibility per card:

  • Hands - visible entries only for the owner (or under reveal effects); other seats get the count alone.
  • Library - count only, plus the top card as a visible entry when the recipient may look at it (e.g. “play with the top card of your library revealed”).
  • Face-down exile (foretell and similar) - a hidden entry per card, so clients render an anonymous face-down card without learning its identity.
  • Face-down battlefield permanents (morphs) - the permanent itself is public. Recipients who may not see the face get a redacted CardDto - blank identity, no text or costs - while public state (counters, tapped, power/toughness) stays. Clients render a face-down card with an empty identity.name as a card back.

Where the turn currently stands - GameViewDto.step. Also referenced by chooseAction’s pass-until response.

type StepKind =
| "untap"
| "upkeep"
| "draw"
| "main1"
| "combatBegin"
| "combatDeclareAttackers"
| "combatDeclareBlockers"
| "combatFirstStrikeDamage"
| "combatDamage"
| "combatEnd"
| "main2"
| "endOfTurn"
| "cleanup";

The engine’s snapshot of a single card - every zone holds these. Only id and the card’s identity are needed to round-trip a choice; the rest drives richer rendering. The prompt pages that show cards reference this same type.

interface CardDto {
id: string;
identity: CardIdentity;
color: string;
manaCost: string;
cmc: number;
types: Array<string>;
subtypes: Array<string>;
supertypes: Array<string>;
power: string | null;
toughness: string | null;
basePower?: number;
baseToughness?: number;
finalChapter?: number;
classLevel?: number;
classLevels: Array<ClassLevelDto>;
sagaChapters: Array<SagaChapterDto>;
text: string;
choices: Array<CardChoiceDto>;
controllerId: string;
ownerId: string;
tapped: boolean;
isCrewed: boolean;
isAttacking: boolean;
attackingPlayerId?: string;
attackTargetId?: string;
keywords: Array<string>;
counters: Record<string, number>;
damage: number;
summoningSick: boolean;
isCopy: boolean;
isDoubleFaced: boolean;
isTransformed: boolean;
isFaceDown: boolean;
isBestowed: boolean;
phasedOut: boolean;
exerted: boolean;
isRingBearer: boolean;
attachedTo?: string;
attachmentIds: Array<string>;
mergedCardIds: Array<string>;
flashbackCost?: string;
kickerCost?: string;
effectiveManaCost?: string;
commanderTax?: number;
madnessCost?: string;
isMadnessExiled: boolean;
isPlotted: boolean;
isWarpExiled: boolean;
foil: boolean;
wouldDieInCombat: boolean;
}

References: CardChoiceDto , CardIdentity , ClassLevelDto , SagaChapterDto

Which card a CardDto or StackObjectDto is - name, printing (setCode/cardNumber), and whether it’s a token.

interface CardIdentity {
name: string;
setCode: string;
cardNumber: string;
isToken: boolean;
tokenScript?: TokenScript;
}

References: TokenScript

A declared block: which blocker is assigned to which attacker.

interface CombatAssignmentDto {
blockerId: string;
attackerId: string;
}

A spell or ability currently on the stack. Its targets are TargetRefs, each tagged with the TargetingIntent the engine inferred for it.

interface StackObjectDto {
id: string;
sourceId: string;
controllerId: string;
ownerId: string;
identity: CardIdentity;
text: string;
sourceAbilityText?: string;
isPermanentSpell: boolean;
isCasting: boolean;
isDoubleFaced: boolean;
faceIndex: number;
targets: Array<TargetRef>;
}

References: CardIdentity , TargetRef

A semantic hint describing what an effect intends to do to its target - the client uses it to pre-highlight sensible targets. Carried by TargetRef and by the targeting prompts.

type TargetingIntent =
| "damage"
| "destroy"
| "sacrifice"
| "exile"
| "bounce"
| "mill"
| "discard"
| "counter"
| "tap"
| "untap"
| "copy"
| "buff"
| "debuff"
| "heal"
| "loseLife"
| "reveal"
| "draw"
| "fetch"
| "gainControl"
| "fight"
| "attach"
| "attack"
| "block"
| "hostile"
| "friendly";

An engine may send a stateDelta instead of a state. It carries a patch against the last state the client applied, base (the fingerprint of that state) and fingerprint (the fingerprint of the result). An engine that sends patches also puts fingerprint on every full state.

To ensure correctness, it is recommended that a client should apply the patch only when base equals the fingerprint of the state it holds.

Patches are completely optional. An engine may always send full states. A conforming client should accept patches.

A patch has the same shape as the state it edits. Objects merge: each key in the patch is applied to the same key in the state, keys absent from the patch are untouched. Four reserved keys carry the rest:

KeyMeaning
$vLiteral replacement.
$dRemoval.
$kKeyed array edits. An object of element key to patch.
$oKeyed array order. The full list of element keys, in their new order.

Arrays whose elements all have a stable key are edited element by element under $k. The key is the element’s id; zone entries have none, so theirs is "<zone>/<ownerId>". $k on a key that does not exist inserts the element at the end. $o lists every key in the new order and is omitted when the order did not change. Any other array is replaced whole.

Scalars and arrays appear as themselves, so null is a value, not a deletion. An object that must replace instead of merge is wrapped in $v.

Player 1’s Lightning Bolt (card-7) resolves against player 0. The stack empties, the card goes on top of player 1’s graveyard, player 0 drops to 17 and gets priority back:

{
"kind": "stateDelta",
"base": "9f3c1a0e5b7d2461",
"fingerprint": "4d0e8b2a7c91f356",
"patch": {
"gameView": {
"priorityPlayerId": "player-0",
"players": {
"$k": { "player-0": { "life": 17 } }
},
"zones": {
"$k": {
"graveyard/player-1": {
"count": 2,
"cards": {
"$k": { "card-7": { "$v": { "visibility": "visible", "id": "card-7", ... } } },
"$o": ["card-7", "card-3"]
}
}
}
},
"stack": { "$d": ["card-7"], "$o": [] }
}
}
}

players keeps its order, so it has $k and no $o. The graveyard gains a new element, so its full CardView arrives under $v and $o puts it first. The stack loses its only element, so $o is empty.