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;
}

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;
counters: { [key in PlayerCounterKind]?: number };
manaPool: { [key in ManaColor]?: number };
commanderDamage: Record<string, number>;
hasCityBlessing: boolean;
ringLevel: number;
speed: number;
}

References: ManaColor , PlayerCounterKind , PlayerStatus

Cards arrive as one ZoneDto per (zone, owner) pair (battlefield entries are bucketed by controller). Each zone’s cards are CardViews ordered top-first where the order is public knowledge, and count is the zone’s true size: zones whose bulk is hidden from the recipient elide those entries, so count >= cards.length.

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 (see Transport) applies visibility per card:

  • Hands — visible entries only for the owner (or under reveal effects); other seats get the count alone.
  • Librarycount 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) — never hidden entries: 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;
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;
madnessCost?: string;
isMadnessExiled: boolean;
isPlotted: boolean;
isWarpExiled: boolean;
foil: boolean;
wouldDieInCombat: boolean;
}

References: 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;
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";