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