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;
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, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
26#[serde(rename_all = "camelCase")]
27#[ts(export, export_to = "game/index.ts")]
28pub struct Mana {
29    pub color: ManaColor,
30    pub amount: i32,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, TS)]
34#[serde(rename_all = "camelCase")]
35#[ts(export, export_to = "game/index.ts")]
36pub enum PlayerCounterKind {
37    Poison,
38    Energy,
39    Experience,
40    Radiation,
41    Ticket,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
45#[serde(rename_all = "camelCase")]
46#[ts(export, export_to = "game/index.ts")]
47pub enum ZoneKind {
48    Battlefield,
49    Hand,
50    Library,
51    Graveyard,
52    Exile,
53    Command,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, TS)]
57#[serde(rename_all = "camelCase")]
58#[ts(export, export_to = "game/index.ts")]
59pub enum StepKind {
60    #[default]
61    Untap,
62    Upkeep,
63    Draw,
64    Main1,
65    CombatBegin,
66    CombatDeclareAttackers,
67    CombatDeclareBlockers,
68    CombatFirstStrikeDamage,
69    CombatDamage,
70    CombatEnd,
71    Main2,
72    EndOfTurn,
73    Cleanup,
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 DayTime {
80    #[default]
81    Neither,
82    Day,
83    Night,
84}
85
86#[allow(clippy::large_enum_variant)]
87#[derive(Debug, Clone, Serialize, Deserialize, TS)]
88#[serde(
89    tag = "visibility",
90    rename_all = "camelCase",
91    rename_all_fields = "camelCase"
92)]
93#[ts(export, export_to = "game/index.ts")]
94pub enum CardView {
95    Visible(CardDto),
96    Hidden { id: String },
97}
98
99// One entry per (zone, owner) pair; battlefield cards are bucketed by controller.
100#[derive(Debug, Clone, Serialize, Deserialize, TS)]
101#[serde(rename_all = "camelCase")]
102#[ts(export, export_to = "game/index.ts")]
103pub struct ZoneDto {
104    pub zone: ZoneKind,
105    pub owner_id: String,
106    // Ordered top-first where order is public knowledge
107    // count can be > cards.len if hidden cards are present (library)
108    pub cards: Vec<CardView>,
109    pub count: usize,
110}
111
112#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
113#[serde(rename_all = "camelCase")]
114#[ts(export, export_to = "game/index.ts")]
115pub struct GameViewDto {
116    pub game_id: String,
117    pub turn: u32,
118    pub step: StepKind,
119    pub combat_assignments: Vec<CombatAssignmentDto>,
120    pub active_player_id: String,
121    pub priority_player_id: String,
122    pub players: Vec<PlayerDto>,
123    pub zones: Vec<ZoneDto>,
124    pub stack: Vec<StackObjectDto>,
125    pub game_over: bool,
126    pub winner_id: Option<String>,
127    pub monarch_id: Option<String>,
128    pub initiative_holder_id: Option<String>,
129    pub day_time: DayTime,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, TS)]
133#[serde(rename_all = "camelCase")]
134#[ts(export, export_to = "game/index.ts")]
135pub struct CombatAssignmentDto {
136    pub blocker_id: String,
137    pub attacker_id: String,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
141#[serde(rename_all = "camelCase")]
142#[ts(export, export_to = "game/index.ts")]
143pub enum PlayerStatus {
144    #[default]
145    Playing,
146    Lost,
147    Conceded,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, TS)]
151#[serde(rename_all = "camelCase")]
152#[ts(export, export_to = "game/index.ts")]
153pub struct PlayerDto {
154    pub id: String,
155    pub name: String,
156    pub status: PlayerStatus,
157    pub is_human: bool,
158    pub life: i32,
159    pub counters: BTreeMap<PlayerCounterKind, u32>,
160    pub mana_pool: BTreeMap<ManaColor, u32>,
161    #[ts(type = "Record<string, number>")]
162    pub commander_damage: HashMap<String, i32>,
163    pub has_city_blessing: bool,
164    pub ring_level: i32,
165    pub speed: i32,
166}
167
168#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
169#[serde(rename_all = "camelCase", default)]
170#[ts(export, export_to = "game/index.ts")]
171pub struct CardIdentity {
172    pub name: String,
173    pub set_code: String,
174    pub card_number: String,
175    pub is_token: bool,
176}
177
178#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
179#[serde(rename_all = "camelCase", default)]
180#[ts(export, export_to = "game/index.ts")]
181pub struct ClassLevelDto {
182    pub level: i32,
183    pub oracle: String,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    #[ts(optional)]
186    pub cost: Option<String>,
187}
188
189#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
190#[serde(rename_all = "camelCase", default)]
191#[ts(export, export_to = "game/index.ts")]
192pub struct SagaChapterDto {
193    pub chapters: Vec<i32>,
194    pub oracle: String,
195}
196
197#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
198#[serde(rename_all = "camelCase", default)]
199#[ts(export, export_to = "game/index.ts")]
200pub struct CardDto {
201    pub id: String,
202    pub identity: CardIdentity,
203    pub color: String,
204    pub mana_cost: String,
205    pub cmc: i32,
206    pub types: Vec<String>,
207    pub subtypes: Vec<String>,
208    pub supertypes: Vec<String>,
209    pub power: Option<String>,
210    pub toughness: Option<String>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    #[ts(optional)]
213    pub base_power: Option<i32>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    #[ts(optional)]
216    pub base_toughness: Option<i32>,
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    #[ts(optional)]
219    pub final_chapter: Option<i32>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    #[ts(optional)]
222    pub class_level: Option<i32>,
223    pub class_levels: Vec<ClassLevelDto>,
224    pub saga_chapters: Vec<SagaChapterDto>,
225    pub text: String,
226    pub controller_id: String,
227    pub owner_id: String,
228    pub tapped: bool,
229    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
230    pub is_crewed: bool,
231    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
232    pub is_attacking: bool,
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    #[ts(optional)]
235    pub attacking_player_id: Option<String>,
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    #[ts(optional)]
238    pub attack_target_id: Option<String>,
239    pub keywords: Vec<String>,
240    // Keyed by the engine's canonical `CounterType` display form ("P1P1",
241    // "Loyalty", one-off counter names uppercase); both producers must match it.
242    #[ts(type = "Record<string, number>")]
243    pub counters: BTreeMap<String, u32>,
244    pub damage: i32,
245    pub summoning_sick: bool,
246    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
247    pub is_copy: bool,
248    pub is_double_faced: bool,
249    pub is_transformed: bool,
250    pub is_face_down: bool,
251    pub is_bestowed: bool,
252    pub phased_out: bool,
253    pub exerted: bool,
254    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
255    pub is_ring_bearer: bool,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    #[ts(optional)]
258    pub attached_to: Option<String>,
259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
260    pub attachment_ids: Vec<String>,
261    // Mutate pile: the card ids merged under this top card.
262    #[serde(default, skip_serializing_if = "Vec::is_empty")]
263    pub merged_card_ids: Vec<String>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    #[ts(optional)]
266    pub flashback_cost: Option<String>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    #[ts(optional)]
269    pub kicker_cost: Option<String>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    #[ts(optional)]
272    pub effective_mana_cost: Option<String>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    #[ts(optional)]
275    pub madness_cost: Option<String>,
276    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
277    pub is_madness_exiled: bool,
278    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
279    pub is_plotted: bool,
280    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
281    pub is_warp_exiled: bool,
282    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
283    pub foil: bool,
284    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
285    pub would_die_in_combat: bool,
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
289#[serde(rename_all = "camelCase", default)]
290#[ts(export, export_to = "game/index.ts")]
291pub struct StackObjectDto {
292    pub id: String,
293    pub source_id: String,
294    pub controller_id: String,
295    pub identity: CardIdentity,
296    pub text: String,
297    pub is_permanent_spell: bool,
298    pub is_casting: bool,
299    pub targets: Vec<TargetRef>,
300}
301
302#[derive(
303    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS, strum_macros::Display,
304)]
305#[serde(rename_all = "camelCase")]
306#[ts(export, export_to = "game/index.ts")]
307pub enum TargetingIntent {
308    #[default]
309    Damage,
310    Destroy,
311    Sacrifice,
312    Exile,
313    Bounce,
314    Mill,
315    Discard,
316    Counter,
317    Tap,
318    Untap,
319    Copy,
320    Buff,
321    Debuff,
322    Heal,
323    LoseLife,
324    Reveal,
325    Draw,
326    Fetch,
327    GainControl,
328    Fight,
329    Attach,
330    Attack,
331    Block,
332    Hostile,
333    Friendly,
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
337#[serde(rename_all = "camelCase")]
338#[ts(export, export_to = "game/index.ts")]
339pub struct PlaymatSettings {
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    #[ts(optional)]
342    pub opacity: Option<f32>,
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    #[ts(optional)]
345    pub texture: Option<f32>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    #[ts(optional)]
348    pub border_width: Option<f32>,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    #[ts(optional)]
351    pub border_color: Option<String>,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    #[ts(optional)]
354    pub fit: Option<String>,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    #[ts(optional)]
357    pub offset_x: Option<f32>,
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    #[ts(optional)]
360    pub offset_y: Option<f32>,
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    #[ts(optional)]
363    pub zoom: Option<f32>,
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    #[ts(optional)]
366    pub blur: Option<f32>,
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    #[ts(optional)]
369    pub brightness: Option<f32>,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    #[ts(optional)]
372    pub color: Option<String>,
373}