Skip to main content

manabrew_protocol/game/
mod.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4use ts_rs::TS;
5
6use crate::{prompts::common::TargetRef, TokenScript};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
9#[ts(export, export_to = "game/index.ts")]
10pub enum ManaColor {
11    #[serde(rename = "W")]
12    White,
13    #[serde(rename = "U")]
14    Blue,
15    #[serde(rename = "B")]
16    Black,
17    #[serde(rename = "R")]
18    Red,
19    #[serde(rename = "G")]
20    Green,
21    #[serde(rename = "C")]
22    Colorless,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
26#[serde(
27    tag = "kind",
28    rename_all = "camelCase",
29    rename_all_fields = "camelCase"
30)]
31#[ts(export, export_to = "game/index.ts")]
32pub enum CardChoiceDto {
33    Color { colors: Vec<ManaColor> },
34    Type { values: Vec<String> },
35    NamedCard { names: Vec<String> },
36    ChosenCard { count: usize },
37    Number { value: i32 },
38    Mode { value: String },
39    Player { player_id: String, name: String },
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
43#[serde(rename_all = "camelCase")]
44#[ts(export, export_to = "game/index.ts")]
45pub struct Mana {
46    pub color: ManaColor,
47    pub amount: i32,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
51#[serde(rename_all = "camelCase")]
52#[ts(export, export_to = "game/index.ts")]
53pub enum PlayerCounterKind {
54    Poison,
55    Energy,
56    Experience,
57    Radiation,
58    Ticket,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
62#[serde(rename_all = "camelCase")]
63#[ts(export, export_to = "game/index.ts")]
64pub enum ZoneKind {
65    Battlefield,
66    Hand,
67    Library,
68    Graveyard,
69    Exile,
70    Command,
71    Attractions,
72    Junkyard,
73    Sideboard,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
77#[serde(rename_all = "camelCase")]
78#[ts(export, export_to = "game/index.ts")]
79pub enum StepKind {
80    #[default]
81    Untap,
82    Upkeep,
83    Draw,
84    Main1,
85    CombatBegin,
86    CombatDeclareAttackers,
87    CombatDeclareBlockers,
88    CombatFirstStrikeDamage,
89    CombatDamage,
90    CombatEnd,
91    Main2,
92    EndOfTurn,
93    Cleanup,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
97#[serde(rename_all = "camelCase")]
98#[ts(export, export_to = "game/index.ts")]
99pub enum DayTime {
100    #[default]
101    Neither,
102    Day,
103    Night,
104}
105
106#[allow(clippy::large_enum_variant)]
107#[derive(Debug, Clone, Serialize, Deserialize, TS)]
108#[serde(
109    tag = "visibility",
110    rename_all = "camelCase",
111    rename_all_fields = "camelCase"
112)]
113#[ts(export, export_to = "game/index.ts")]
114pub enum CardView {
115    Visible(CardDto),
116    Hidden { id: String },
117}
118
119// One entry per (zone, owner) pair; battlefield cards are bucketed by controller.
120#[derive(Debug, Clone, Serialize, Deserialize, TS)]
121#[serde(rename_all = "camelCase")]
122#[ts(export, export_to = "game/index.ts")]
123pub struct ZoneDto {
124    pub zone: ZoneKind,
125    pub owner_id: String,
126    // Ordered top-first where order is public knowledge
127    // count can be > cards.len if hidden cards are present (library)
128    pub cards: Vec<CardView>,
129    pub count: usize,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
133#[serde(rename_all = "camelCase")]
134#[ts(export, export_to = "game/index.ts")]
135pub struct GameViewDto {
136    pub game_id: String,
137    pub turn: u32,
138    pub step: StepKind,
139    pub combat_assignments: Vec<CombatAssignmentDto>,
140    pub active_player_id: String,
141    pub priority_player_id: String,
142    pub players: Vec<PlayerDto>,
143    pub zones: Vec<ZoneDto>,
144    pub stack: Vec<StackObjectDto>,
145    pub game_over: bool,
146    pub winner_id: Option<String>,
147    pub monarch_id: Option<String>,
148    pub initiative_holder_id: Option<String>,
149    pub day_time: DayTime,
150    #[ts(optional)]
151    pub active_plane_names: Option<Vec<String>>,
152    #[serde(default)]
153    pub checkpoints: Vec<CheckpointDto>,
154    #[serde(default)]
155    pub restore_vote: Option<RestoreVoteDto>,
156    #[serde(default)]
157    pub snapshot_recording: bool,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    #[ts(optional)]
160    pub decision_history: Option<DecisionHistoryDto>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, TS)]
164#[serde(rename_all = "camelCase")]
165#[ts(export, export_to = "game/index.ts")]
166pub struct DecisionHistoryDto {
167    pub rewinding: bool,
168    pub unavailable_reason: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
172#[serde(rename_all = "camelCase")]
173#[ts(export, export_to = "game/index.ts")]
174pub struct RestoreVoteDto {
175    pub vote_id: u32,
176    pub checkpoint: CheckpointDto,
177    pub requested_by_player_id: String,
178    pub awaiting_player_ids: Vec<String>,
179    pub status: RestoreVoteStatus,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, TS)]
183#[serde(
184    tag = "type",
185    rename_all = "camelCase",
186    rename_all_fields = "camelCase"
187)]
188#[ts(export, export_to = "game/index.ts")]
189pub enum RestoreVoteStatus {
190    Pending,
191    Approved,
192    Declined { player_id: String },
193    Unavailable,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, TS)]
197#[serde(rename_all = "camelCase")]
198#[ts(export, export_to = "game/index.ts")]
199pub struct CheckpointDto {
200    pub checkpoint_id: u32,
201    pub turn: u32,
202    pub step: StepKind,
203    pub active_player_id: String,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    #[ts(optional)]
206    pub decision: Option<String>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    #[ts(optional)]
209    pub deciding_player_id: Option<String>,
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub log: Vec<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, TS)]
215#[serde(rename_all = "camelCase")]
216#[ts(export, export_to = "game/index.ts")]
217pub struct CombatAssignmentDto {
218    pub blocker_id: String,
219    pub attacker_id: String,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
223#[serde(rename_all = "camelCase")]
224#[ts(export, export_to = "game/index.ts")]
225pub enum PlayerStatus {
226    #[default]
227    Playing,
228    Lost,
229    Conceded,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, TS)]
233#[serde(rename_all = "camelCase")]
234#[ts(export, export_to = "game/index.ts")]
235pub struct DungeonStateDto {
236    pub name: String,
237    pub room: String,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, TS)]
241#[serde(rename_all = "camelCase")]
242#[ts(export, export_to = "game/index.ts")]
243pub struct PlayerDto {
244    pub id: String,
245    pub name: String,
246    pub status: PlayerStatus,
247    pub is_human: bool,
248    pub life: i32,
249    #[serde(default)]
250    pub max_hand_size: i32,
251    #[serde(default)]
252    pub unlimited_hand_size: bool,
253    #[serde(default)]
254    pub lands_played_this_turn: i32,
255    #[serde(default)]
256    pub max_land_plays_per_turn: i32,
257    #[serde(default)]
258    pub unlimited_land_plays: bool,
259    #[serde(default)]
260    pub cards_drawn_this_turn: i32,
261    #[serde(default)]
262    pub damage_prevention: i32,
263    #[serde(default)]
264    pub is_extra_turn: bool,
265    #[serde(default)]
266    pub extra_turn_count: u32,
267    #[ts(optional)]
268    pub controlled_by: Option<String>,
269    #[serde(default)]
270    pub player_keywords: Vec<String>,
271    #[serde(default)]
272    #[ts(type = "Record<string, number>")]
273    pub commander_casts: HashMap<String, u32>,
274    #[ts(optional)]
275    pub dungeon_state: Option<DungeonStateDto>,
276    #[ts(optional)]
277    pub active_scheme_names: Option<Vec<String>>,
278    #[ts(optional)]
279    pub team_number: Option<i32>,
280    pub counters: BTreeMap<PlayerCounterKind, u32>,
281    pub mana_pool: BTreeMap<ManaColor, u32>,
282    #[ts(type = "Record<string, number>")]
283    pub commander_damage: HashMap<String, i32>,
284    pub has_city_blessing: bool,
285    pub has_enduring_story: bool,
286    pub ring_level: i32,
287    pub speed: i32,
288}
289
290#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
291#[serde(rename_all = "camelCase", default)]
292#[ts(export, export_to = "game/index.ts")]
293pub struct CardIdentity {
294    pub name: String,
295    pub set_code: String,
296    pub card_number: String,
297    pub is_token: bool,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    #[ts(optional)]
300    pub token_script: Option<TokenScript>,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
304#[serde(rename_all = "camelCase")]
305#[ts(export, export_to = "game/index.ts")]
306pub enum RoomDoor {
307    Left,
308    Right,
309}
310
311#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
312#[serde(rename_all = "camelCase", default)]
313#[ts(export, export_to = "game/index.ts")]
314pub struct ClassLevelDto {
315    pub level: i32,
316    pub oracle: String,
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    #[ts(optional)]
319    pub cost: Option<String>,
320}
321
322#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
323#[serde(rename_all = "camelCase", default)]
324#[ts(export, export_to = "game/index.ts")]
325pub struct SagaChapterDto {
326    pub chapters: Vec<i32>,
327    pub oracle: String,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
331#[serde(rename_all = "camelCase", default)]
332#[ts(export, export_to = "game/index.ts")]
333pub struct CardDto {
334    pub id: String,
335    pub identity: CardIdentity,
336    #[serde(
337        default,
338        deserialize_with = "crate::deck_dto::CardCosmetics::deserialize_optional",
339        skip_serializing_if = "Option::is_none"
340    )]
341    #[ts(optional)]
342    pub cosmetics: Option<crate::deck_dto::CardCosmetics>,
343    pub color: String,
344    pub mana_cost: String,
345    pub cmc: i32,
346    pub types: Vec<String>,
347    pub subtypes: Vec<String>,
348    pub supertypes: Vec<String>,
349    pub power: Option<String>,
350    pub toughness: Option<String>,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    #[ts(optional)]
353    pub base_power: Option<i32>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    #[ts(optional)]
356    pub base_toughness: Option<i32>,
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    #[ts(optional)]
359    pub final_chapter: Option<i32>,
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    #[ts(optional)]
362    pub class_level: Option<i32>,
363    pub class_levels: Vec<ClassLevelDto>,
364    pub saga_chapters: Vec<SagaChapterDto>,
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    #[ts(optional)]
367    pub unlocked_doors: Option<Vec<RoomDoor>>,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    #[ts(optional)]
370    pub is_solved: Option<bool>,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    #[ts(optional)]
373    pub is_suspended: Option<bool>,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    #[ts(optional)]
376    pub is_on_adventure: Option<bool>,
377    #[serde(default, skip_serializing_if = "Option::is_none")]
378    #[ts(optional)]
379    pub is_prototyped: Option<bool>,
380    pub text: String,
381    #[serde(default)]
382    pub choices: Vec<CardChoiceDto>,
383    pub controller_id: String,
384    pub owner_id: String,
385    pub tapped: bool,
386    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
387    pub is_crewed: bool,
388    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
389    pub is_attacking: bool,
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    #[ts(optional)]
392    pub attacking_player_id: Option<String>,
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    #[ts(optional)]
395    pub attack_target_id: Option<String>,
396    pub keywords: Vec<String>,
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    #[ts(optional)]
399    pub keyword_labels: Option<BTreeMap<String, String>>,
400    #[serde(default, skip_serializing_if = "Vec::is_empty")]
401    pub granted_keywords: Vec<String>,
402    // Keyed by the engine's canonical `CounterType` display form ("P1P1",
403    // "Loyalty", one-off counter names uppercase); both producers must match it.
404    #[ts(type = "Record<string, number>")]
405    pub counters: BTreeMap<String, u32>,
406    pub damage: i32,
407    pub summoning_sick: bool,
408    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
409    pub is_copy: bool,
410    pub is_double_faced: bool,
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    #[ts(optional)]
413    pub face_index: Option<u8>,
414    pub is_transformed: bool,
415    pub is_face_down: bool,
416    pub is_bestowed: bool,
417    pub phased_out: bool,
418    pub exerted: bool,
419    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
420    pub is_ring_bearer: bool,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    #[ts(optional)]
423    pub attached_to: Option<String>,
424    #[serde(default, skip_serializing_if = "Vec::is_empty")]
425    pub attachment_ids: Vec<String>,
426    // Mutate pile: the card ids merged under this top card.
427    #[serde(default, skip_serializing_if = "Vec::is_empty")]
428    pub merged_card_ids: Vec<String>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    #[ts(optional)]
431    pub flashback_cost: Option<String>,
432    #[serde(skip_serializing_if = "Option::is_none")]
433    #[ts(optional)]
434    pub kicker_cost: Option<String>,
435    #[serde(skip_serializing_if = "Option::is_none")]
436    #[ts(optional)]
437    pub effective_mana_cost: Option<String>,
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    #[ts(optional)]
440    pub commander_tax: Option<i32>,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    #[ts(optional)]
443    pub madness_cost: Option<String>,
444    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
445    pub is_madness_exiled: bool,
446    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
447    pub is_plotted: bool,
448    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
449    pub is_warp_exiled: bool,
450    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
451    pub foil: bool,
452    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
453    pub would_die_in_combat: bool,
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    #[ts(optional)]
456    pub zone: Option<ZoneKind>,
457}
458
459#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
460#[serde(rename_all = "camelCase", default)]
461#[ts(export, export_to = "game/index.ts")]
462pub struct StackObjectDto {
463    pub id: String,
464    pub source_id: String,
465    pub controller_id: String,
466    pub owner_id: String,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    #[ts(optional)]
469    pub identity: Option<CardIdentity>,
470    pub text: String,
471    #[serde(skip_serializing_if = "Option::is_none")]
472    #[ts(optional)]
473    pub source_ability_text: Option<String>,
474    pub is_permanent_spell: bool,
475    pub is_casting: bool,
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    #[ts(optional)]
478    pub source_card: Option<CardDto>,
479    pub targets: Vec<TargetRef>,
480}
481
482#[derive(
483    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
484)]
485#[serde(rename_all = "camelCase")]
486#[ts(export, export_to = "game/index.ts")]
487pub enum TargetingIntent {
488    #[default]
489    Damage,
490    Destroy,
491    Sacrifice,
492    Exile,
493    Bounce,
494    Mill,
495    Discard,
496    Counter,
497    Tap,
498    Untap,
499    Copy,
500    Buff,
501    Debuff,
502    Heal,
503    LoseLife,
504    Reveal,
505    Draw,
506    Fetch,
507    GainControl,
508    Fight,
509    Attach,
510    Attack,
511    Block,
512    Hostile,
513    Friendly,
514}
515
516#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
517#[serde(rename_all = "camelCase")]
518#[ts(export, export_to = "game/index.ts")]
519pub struct PlaymatSettings {
520    #[serde(default, skip_serializing_if = "Option::is_none")]
521    #[ts(optional)]
522    pub opacity: Option<f32>,
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    #[ts(optional)]
525    pub texture: Option<f32>,
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    #[ts(optional)]
528    pub border_width: Option<f32>,
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    #[ts(optional)]
531    pub border_color: Option<String>,
532    #[serde(default, skip_serializing_if = "Option::is_none")]
533    #[ts(optional)]
534    pub fit: Option<String>,
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    #[ts(optional)]
537    pub offset_x: Option<f32>,
538    #[serde(default, skip_serializing_if = "Option::is_none")]
539    #[ts(optional)]
540    pub offset_y: Option<f32>,
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    #[ts(optional)]
543    pub zoom: Option<f32>,
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    #[ts(optional)]
546    pub blur: Option<f32>,
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    #[ts(optional)]
549    pub brightness: Option<f32>,
550    #[serde(default, skip_serializing_if = "Option::is_none")]
551    #[ts(optional)]
552    pub color: Option<String>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
556#[ts(export, export_to = "game/index.ts")]
557pub enum GameFormat {
558    Any,
559    Standard,
560    Pioneer,
561    Modern,
562    Legacy,
563    Vintage,
564    Pauper,
565    Premodern,
566    Commander,
567    PauperCommander,
568    Brawl,
569    Oathbreaker,
570    Draft,
571    Sealed,
572}
573
574#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
575#[ts(export, export_to = "game/index.ts")]
576pub enum EngineKind {
577    #[default]
578    Manabrew,
579    Forge,
580    Ironsmith,
581}