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}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
74#[serde(rename_all = "camelCase")]
75#[ts(export, export_to = "game/index.ts")]
76pub enum StepKind {
77    #[default]
78    Untap,
79    Upkeep,
80    Draw,
81    Main1,
82    CombatBegin,
83    CombatDeclareAttackers,
84    CombatDeclareBlockers,
85    CombatFirstStrikeDamage,
86    CombatDamage,
87    CombatEnd,
88    Main2,
89    EndOfTurn,
90    Cleanup,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
94#[serde(rename_all = "camelCase")]
95#[ts(export, export_to = "game/index.ts")]
96pub enum DayTime {
97    #[default]
98    Neither,
99    Day,
100    Night,
101}
102
103#[allow(clippy::large_enum_variant)]
104#[derive(Debug, Clone, Serialize, Deserialize, TS)]
105#[serde(
106    tag = "visibility",
107    rename_all = "camelCase",
108    rename_all_fields = "camelCase"
109)]
110#[ts(export, export_to = "game/index.ts")]
111pub enum CardView {
112    Visible(CardDto),
113    Hidden { id: String },
114}
115
116// One entry per (zone, owner) pair; battlefield cards are bucketed by controller.
117#[derive(Debug, Clone, Serialize, Deserialize, TS)]
118#[serde(rename_all = "camelCase")]
119#[ts(export, export_to = "game/index.ts")]
120pub struct ZoneDto {
121    pub zone: ZoneKind,
122    pub owner_id: String,
123    // Ordered top-first where order is public knowledge
124    // count can be > cards.len if hidden cards are present (library)
125    pub cards: Vec<CardView>,
126    pub count: usize,
127}
128
129#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
130#[serde(rename_all = "camelCase")]
131#[ts(export, export_to = "game/index.ts")]
132pub struct GameViewDto {
133    pub game_id: String,
134    pub turn: u32,
135    pub step: StepKind,
136    pub combat_assignments: Vec<CombatAssignmentDto>,
137    pub active_player_id: String,
138    pub priority_player_id: String,
139    pub players: Vec<PlayerDto>,
140    pub zones: Vec<ZoneDto>,
141    pub stack: Vec<StackObjectDto>,
142    pub game_over: bool,
143    pub winner_id: Option<String>,
144    pub monarch_id: Option<String>,
145    pub initiative_holder_id: Option<String>,
146    pub day_time: DayTime,
147    #[ts(optional)]
148    pub active_plane_names: Option<Vec<String>>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, TS)]
152#[serde(rename_all = "camelCase")]
153#[ts(export, export_to = "game/index.ts")]
154pub struct CombatAssignmentDto {
155    pub blocker_id: String,
156    pub attacker_id: String,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
160#[serde(rename_all = "camelCase")]
161#[ts(export, export_to = "game/index.ts")]
162pub enum PlayerStatus {
163    #[default]
164    Playing,
165    Lost,
166    Conceded,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, TS)]
170#[serde(rename_all = "camelCase")]
171#[ts(export, export_to = "game/index.ts")]
172pub struct DungeonStateDto {
173    pub name: String,
174    pub room: String,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, TS)]
178#[serde(rename_all = "camelCase")]
179#[ts(export, export_to = "game/index.ts")]
180pub struct PlayerDto {
181    pub id: String,
182    pub name: String,
183    pub status: PlayerStatus,
184    pub is_human: bool,
185    pub life: i32,
186    #[serde(default)]
187    pub max_hand_size: i32,
188    #[serde(default)]
189    pub unlimited_hand_size: bool,
190    #[serde(default)]
191    pub lands_played_this_turn: i32,
192    #[serde(default)]
193    pub max_land_plays_per_turn: i32,
194    #[serde(default)]
195    pub unlimited_land_plays: bool,
196    #[serde(default)]
197    pub cards_drawn_this_turn: i32,
198    #[serde(default)]
199    pub damage_prevention: i32,
200    #[serde(default)]
201    pub is_extra_turn: bool,
202    #[serde(default)]
203    pub extra_turn_count: u32,
204    #[ts(optional)]
205    pub controlled_by: Option<String>,
206    #[serde(default)]
207    pub player_keywords: Vec<String>,
208    #[serde(default)]
209    #[ts(type = "Record<string, number>")]
210    pub commander_casts: HashMap<String, u32>,
211    #[ts(optional)]
212    pub dungeon_state: Option<DungeonStateDto>,
213    #[ts(optional)]
214    pub active_scheme_names: Option<Vec<String>>,
215    #[ts(optional)]
216    pub team_number: Option<i32>,
217    pub counters: BTreeMap<PlayerCounterKind, u32>,
218    pub mana_pool: BTreeMap<ManaColor, u32>,
219    #[ts(type = "Record<string, number>")]
220    pub commander_damage: HashMap<String, i32>,
221    pub has_city_blessing: bool,
222    pub has_enduring_story: bool,
223    pub ring_level: i32,
224    pub speed: i32,
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
228#[serde(rename_all = "camelCase", default)]
229#[ts(export, export_to = "game/index.ts")]
230pub struct CardIdentity {
231    pub name: String,
232    pub set_code: String,
233    pub card_number: String,
234    pub is_token: bool,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    #[ts(optional)]
237    pub token_script: Option<TokenScript>,
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
241#[serde(rename_all = "camelCase", default)]
242#[ts(export, export_to = "game/index.ts")]
243pub struct ClassLevelDto {
244    pub level: i32,
245    pub oracle: String,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    #[ts(optional)]
248    pub cost: Option<String>,
249}
250
251#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
252#[serde(rename_all = "camelCase", default)]
253#[ts(export, export_to = "game/index.ts")]
254pub struct SagaChapterDto {
255    pub chapters: Vec<i32>,
256    pub oracle: String,
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
260#[serde(rename_all = "camelCase", default)]
261#[ts(export, export_to = "game/index.ts")]
262pub struct CardDto {
263    pub id: String,
264    pub identity: CardIdentity,
265    pub color: String,
266    pub mana_cost: String,
267    pub cmc: i32,
268    pub types: Vec<String>,
269    pub subtypes: Vec<String>,
270    pub supertypes: Vec<String>,
271    pub power: Option<String>,
272    pub toughness: Option<String>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    #[ts(optional)]
275    pub base_power: Option<i32>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    #[ts(optional)]
278    pub base_toughness: Option<i32>,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    #[ts(optional)]
281    pub final_chapter: Option<i32>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    #[ts(optional)]
284    pub class_level: Option<i32>,
285    pub class_levels: Vec<ClassLevelDto>,
286    pub saga_chapters: Vec<SagaChapterDto>,
287    pub text: String,
288    #[serde(default)]
289    pub choices: Vec<CardChoiceDto>,
290    pub controller_id: String,
291    pub owner_id: String,
292    pub tapped: bool,
293    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
294    pub is_crewed: bool,
295    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
296    pub is_attacking: bool,
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    #[ts(optional)]
299    pub attacking_player_id: Option<String>,
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    #[ts(optional)]
302    pub attack_target_id: Option<String>,
303    pub keywords: Vec<String>,
304    // Keyed by the engine's canonical `CounterType` display form ("P1P1",
305    // "Loyalty", one-off counter names uppercase); both producers must match it.
306    #[ts(type = "Record<string, number>")]
307    pub counters: BTreeMap<String, u32>,
308    pub damage: i32,
309    pub summoning_sick: bool,
310    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311    pub is_copy: bool,
312    pub is_double_faced: bool,
313    pub is_transformed: bool,
314    pub is_face_down: bool,
315    pub is_bestowed: bool,
316    pub phased_out: bool,
317    pub exerted: bool,
318    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
319    pub is_ring_bearer: bool,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    #[ts(optional)]
322    pub attached_to: Option<String>,
323    #[serde(default, skip_serializing_if = "Vec::is_empty")]
324    pub attachment_ids: Vec<String>,
325    // Mutate pile: the card ids merged under this top card.
326    #[serde(default, skip_serializing_if = "Vec::is_empty")]
327    pub merged_card_ids: Vec<String>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    #[ts(optional)]
330    pub flashback_cost: Option<String>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    #[ts(optional)]
333    pub kicker_cost: Option<String>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    #[ts(optional)]
336    pub effective_mana_cost: Option<String>,
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    #[ts(optional)]
339    pub commander_tax: Option<i32>,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    #[ts(optional)]
342    pub madness_cost: Option<String>,
343    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
344    pub is_madness_exiled: bool,
345    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
346    pub is_plotted: bool,
347    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
348    pub is_warp_exiled: bool,
349    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
350    pub foil: bool,
351    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
352    pub would_die_in_combat: bool,
353}
354
355#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
356#[serde(rename_all = "camelCase", default)]
357#[ts(export, export_to = "game/index.ts")]
358pub struct StackObjectDto {
359    pub id: String,
360    pub source_id: String,
361    pub controller_id: String,
362    pub owner_id: String,
363    pub identity: CardIdentity,
364    pub text: String,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    #[ts(optional)]
367    pub source_ability_text: Option<String>,
368    pub is_permanent_spell: bool,
369    pub is_casting: bool,
370    pub is_double_faced: bool,
371    pub face_index: u8,
372    pub targets: Vec<TargetRef>,
373}
374
375#[cfg(test)]
376mod stack_object_tests {
377    use super::{CardIdentity, StackObjectDto};
378
379    #[test]
380    fn serializes_card_face_fields() {
381        let stack_object = StackObjectDto {
382            id: "stack-1".into(),
383            source_id: "card-1".into(),
384            controller_id: "player-0".into(),
385            owner_id: "player-1".into(),
386            identity: CardIdentity::default(),
387            text: String::new(),
388            source_ability_text: Some("When this enters, draw a card.".into()),
389            is_permanent_spell: true,
390            is_casting: true,
391            is_double_faced: true,
392            face_index: 1,
393            targets: Vec::new(),
394        };
395
396        let value = serde_json::to_value(stack_object).unwrap();
397        assert_eq!(value["sourceAbilityText"], "When this enters, draw a card.");
398
399        assert_eq!(value["ownerId"], "player-1");
400        assert_eq!(value["isDoubleFaced"], true);
401        assert_eq!(value["faceIndex"], 1);
402    }
403
404    #[test]
405    fn defaults_missing_card_face_fields() {
406        let value = serde_json::json!({
407            "id": "stack-1",
408            "sourceId": "card-1",
409            "controllerId": "player-0"
410        });
411
412        let stack_object: StackObjectDto = serde_json::from_value(value).unwrap();
413
414        assert!(stack_object.owner_id.is_empty());
415        assert!(!stack_object.is_double_faced);
416        assert_eq!(stack_object.face_index, 0);
417    }
418}
419
420#[derive(
421    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
422)]
423#[serde(rename_all = "camelCase")]
424#[ts(export, export_to = "game/index.ts")]
425pub enum TargetingIntent {
426    #[default]
427    Damage,
428    Destroy,
429    Sacrifice,
430    Exile,
431    Bounce,
432    Mill,
433    Discard,
434    Counter,
435    Tap,
436    Untap,
437    Copy,
438    Buff,
439    Debuff,
440    Heal,
441    LoseLife,
442    Reveal,
443    Draw,
444    Fetch,
445    GainControl,
446    Fight,
447    Attach,
448    Attack,
449    Block,
450    Hostile,
451    Friendly,
452}
453
454#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
455#[serde(rename_all = "camelCase")]
456#[ts(export, export_to = "game/index.ts")]
457pub struct PlaymatSettings {
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    #[ts(optional)]
460    pub opacity: Option<f32>,
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    #[ts(optional)]
463    pub texture: Option<f32>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    #[ts(optional)]
466    pub border_width: Option<f32>,
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    #[ts(optional)]
469    pub border_color: Option<String>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    #[ts(optional)]
472    pub fit: Option<String>,
473    #[serde(default, skip_serializing_if = "Option::is_none")]
474    #[ts(optional)]
475    pub offset_x: Option<f32>,
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    #[ts(optional)]
478    pub offset_y: Option<f32>,
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    #[ts(optional)]
481    pub zoom: Option<f32>,
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    #[ts(optional)]
484    pub blur: Option<f32>,
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    #[ts(optional)]
487    pub brightness: Option<f32>,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    #[ts(optional)]
490    pub color: Option<String>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
494#[ts(export, export_to = "game/index.ts")]
495pub enum GameFormat {
496    Any,
497    Standard,
498    Pioneer,
499    Modern,
500    Legacy,
501    Vintage,
502    Pauper,
503    Premodern,
504    Commander,
505    Brawl,
506    Oathbreaker,
507    Draft,
508    Sealed,
509}
510
511#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
512#[ts(export, export_to = "game/index.ts")]
513pub enum EngineKind {
514    #[default]
515    Manabrew,
516    Forge,
517    Ironsmith,
518}