Skip to main content

manabrew_engine/player/
state.rs

1use std::collections::{BTreeSet, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use crate::ids::{CardId, PlayerId};
6
7use super::player_outcome::PlayerOutcome;
8use super::player_statistics::PlayerStatistics;
9
10/// Mutable game-state for a single player.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PlayerState {
13    pub id: PlayerId,
14    pub name: String,
15
16    pub life: i32,
17    pub starting_life: i32,
18    pub life_started_this_turn_with: i32,
19    pub life_gained_this_turn: i32,
20    pub life_gained_by_team_this_turn: i32,
21    pub life_gained_times_this_turn: i32,
22    pub life_lost_this_turn: i32,
23    pub life_lost_last_turn: i32,
24
25    pub poison_counters: i32,
26
27    pub lands_played_this_turn: i32,
28    pub lands_played_last_turn: i32,
29    pub max_land_plays_per_turn: i32,
30    pub spells_cast_this_turn: i32,
31    pub spells_cast_last_turn: i32,
32    pub spells_cast_this_game: i32,
33    pub cards_cast_this_turn: Vec<CardId>,
34
35    pub max_hand_size: i32,
36    pub starting_hand_size: i32,
37    pub unlimited_hand_size: bool,
38
39    pub drawn_this_turn: i32,
40    pub drawn_last_turn: i32,
41    pub drawn_this_draw_step: i32,
42    pub tried_to_draw_from_empty_library: bool,
43    pub num_cards_in_hand_started_this_turn_with: i32,
44
45    pub has_lost: bool,
46    pub has_won: bool,
47    pub has_conceded: bool,
48    pub outcome: Option<PlayerOutcome>,
49
50    pub commander_damage_received: HashMap<u32, i32>,
51    pub commanders: Vec<CardId>,
52    pub commander_casts: HashMap<u32, u32>,
53    pub commander_damage_enabled: bool,
54
55    pub skip_turns: i32,
56    pub skip_next_draw: bool,
57    pub skip_next_combat: bool,
58    pub skip_next_untap: bool,
59
60    pub damage_prevention: i32,
61
62    pub energy_counters: i32,
63    // NOT IMPLEMENTED: experience counters (Commander sets — Meren, Mizzix,
64    // …) and ticket counters (Unfinity stickers). Neither is tracked on
65    // `PlayerState` yet, so `game_view_dto` serialises 0 for both. The DTO
66    // fields (`PlayerDto::experience_counters` / `ticket_counters`) and the UI
67    // badges already exist — the Java host populates them; wire real values
68    // here once these become engine state.
69    pub mana_shards: i32,
70
71    pub mana_expended_this_turn: i32,
72    pub surveilled_this_turn: i32,
73    pub tokens_created_this_turn: i32,
74    pub foretold_this_turn: i32,
75    pub investigated_this_turn: i32,
76    pub ventured_this_turn: i32,
77    pub sacrificed_this_turn: i32,
78    pub library_searched_this_turn: i32,
79
80    pub controlled_by: Option<PlayerId>,
81    pub team_number: i32,
82    pub unlimited_land_plays: bool,
83
84    pub has_city_blessing: bool,
85    pub ring_level: i32,
86    pub speed: i32,
87    pub speed_effect_card: Option<CardId>,
88    pub commander_effect_card: Option<CardId>,
89    pub monarch_effect_card: Option<CardId>,
90    pub initiative_effect_card: Option<CardId>,
91    pub blessing_effect_card: Option<CardId>,
92    pub radiation_effect_card: Option<CardId>,
93    pub ring_effect_card: Option<CardId>,
94    pub contraption_sprocket_effect_card: Option<CardId>,
95    pub keyword_effect_card: Option<CardId>,
96    pub planar_dice_effect_card: Option<CardId>,
97    pub companion_effect_card: Option<CardId>,
98    pub discarded_this_turn: i32,
99    pub explored_this_turn: i32,
100    pub assigned_damage_this_turn: i32,
101    pub assigned_combat_damage_this_turn: i32,
102    pub opponents_assigned_damage_this_turn: i32,
103    pub attacked_players_this_turn: Vec<PlayerId>,
104    pub attacked_players_last_turn: Vec<PlayerId>,
105    pub attacked_players_this_combat: Vec<PlayerId>,
106    pub been_dealt_combat_damage_since_last_turn: bool,
107    pub attractions_visited_this_turn: i32,
108    pub num_flips_this_turn: i32,
109    pub num_rolls_this_turn: i32,
110    pub dice_rolls_this_turn: Vec<i32>,
111    pub ring_bearer: Option<CardId>,
112    pub radiation_counters: i32,
113    pub permanents_left_battlefield_this_turn: i32,
114    pub lands_entered_battlefield_this_turn: i32,
115    pub permanents_put_into_graveyard_this_turn: i32,
116    pub completed_dungeons: Vec<CardId>,
117    pub notes: HashMap<String, Vec<String>>,
118    pub noted_num: HashMap<String, i32>,
119    pub tapped_land_for_mana_this_turn: bool,
120    pub committed_crime_this_turn: i32,
121    pub changed_keywords: Vec<String>,
122    #[serde(default)]
123    pub keywords_until_my_next_turn: Vec<String>,
124    #[serde(default)]
125    pub keywords_until_end_of_turn: Vec<String>,
126    pub maingame_card_mapping: HashMap<CardId, CardId>,
127    pub controlled_while_searching: BTreeSet<PlayerId>,
128    pub avatar_index: i32,
129    pub sleeve_index: i32,
130    pub crank_counter: i32,
131    pub additional_votes: HashMap<i64, i32>,
132    pub additional_optional_votes: HashMap<i64, i32>,
133    pub control_votes: BTreeSet<i64>,
134    pub additional_villainous_choices: HashMap<i64, i32>,
135    pub declares_attackers: BTreeSet<PlayerId>,
136    pub declares_blockers: BTreeSet<PlayerId>,
137    pub elemental_bend_triggers: BTreeSet<String>,
138    pub inbound_tokens: Vec<CardId>,
139    pub planeswalked_to_this_turn: Vec<CardId>,
140    pub lost_ownership: Vec<CardId>,
141    pub gained_ownership: Vec<CardId>,
142    pub paid_for_stack: Vec<crate::spellability::SpellAbility>,
143    pub devotion_mod: i32,
144    pub draft_notes: HashMap<String, String>,
145    pub statistics: PlayerStatistics,
146}
147
148impl PlayerState {
149    pub fn new(id: PlayerId, name: String, starting_life: i32) -> Self {
150        PlayerState {
151            id,
152            name,
153            life: starting_life,
154            starting_life,
155            life_started_this_turn_with: starting_life,
156            life_gained_this_turn: 0,
157            life_gained_by_team_this_turn: 0,
158            life_gained_times_this_turn: 0,
159            life_lost_this_turn: 0,
160            life_lost_last_turn: 0,
161            poison_counters: 0,
162            lands_played_this_turn: 0,
163            lands_played_last_turn: 0,
164            max_land_plays_per_turn: 1,
165            spells_cast_this_turn: 0,
166            spells_cast_last_turn: 0,
167            spells_cast_this_game: 0,
168            cards_cast_this_turn: Vec::new(),
169            max_hand_size: 7,
170            starting_hand_size: 7,
171            unlimited_hand_size: false,
172            drawn_this_turn: 0,
173            drawn_last_turn: 0,
174            drawn_this_draw_step: 0,
175            tried_to_draw_from_empty_library: false,
176            num_cards_in_hand_started_this_turn_with: 0,
177            has_lost: false,
178            has_won: false,
179            has_conceded: false,
180            outcome: None,
181            commander_damage_received: HashMap::new(),
182            commanders: Vec::new(),
183            commander_casts: HashMap::new(),
184            commander_damage_enabled: true,
185            skip_turns: 0,
186            skip_next_draw: false,
187            skip_next_combat: false,
188            skip_next_untap: false,
189            damage_prevention: 0,
190            energy_counters: 0,
191            mana_shards: 0,
192            mana_expended_this_turn: 0,
193            surveilled_this_turn: 0,
194            tokens_created_this_turn: 0,
195            foretold_this_turn: 0,
196            investigated_this_turn: 0,
197            ventured_this_turn: 0,
198            sacrificed_this_turn: 0,
199            library_searched_this_turn: 0,
200            controlled_by: None,
201            team_number: -1,
202            unlimited_land_plays: false,
203            has_city_blessing: false,
204            ring_level: 0,
205            speed: 0,
206            speed_effect_card: None,
207            commander_effect_card: None,
208            monarch_effect_card: None,
209            initiative_effect_card: None,
210            blessing_effect_card: None,
211            radiation_effect_card: None,
212            ring_effect_card: None,
213            contraption_sprocket_effect_card: None,
214            keyword_effect_card: None,
215            planar_dice_effect_card: None,
216            companion_effect_card: None,
217            discarded_this_turn: 0,
218            explored_this_turn: 0,
219            assigned_damage_this_turn: 0,
220            assigned_combat_damage_this_turn: 0,
221            opponents_assigned_damage_this_turn: 0,
222            attacked_players_this_turn: Vec::new(),
223            attacked_players_last_turn: Vec::new(),
224            attacked_players_this_combat: Vec::new(),
225            been_dealt_combat_damage_since_last_turn: false,
226            attractions_visited_this_turn: 0,
227            num_flips_this_turn: 0,
228            num_rolls_this_turn: 0,
229            dice_rolls_this_turn: Vec::new(),
230            ring_bearer: None,
231            radiation_counters: 0,
232            permanents_left_battlefield_this_turn: 0,
233            lands_entered_battlefield_this_turn: 0,
234            permanents_put_into_graveyard_this_turn: 0,
235            completed_dungeons: Vec::new(),
236            notes: HashMap::new(),
237            noted_num: HashMap::new(),
238            tapped_land_for_mana_this_turn: false,
239            committed_crime_this_turn: 0,
240            changed_keywords: Vec::new(),
241            keywords_until_my_next_turn: Vec::new(),
242            keywords_until_end_of_turn: Vec::new(),
243            maingame_card_mapping: HashMap::new(),
244            controlled_while_searching: BTreeSet::new(),
245            avatar_index: 0,
246            sleeve_index: 0,
247            crank_counter: 3,
248            additional_votes: HashMap::new(),
249            additional_optional_votes: HashMap::new(),
250            control_votes: BTreeSet::new(),
251            additional_villainous_choices: HashMap::new(),
252            declares_attackers: BTreeSet::new(),
253            declares_blockers: BTreeSet::new(),
254            elemental_bend_triggers: BTreeSet::new(),
255            inbound_tokens: Vec::new(),
256            planeswalked_to_this_turn: Vec::new(),
257            lost_ownership: Vec::new(),
258            gained_ownership: Vec::new(),
259            paid_for_stack: Vec::new(),
260            devotion_mod: 0,
261            draft_notes: HashMap::new(),
262            statistics: PlayerStatistics {
263                opening_hand_size: 7,
264                ..PlayerStatistics::default()
265            },
266        }
267    }
268
269    pub fn gain_life(&mut self, amount: i32) {
270        if amount <= 0 {
271            return;
272        }
273        self.life += amount;
274        self.life_gained_this_turn += amount;
275        self.life_gained_times_this_turn += 1;
276    }
277
278    pub fn lose_life(&mut self, amount: i32) {
279        if amount <= 0 {
280            return;
281        }
282        self.life -= amount;
283        self.life_lost_this_turn += amount;
284    }
285
286    pub fn set_life(&mut self, amount: i32) -> i32 {
287        let diff = amount - self.life;
288        self.life = amount;
289        if diff > 0 {
290            self.life_gained_this_turn += diff;
291            self.life_gained_times_this_turn += 1;
292        } else if diff < 0 {
293            self.life_lost_this_turn += diff.abs();
294        }
295        diff
296    }
297
298    pub fn deal_damage(&mut self, amount: i32) {
299        if amount > 0 {
300            self.lose_life(amount);
301        }
302    }
303
304    pub fn can_play_land(&self) -> bool {
305        self.lands_played_this_turn < self.max_land_plays_per_turn
306    }
307
308    pub fn is_alive(&self) -> bool {
309        !self.has_lost && !self.has_conceded
310    }
311
312    pub fn has_outcome(&self) -> bool {
313        self.outcome.is_some()
314    }
315
316    pub fn mark_lost(&mut self, outcome: PlayerOutcome) {
317        self.has_lost = true;
318        self.has_won = false;
319        self.has_conceded = matches!(outcome, PlayerOutcome::Conceded);
320        self.outcome = Some(outcome.clone());
321        self.statistics.set_outcome(Some(outcome));
322    }
323
324    pub fn mark_won(&mut self, outcome: PlayerOutcome) {
325        self.has_won = true;
326        self.has_lost = false;
327        self.has_conceded = false;
328        self.outcome = Some(outcome.clone());
329        self.statistics.set_outcome(Some(outcome));
330    }
331
332    pub fn clear_outcome(&mut self) {
333        self.has_lost = false;
334        self.has_won = false;
335        self.has_conceded = false;
336        self.outcome = None;
337        self.statistics.set_outcome(None);
338    }
339
340    pub fn reset_for_restart(&mut self) {
341        self.life = self.starting_life;
342        self.life_started_this_turn_with = self.starting_life;
343        self.life_gained_this_turn = 0;
344        self.life_gained_by_team_this_turn = 0;
345        self.life_gained_times_this_turn = 0;
346        self.life_lost_this_turn = 0;
347        self.life_lost_last_turn = 0;
348        self.poison_counters = 0;
349        self.lands_played_this_turn = 0;
350        self.lands_played_last_turn = 0;
351        self.max_land_plays_per_turn = 1;
352        self.spells_cast_this_turn = 0;
353        self.spells_cast_last_turn = 0;
354        self.spells_cast_this_game = 0;
355        self.cards_cast_this_turn.clear();
356        self.max_hand_size = 7;
357        self.starting_hand_size = 7;
358        self.unlimited_hand_size = false;
359        self.drawn_this_turn = 0;
360        self.drawn_last_turn = 0;
361        self.drawn_this_draw_step = 0;
362        self.tried_to_draw_from_empty_library = false;
363        self.num_cards_in_hand_started_this_turn_with = 0;
364        self.clear_outcome();
365        self.commander_damage_received.clear();
366        self.commanders.clear();
367        self.commander_casts.clear();
368        self.skip_turns = 0;
369        self.skip_next_draw = false;
370        self.skip_next_combat = false;
371        self.skip_next_untap = false;
372        self.damage_prevention = 0;
373        self.energy_counters = 0;
374        self.mana_shards = 0;
375        self.mana_expended_this_turn = 0;
376        self.surveilled_this_turn = 0;
377        self.tokens_created_this_turn = 0;
378        self.foretold_this_turn = 0;
379        self.investigated_this_turn = 0;
380        self.ventured_this_turn = 0;
381        self.sacrificed_this_turn = 0;
382        self.library_searched_this_turn = 0;
383        self.controlled_by = None;
384        self.unlimited_land_plays = false;
385        self.has_city_blessing = false;
386        self.ring_level = 0;
387        self.speed = 0;
388        self.speed_effect_card = None;
389        self.commander_effect_card = None;
390        self.monarch_effect_card = None;
391        self.initiative_effect_card = None;
392        self.blessing_effect_card = None;
393        self.radiation_effect_card = None;
394        self.ring_effect_card = None;
395        self.contraption_sprocket_effect_card = None;
396        self.keyword_effect_card = None;
397        self.planar_dice_effect_card = None;
398        self.companion_effect_card = None;
399        self.discarded_this_turn = 0;
400        self.explored_this_turn = 0;
401        self.assigned_damage_this_turn = 0;
402        self.assigned_combat_damage_this_turn = 0;
403        self.opponents_assigned_damage_this_turn = 0;
404        self.attacked_players_this_turn.clear();
405        self.attacked_players_last_turn.clear();
406        self.attacked_players_this_combat.clear();
407        self.been_dealt_combat_damage_since_last_turn = false;
408        self.attractions_visited_this_turn = 0;
409        self.num_flips_this_turn = 0;
410        self.num_rolls_this_turn = 0;
411        self.dice_rolls_this_turn.clear();
412        self.ring_bearer = None;
413        self.radiation_counters = 0;
414        self.permanents_left_battlefield_this_turn = 0;
415        self.lands_entered_battlefield_this_turn = 0;
416        self.permanents_put_into_graveyard_this_turn = 0;
417        self.completed_dungeons.clear();
418        self.notes.clear();
419        self.noted_num.clear();
420        self.tapped_land_for_mana_this_turn = false;
421        self.committed_crime_this_turn = 0;
422        self.changed_keywords.clear();
423        self.maingame_card_mapping.clear();
424        self.controlled_while_searching.clear();
425        self.avatar_index = 0;
426        self.sleeve_index = 0;
427        self.crank_counter = 3;
428        self.additional_votes.clear();
429        self.additional_optional_votes.clear();
430        self.control_votes.clear();
431        self.additional_villainous_choices.clear();
432        self.declares_attackers.clear();
433        self.declares_blockers.clear();
434        self.elemental_bend_triggers.clear();
435        self.inbound_tokens.clear();
436        self.planeswalked_to_this_turn.clear();
437        self.lost_ownership.clear();
438        self.gained_ownership.clear();
439        self.paid_for_stack.clear();
440        self.devotion_mod = 0;
441        self.draft_notes.clear();
442        self.statistics = PlayerStatistics::default();
443    }
444
445    pub fn new_turn(&mut self) {
446        self.statistics.next_turn();
447        self.lands_played_last_turn = self.lands_played_this_turn;
448        self.lands_played_this_turn = 0;
449        self.spells_cast_last_turn = self.spells_cast_this_turn;
450        self.spells_cast_this_turn = 0;
451        self.cards_cast_this_turn.clear();
452        self.life_started_this_turn_with = self.life;
453        self.life_lost_last_turn = self.life_lost_this_turn;
454        self.life_gained_this_turn = 0;
455        self.life_gained_by_team_this_turn = 0;
456        self.life_gained_times_this_turn = 0;
457        self.life_lost_this_turn = 0;
458        self.drawn_last_turn = self.drawn_this_turn;
459        self.drawn_this_turn = 0;
460        self.drawn_this_draw_step = 0;
461        self.mana_expended_this_turn = 0;
462        self.surveilled_this_turn = 0;
463        self.tokens_created_this_turn = 0;
464        self.foretold_this_turn = 0;
465        self.investigated_this_turn = 0;
466        self.ventured_this_turn = 0;
467        self.sacrificed_this_turn = 0;
468        self.library_searched_this_turn = 0;
469        self.discarded_this_turn = 0;
470        self.explored_this_turn = 0;
471        self.assigned_damage_this_turn = 0;
472        self.assigned_combat_damage_this_turn = 0;
473        self.opponents_assigned_damage_this_turn = 0;
474        self.attacked_players_last_turn = self.attacked_players_this_turn.clone();
475        self.attacked_players_this_turn.clear();
476        self.attacked_players_this_combat.clear();
477        self.been_dealt_combat_damage_since_last_turn = false;
478        self.attractions_visited_this_turn = 0;
479        self.num_flips_this_turn = 0;
480        self.num_rolls_this_turn = 0;
481        self.dice_rolls_this_turn.clear();
482        self.permanents_left_battlefield_this_turn = 0;
483        self.lands_entered_battlefield_this_turn = 0;
484        self.permanents_put_into_graveyard_this_turn = 0;
485        self.tapped_land_for_mana_this_turn = false;
486        self.committed_crime_this_turn = 0;
487        self.elemental_bend_triggers.clear();
488        self.planeswalked_to_this_turn.clear();
489        self.statistics.clear_turn_cache();
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn player_life() {
499        let mut p = PlayerState::new(PlayerId(0), "Alice".to_string(), 20);
500        assert_eq!(p.life, 20);
501        p.deal_damage(3);
502        assert_eq!(p.life, 17);
503        p.gain_life(2);
504        assert_eq!(p.life, 19);
505    }
506
507    #[test]
508    fn land_plays() {
509        let mut p = PlayerState::new(PlayerId(0), "Alice".to_string(), 20);
510        assert!(p.can_play_land());
511        p.lands_played_this_turn = 1;
512        assert!(!p.can_play_land());
513    }
514}