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.
GameViewDto
Section titled “GameViewDto”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;}#[serde(rename_all = "camelCase")]pub struct GameViewDto { pub game_id: String, pub turn: u32, pub step: StepKind, pub combat_assignments: Vec<CombatAssignmentDto>, pub active_player_id: String, pub priority_player_id: String, pub players: Vec<PlayerDto>, pub zones: Vec<ZoneDto>, pub stack: Vec<StackObjectDto>, pub game_over: bool, pub winner_id: Option<String>, pub monarch_id: Option<String>, pub initiative_holder_id: Option<String>, pub day_time: DayTime,}
References: CombatAssignmentDto , DayTime , PlayerDto , StackObjectDto , StepKind , ZoneDto
PlayerDto
Section titled “PlayerDto”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;}#[serde(rename_all = "camelCase")]pub struct PlayerDto { pub id: String, pub name: String, pub status: PlayerStatus, pub is_human: bool, pub life: i32, pub counters: BTreeMap<PlayerCounterKind, u32>, pub mana_pool: BTreeMap<ManaColor, u32>, pub commander_damage: HashMap<String, i32>, pub has_city_blessing: bool, pub ring_level: i32, pub speed: i32,}
References: ManaColor , PlayerCounterKind , PlayerStatus
ZoneDto and CardView — visibility
Section titled “ZoneDto and CardView — visibility”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;}// One entry per (zone, owner) pair; battlefield cards are bucketed by controller.#[serde(rename_all = "camelCase")]pub struct ZoneDto { pub zone: ZoneKind, pub owner_id: String, // Ordered top-first where order is public knowledge // count can be > cards.len if hidden cards are present (library) pub cards: Vec<CardView>, pub count: usize,}
References: CardView , ZoneKind
type CardView = | ({ visibility: "visible" } & CardDto) | { visibility: "hidden"; id: string };#[allow(clippy::large_enum_variant)]#[serde( tag = "visibility", rename_all = "camelCase", rename_all_fields = "camelCase")]pub enum CardView { Visible(CardDto), 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
countalone. - Library —
countonly, 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
hiddenentry 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— blankidentity, no text or costs — while public state (counters, tapped, power/toughness) stays. Clients render a face-down card with an emptyidentity.nameas a card back.
StepKind
Section titled “StepKind”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";#[serde(rename_all = "camelCase")]pub enum StepKind { #[default] Untap, Upkeep, Draw, Main1, CombatBegin, CombatDeclareAttackers, CombatDeclareBlockers, CombatFirstStrikeDamage, CombatDamage, CombatEnd, Main2, EndOfTurn, Cleanup,}CardDto
Section titled “CardDto”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;}#[serde(rename_all = "camelCase", default)]pub struct CardDto { pub id: String, pub identity: CardIdentity, pub color: String, pub mana_cost: String, pub cmc: i32, pub types: Vec<String>, pub subtypes: Vec<String>, pub supertypes: Vec<String>, pub power: Option<String>, pub toughness: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub base_power: Option<i32>, #[serde(skip_serializing_if = "Option::is_none")] pub base_toughness: Option<i32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub final_chapter: Option<i32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub class_level: Option<i32>, pub class_levels: Vec<ClassLevelDto>, pub saga_chapters: Vec<SagaChapterDto>, pub text: String, pub controller_id: String, pub owner_id: String, pub tapped: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_crewed: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_attacking: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub attacking_player_id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub attack_target_id: Option<String>, pub keywords: Vec<String>, // Keyed by the engine's canonical `CounterType` display form ("P1P1", // "Loyalty", one-off counter names uppercase); both producers must match it. pub counters: BTreeMap<String, u32>, pub damage: i32, pub summoning_sick: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_copy: bool, pub is_double_faced: bool, pub is_transformed: bool, pub is_face_down: bool, pub is_bestowed: bool, pub phased_out: bool, pub exerted: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_ring_bearer: bool, #[serde(skip_serializing_if = "Option::is_none")] pub attached_to: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub attachment_ids: Vec<String>, // Mutate pile: the card ids merged under this top card. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub merged_card_ids: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")] pub flashback_cost: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub kicker_cost: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub effective_mana_cost: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub madness_cost: Option<String>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_madness_exiled: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_plotted: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub is_warp_exiled: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub foil: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub would_die_in_combat: bool,}
References: CardIdentity , ClassLevelDto , SagaChapterDto
CardIdentity
Section titled “CardIdentity”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;}#[serde(rename_all = "camelCase", default)]pub struct CardIdentity { pub name: String, pub set_code: String, pub card_number: String, pub is_token: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub token_script: Option<TokenScript>,}
References: TokenScript
CombatAssignmentDto
Section titled “CombatAssignmentDto”A declared block: which blocker is assigned to which attacker.
interface CombatAssignmentDto { blockerId: string; attackerId: string;}#[serde(rename_all = "camelCase")]pub struct CombatAssignmentDto { pub blocker_id: String, pub attacker_id: String,}StackObjectDto
Section titled “StackObjectDto”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>;}#[serde(rename_all = "camelCase", default)]pub struct StackObjectDto { pub id: String, pub source_id: String, pub controller_id: String, pub owner_id: String, pub identity: CardIdentity, pub text: String, pub is_permanent_spell: bool, pub is_casting: bool, pub is_double_faced: bool, pub face_index: u8, pub targets: Vec<TargetRef>,}
References: CardIdentity , TargetRef
TargetingIntent
Section titled “TargetingIntent”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"; Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,)]#[serde(rename_all = "camelCase")]pub enum TargetingIntent { #[default] 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,}