Skip to main content

manabrew_engine/player/
service.rs

1use std::collections::HashSet;
2
3use forge_foundation::ZoneType;
4
5use crate::card::CardInstance;
6use crate::event::RunParams;
7use crate::game::GameState;
8use crate::ids::{CardId, PlayerId};
9use crate::player::player_factory_util::{add_trigger_ability, new_player_effect_card};
10use crate::player::{GameLossReason, PlayerOutcome, RegisteredPlayer};
11use crate::replacement::replacement_handler::{
12    apply_replacements, apply_replacements_with_agents, ReplacementEvent,
13};
14use crate::replacement::replacement_result::ReplacementResult;
15use crate::trigger::handler::TriggerHandler;
16use crate::trigger::TriggerType;
17
18impl GameState {
19    pub fn new_from_registered_players(players: &[RegisteredPlayer]) -> Self {
20        let names: Vec<&str> = players.iter().map(|p| p.name.as_str()).collect();
21        let starting_life = players.first().map(|p| p.starting_life).unwrap_or(20);
22        let mut game = Self::new(&names, starting_life);
23        for (idx, rp) in players.iter().enumerate() {
24            let pid = PlayerId(idx as u32);
25            let player = game.player_mut(pid);
26            player.name = rp.name.clone();
27            player.id = rp.id.unwrap_or(pid);
28            player.starting_life = rp.starting_life;
29            player.life = rp.starting_life;
30            player.starting_hand_size = rp.starting_hand;
31            player.max_hand_size = rp.max_hand_size;
32            player.statistics.notify_opening_hand_size(rp.starting_hand);
33            player.mana_shards = rp.mana_shards;
34            player.team_number = rp.team_number;
35            player.commander_damage_enabled = rp.commander_damage_enabled;
36        }
37        game
38    }
39
40    pub fn initialize_registered_player_cards(
41        &mut self,
42        player: PlayerId,
43        registered: &RegisteredPlayer,
44        cards: Vec<(CardInstance, ZoneType)>,
45        trigger_handler: Option<&mut TriggerHandler>,
46    ) {
47        for (mut card, zone) in cards {
48            card.set_owner(player);
49            card.set_controller(player);
50            let card_id = self.create_card(card);
51            self.move_card(card_id, zone, player);
52        }
53        self.initialize_player_commanders_from_registered(player, registered, trigger_handler);
54    }
55
56    fn remove_player_effect_card(&mut self, player: PlayerId, effect_id: Option<CardId>) {
57        let Some(effect_id) = effect_id else {
58            return;
59        };
60        let zone = self.card(effect_id).zone;
61        if zone != ZoneType::None {
62            self.remove_card_from_zone(zone, player, effect_id);
63        }
64        self.card_mut(effect_id).zone = ZoneType::None;
65    }
66
67    fn ensure_speed_effect(&mut self, player: PlayerId) -> Option<CardId> {
68        if self.player(player).speed == 0 {
69            if let Some(effect_id) = self.player(player).speed_effect_card {
70                self.remove_player_effect_card(player, Some(effect_id));
71                self.player_mut(player).speed_effect_card = None;
72            }
73            return None;
74        }
75        if let Some(effect_id) = self.player(player).speed_effect_card {
76            return Some(effect_id);
77        }
78
79        let mut effect = new_player_effect_card(player, "Start Your Engines!", None);
80        add_trigger_ability(
81            &mut effect,
82            "Mode$ LifeLostAll | ValidPlayer$ Opponent | TriggerZones$ Command | ActivationLimit$ 1 | PlayerTurn$ True | CheckSVar$ Count$YourSpeed | SVarCompare$ LT4 | Execute$ SpeedUp | TriggerDescription$ Whenever one or more opponents lose life during your turn, if your speed is less than 4, your speed increases by 1. This ability triggers only once each turn.",
83            [("SpeedUp", "DB$ ChangeSpeed")],
84        );
85
86        let effect_id = self.create_card(effect);
87        self.move_card(effect_id, ZoneType::Command, player);
88        self.player_mut(player).speed_effect_card = Some(effect_id);
89        Some(effect_id)
90    }
91
92    fn ensure_monarch_effect(&mut self, player: PlayerId) -> CardId {
93        if let Some(effect_id) = self.player(player).monarch_effect_card {
94            return effect_id;
95        }
96        let mut effect = new_player_effect_card(player, "The Monarch", None);
97        add_trigger_ability(
98            &mut effect,
99            "Mode$ Phase | Phase$ End of Turn | TriggerZones$ Command | ValidPlayer$ You | Execute$ DrawMonarchCard | TriggerDescription$ At the beginning of your end step, draw a card.",
100            [("DrawMonarchCard", "DB$ Draw | Defined$ You")],
101        );
102        add_trigger_ability(
103            &mut effect,
104            "Mode$ DamageDone | ValidSource$ Creature | ValidTarget$ You | CombatDamage$ True | TriggerZones$ Command | Execute$ GainMonarch | TriggerDescription$ Whenever a creature deals combat damage to you, its controller becomes the monarch.",
105            [(
106                "GainMonarch",
107                "DB$ BecomeMonarch | Defined$ TriggeredSourceController",
108            )],
109        );
110        let effect_id = self.create_card(effect);
111        self.move_card(effect_id, ZoneType::Command, player);
112        self.player_mut(player).monarch_effect_card = Some(effect_id);
113        effect_id
114    }
115
116    fn remove_monarch_effect(&mut self, player: PlayerId) {
117        let effect_id = self.player(player).monarch_effect_card;
118        self.remove_player_effect_card(player, effect_id);
119        self.player_mut(player).monarch_effect_card = None;
120    }
121
122    fn ensure_initiative_effect(&mut self, player: PlayerId) -> CardId {
123        if let Some(effect_id) = self.player(player).initiative_effect_card {
124            return effect_id;
125        }
126        let mut effect = new_player_effect_card(player, "The Initiative", None);
127        add_trigger_ability(
128            &mut effect,
129            "Mode$ DamageDoneOnceByController | ValidSource$ Player | ValidTarget$ You | CombatDamage$ True | TriggerZones$ Command | Execute$ TakeTheInitiative | TriggerDescription$ Whenever one or more creatures a player controls deal combat damage to you, that player takes the initiative.",
130            [(
131                "TakeTheInitiative",
132                "DB$ TakeInitiative | Defined$ TriggeredSource",
133            )],
134        );
135        add_trigger_ability(
136            &mut effect,
137            "Mode$ Phase | Phase$ Upkeep | TriggerZones$ Command | ValidPlayer$ You | Execute$ VentureUndercity | TriggerDescription$ At the beginning of your upkeep, venture into Undercity. | Secondary$ True",
138            [("VentureUndercity", "DB$ Venture | Dungeon$ Undercity")],
139        );
140        add_trigger_ability(
141            &mut effect,
142            "Mode$ TakesInitiative | ValidPlayer$ You | TriggerZones$ Command | Execute$ VentureUndercity | TriggerDescription$ Whenever you take the initiative, venture into Undercity.",
143            [("VentureUndercity", "DB$ Venture | Dungeon$ Undercity")],
144        );
145        let effect_id = self.create_card(effect);
146        self.move_card(effect_id, ZoneType::Command, player);
147        self.player_mut(player).initiative_effect_card = Some(effect_id);
148        effect_id
149    }
150
151    fn remove_initiative_effect(&mut self, player: PlayerId) {
152        let effect_id = self.player(player).initiative_effect_card;
153        self.remove_player_effect_card(player, effect_id);
154        self.player_mut(player).initiative_effect_card = None;
155    }
156
157    fn ensure_blessing_effect(&mut self, player: PlayerId) -> Option<CardId> {
158        if !self.player(player).has_city_blessing {
159            let effect_id = self.player(player).blessing_effect_card;
160            self.remove_player_effect_card(player, effect_id);
161            self.player_mut(player).blessing_effect_card = None;
162            return None;
163        }
164        if let Some(effect_id) = self.player(player).blessing_effect_card {
165            return Some(effect_id);
166        }
167        let effect = new_player_effect_card(player, "City's Blessing", None);
168        let effect_id = self.create_card(effect);
169        self.move_card(effect_id, ZoneType::Command, player);
170        self.player_mut(player).blessing_effect_card = Some(effect_id);
171        Some(effect_id)
172    }
173
174    fn ensure_radiation_effect(&mut self, player: PlayerId) -> Option<CardId> {
175        if self.player(player).radiation_counters <= 0 {
176            let effect_id = self.player(player).radiation_effect_card;
177            self.remove_player_effect_card(player, effect_id);
178            self.player_mut(player).radiation_effect_card = None;
179            return None;
180        }
181        if let Some(effect_id) = self.player(player).radiation_effect_card {
182            return Some(effect_id);
183        }
184        let mut effect = new_player_effect_card(player, "Radiation", None);
185        add_trigger_ability(
186            &mut effect,
187            "Mode$ Phase | Phase$ Main1 | ValidPlayer$ You | TriggerZones$ Command | Execute$ ProcessRadiation | TriggerDescription$ At the beginning of your precombat main phase, if you have any rad counters, mill that many cards. For each nonland card milled this way, you lose 1 life and a rad counter.",
188            [("ProcessRadiation", "DB$ InternalRadiation")],
189        );
190        let effect_id = self.create_card(effect);
191        self.move_card(effect_id, ZoneType::Command, player);
192        self.player_mut(player).radiation_effect_card = Some(effect_id);
193        Some(effect_id)
194    }
195
196    pub fn player_register_radiation_effect(
197        &mut self,
198        player: PlayerId,
199        trigger_handler: &mut TriggerHandler,
200    ) {
201        if let Some(effect_id) = self.ensure_radiation_effect(player) {
202            trigger_handler.register_active_trigger(self, effect_id);
203        }
204    }
205
206    pub fn player_cleanup_turn_state(&mut self, player: PlayerId) {
207        self.player_clear_damage_prevention(player);
208    }
209
210    pub fn player_mark_lost(&mut self, player: PlayerId, reason: GameLossReason) {
211        self.player_mut(player).mark_lost(PlayerOutcome::Loss {
212            reason,
213            spell_name: None,
214        });
215    }
216
217    pub fn player_mark_won(&mut self, player: PlayerId) {
218        self.player_mut(player).mark_won(PlayerOutcome::Win);
219    }
220
221    pub fn player_alt_win_by_spell_effect(
222        &mut self,
223        player: PlayerId,
224        source_name: Option<String>,
225    ) {
226        self.player_mut(player)
227            .mark_won(PlayerOutcome::AltWin { source_name });
228    }
229
230    pub fn player_concede(&mut self, player: PlayerId) {
231        self.player_mut(player).mark_lost(PlayerOutcome::Conceded);
232    }
233
234    pub fn player_clear_outcome(&mut self, player: PlayerId) {
235        self.player_mut(player).clear_outcome();
236    }
237
238    pub fn player_reset_for_restart(&mut self, player: PlayerId) {
239        self.player_mut(player).reset_for_restart();
240    }
241
242    pub fn player_set_controlled_by(&mut self, player: PlayerId, controller: Option<PlayerId>) {
243        self.player_mut(player).controlled_by = controller;
244    }
245
246    pub fn player_add_skip_turns(&mut self, player: PlayerId, amount: i32) {
247        self.player_mut(player).skip_turns += amount;
248    }
249
250    pub fn player_decrement_skip_turns(&mut self, player: PlayerId) {
251        let p = self.player_mut(player);
252        p.skip_turns = (p.skip_turns - 1).max(0);
253    }
254
255    pub fn player_set_monarch(
256        &mut self,
257        player: PlayerId,
258        trigger_handler: Option<&mut TriggerHandler>,
259    ) {
260        if let Some(previous) = self.monarch {
261            if previous != player {
262                self.remove_monarch_effect(previous);
263            }
264        }
265        self.monarch = Some(player);
266        let effect_id = self.ensure_monarch_effect(player);
267        if let Some(handler) = trigger_handler {
268            handler.register_active_trigger(self, effect_id);
269            handler.run_trigger(
270                TriggerType::BecomeMonarch,
271                RunParams {
272                    player: Some(player),
273                    ..Default::default()
274                },
275                false,
276            );
277        }
278    }
279
280    pub fn player_take_initiative(
281        &mut self,
282        player: PlayerId,
283        trigger_handler: Option<&mut TriggerHandler>,
284    ) {
285        if let Some(previous) = self.initiative_holder {
286            if previous != player {
287                self.remove_initiative_effect(previous);
288            }
289        }
290        self.initiative_holder = Some(player);
291        let effect_id = self.ensure_initiative_effect(player);
292        if let Some(handler) = trigger_handler {
293            handler.register_active_trigger(self, effect_id);
294            handler.run_trigger(
295                TriggerType::TakeInitiative,
296                RunParams {
297                    player: Some(player),
298                    ..Default::default()
299                },
300                false,
301            );
302        }
303    }
304
305    pub fn player_set_ring_bearer(&mut self, player: PlayerId, bearer: Option<CardId>) {
306        self.player_mut(player).ring_bearer = bearer;
307    }
308
309    pub fn player_set_blessing(&mut self, player: PlayerId, value: bool) {
310        self.player_mut(player).has_city_blessing = value;
311        self.ensure_blessing_effect(player);
312    }
313
314    pub fn player_ring_tempt(&mut self, player: PlayerId) {
315        let current = self.player(player).ring_level;
316        if current < 4 {
317            self.player_mut(player).ring_level = current + 1;
318        }
319    }
320
321    pub fn player_add_poison(&mut self, player: PlayerId, amount: i32) {
322        if amount > 0 {
323            self.player_mut(player).poison_counters += amount;
324        }
325    }
326
327    pub fn player_remove_poison(&mut self, player: PlayerId, amount: i32) {
328        if amount > 0 {
329            let p = self.player_mut(player);
330            p.poison_counters = (p.poison_counters - amount).max(0);
331        }
332    }
333
334    pub fn player_add_energy(&mut self, player: PlayerId, amount: i32) {
335        self.player_mut(player).energy_counters += amount;
336    }
337
338    pub fn player_can_pay_energy(&self, player: PlayerId, amount: i32) -> bool {
339        amount <= 0 || self.player(player).energy_counters >= amount
340    }
341
342    pub fn player_lose_energy(&mut self, player: PlayerId, amount: i32) -> i32 {
343        if amount <= 0 {
344            return 0;
345        }
346        let p = self.player_mut(player);
347        let paid = amount.min(p.energy_counters);
348        p.energy_counters -= paid;
349        paid
350    }
351
352    pub fn player_pay_energy(&mut self, player: PlayerId, amount: i32) -> bool {
353        if !self.player_can_pay_energy(player, amount) {
354            return false;
355        }
356        self.player_lose_energy(player, amount);
357        true
358    }
359
360    pub fn player_add_shards(&mut self, player: PlayerId, amount: i32) {
361        self.player_mut(player).mana_shards += amount;
362    }
363
364    pub fn player_can_pay_shards(&self, player: PlayerId, amount: i32) -> bool {
365        amount <= 0 || self.player(player).mana_shards >= amount
366    }
367
368    pub fn player_lose_shards(&mut self, player: PlayerId, amount: i32) -> i32 {
369        if amount <= 0 {
370            return 0;
371        }
372        let p = self.player_mut(player);
373        let paid = amount.min(p.mana_shards);
374        p.mana_shards -= paid;
375        paid
376    }
377
378    pub fn player_pay_shards(&mut self, player: PlayerId, amount: i32) -> bool {
379        if !self.player_can_pay_shards(player, amount) {
380            return false;
381        }
382        self.player_lose_shards(player, amount);
383        true
384    }
385
386    pub fn player_add_radiation(&mut self, player: PlayerId, amount: i32) {
387        self.player_mut(player).radiation_counters += amount;
388        self.ensure_radiation_effect(player);
389    }
390
391    pub fn player_remove_radiation(&mut self, player: PlayerId, amount: i32) {
392        if amount > 0 {
393            let p = self.player_mut(player);
394            p.radiation_counters = (p.radiation_counters - amount).max(0);
395        }
396        self.ensure_radiation_effect(player);
397    }
398
399    pub fn player_set_radiation(&mut self, player: PlayerId, amount: i32) {
400        self.player_mut(player).radiation_counters = amount.max(0);
401        self.ensure_radiation_effect(player);
402    }
403
404    pub fn player_gain_life(&mut self, player: PlayerId, amount: i32) -> i32 {
405        if amount <= 0 {
406            return 0;
407        }
408        self.player_mut(player).gain_life(amount);
409        amount
410    }
411
412    pub fn player_can_gain_life(&self, player: PlayerId) -> bool {
413        !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_gain_life(self, player)
414    }
415
416    pub fn player_lose_life(&mut self, player: PlayerId, amount: i32) -> i32 {
417        if amount <= 0 {
418            return 0;
419        }
420        self.player_mut(player).lose_life(amount);
421        amount
422    }
423
424    pub fn player_deal_damage(&mut self, player: PlayerId, amount: i32) -> i32 {
425        if amount <= 0 {
426            return 0;
427        }
428        if crate::staticability::static_ability_cant_gain_lose_pay_life::cant_lose_life(
429            self, player,
430        ) {
431            return 0;
432        }
433        let mut event = ReplacementEvent::LifeReduced {
434            player,
435            amount,
436            is_damage: true,
437        };
438        let result = apply_replacements(self, &mut event);
439        if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
440            return 0;
441        }
442        let final_amount = if let ReplacementEvent::LifeReduced { amount, .. } = event {
443            amount
444        } else {
445            amount
446        };
447        if final_amount <= 0 {
448            return 0;
449        }
450        self.player_mut(player).deal_damage(final_amount);
451        final_amount
452    }
453
454    pub fn player_can_lose_life(&self, player: PlayerId) -> bool {
455        self.player(player).is_alive()
456    }
457
458    pub fn player_can_pay_life(&self, player: PlayerId, amount: i32) -> bool {
459        amount <= 0 || self.player(player).life >= amount
460    }
461
462    pub fn player_pay_life(&mut self, player: PlayerId, amount: i32) -> bool {
463        if !self.player_can_pay_life(player, amount) {
464            return false;
465        }
466        self.player_lose_life(player, amount);
467        true
468    }
469
470    pub fn player_set_life(&mut self, player: PlayerId, amount: i32) -> i32 {
471        self.player_mut(player).set_life(amount)
472    }
473
474    pub fn player_exchange_life_totals(&mut self, a: PlayerId, b: PlayerId) {
475        let life_a = self.player(a).life;
476        let life_b = self.player(b).life;
477        self.player_set_life(a, life_b);
478        self.player_set_life(b, life_a);
479    }
480
481    pub fn player_add_damage_prevention(&mut self, player: PlayerId, amount: i32) {
482        if amount > 0 {
483            self.player_mut(player).damage_prevention += amount;
484        }
485    }
486
487    pub fn player_clear_damage_prevention(&mut self, player: PlayerId) {
488        self.player_mut(player).damage_prevention = 0;
489    }
490
491    pub fn player_record_land_play(&mut self, player: PlayerId) {
492        self.player_mut(player).lands_played_this_turn += 1;
493    }
494
495    pub fn player_add_team_life_gained(&mut self, player: PlayerId, amount: i32) {
496        if amount > 0 {
497            self.player_mut(player).life_gained_by_team_this_turn += amount;
498        }
499    }
500
501    pub fn player_record_permanent_left_battlefield(&mut self, player: PlayerId) {
502        self.player_mut(player)
503            .permanents_left_battlefield_this_turn += 1;
504    }
505
506    pub fn player_record_landfall(&mut self, player: PlayerId) {
507        self.player_mut(player).lands_entered_battlefield_this_turn += 1;
508    }
509
510    pub fn player_record_permanent_put_into_graveyard(&mut self, player: PlayerId) {
511        self.player_mut(player)
512            .permanents_put_into_graveyard_this_turn += 1;
513    }
514
515    pub fn player_add_mana_expended(&mut self, player: PlayerId, amount: i32) {
516        if amount > 0 {
517            self.player_mut(player).mana_expended_this_turn += amount;
518        }
519    }
520
521    pub fn player_set_mana_expended(&mut self, player: PlayerId, amount: i32) {
522        self.player_mut(player).mana_expended_this_turn = amount.max(0);
523    }
524
525    pub fn player_record_discard(&mut self, player: PlayerId, amount: i32) {
526        if amount > 0 {
527            self.player_mut(player).discarded_this_turn += amount;
528        }
529    }
530
531    pub fn player_record_explore(&mut self, player: PlayerId, amount: i32) {
532        if amount > 0 {
533            self.player_mut(player).explored_this_turn += amount;
534        }
535    }
536
537    pub fn player_record_roll(&mut self, player: PlayerId, result: Option<i32>) {
538        let p = self.player_mut(player);
539        p.num_rolls_this_turn += 1;
540        if let Some(v) = result {
541            p.dice_rolls_this_turn.push(v);
542        }
543    }
544
545    pub fn player_record_attraction_visit(&mut self, player: PlayerId, amount: i32) {
546        self.player_mut(player).attractions_visited_this_turn += amount;
547    }
548
549    pub fn player_clear_skip_untap(&mut self, player: PlayerId) {
550        self.player_mut(player).skip_next_untap = false;
551    }
552
553    pub fn player_clear_skip_draw(&mut self, player: PlayerId) {
554        self.player_mut(player).skip_next_draw = false;
555    }
556
557    pub fn player_clear_skip_combat(&mut self, player: PlayerId) {
558        self.player_mut(player).skip_next_combat = false;
559    }
560
561    pub fn player_set_skip_untap(&mut self, player: PlayerId) {
562        self.player_mut(player).skip_next_untap = true;
563    }
564
565    pub fn player_set_skip_draw(&mut self, player: PlayerId) {
566        self.player_mut(player).skip_next_draw = true;
567    }
568
569    pub fn player_set_skip_combat(&mut self, player: PlayerId) {
570        self.player_mut(player).skip_next_combat = true;
571    }
572
573    pub fn player_attack_combat_reset(&mut self, player: PlayerId) {
574        self.player_mut(player).attacked_players_this_combat.clear();
575    }
576
577    pub fn player_record_attacked_player(&mut self, player: PlayerId, defender: PlayerId) {
578        let p = self.player_mut(player);
579        p.attacked_players_this_turn.push(defender);
580        p.attacked_players_this_combat.push(defender);
581    }
582
583    pub fn player_apply_mana_burn(&mut self, player: PlayerId, amount: i32) {
584        if amount > 0 {
585            self.player_lose_life(player, amount);
586        }
587    }
588
589    pub fn player_set_speed_effect(&mut self, player: PlayerId, effect_id: Option<CardId>) {
590        self.player_mut(player).speed_effect_card = effect_id;
591    }
592
593    pub fn player_set_speed(
594        &mut self,
595        player: PlayerId,
596        speed: i32,
597        trigger_handler: Option<&mut TriggerHandler>,
598    ) {
599        self.player_mut(player).speed = speed.clamp(0, 4);
600        let effect_id = self.ensure_speed_effect(player);
601        if let (Some(handler), Some(effect_id)) = (trigger_handler, effect_id) {
602            handler.register_active_trigger(self, effect_id);
603        }
604    }
605
606    pub fn increase_player_speed(
607        &mut self,
608        player: PlayerId,
609        trigger_handler: Option<&mut TriggerHandler>,
610    ) {
611        let current = self.player(player).speed;
612        if current < 4 {
613            self.player_set_speed(player, current + 1, trigger_handler);
614        }
615    }
616
617    pub fn decrease_player_speed(
618        &mut self,
619        player: PlayerId,
620        trigger_handler: Option<&mut TriggerHandler>,
621    ) {
622        let current = self.player(player).speed;
623        if current > 1 {
624            self.player_set_speed(player, current - 1, trigger_handler);
625        }
626    }
627
628    pub fn player_add_commander_damage(
629        &mut self,
630        player: PlayerId,
631        commander: CardId,
632        amount: i32,
633    ) {
634        if amount <= 0 || !self.player(player).commander_damage_enabled {
635            return;
636        }
637        self.player_mut(player)
638            .commander_damage_received
639            .entry(commander.0)
640            .and_modify(|v| *v += amount)
641            .or_insert(amount);
642    }
643
644    pub fn player_record_damage_assignment(
645        &mut self,
646        source: Option<CardId>,
647        target_player: Option<PlayerId>,
648        amount: i32,
649        is_combat: bool,
650    ) {
651        if amount <= 0 {
652            return;
653        }
654        let Some(source_id) = source else {
655            return;
656        };
657        let controller = self.card(source_id).controller;
658        {
659            let controller_state = self.player_mut(controller);
660            controller_state.assigned_damage_this_turn += amount;
661            if is_combat {
662                controller_state.assigned_combat_damage_this_turn += amount;
663            }
664        }
665        if let Some(target) = target_player {
666            if target != controller {
667                self.player_mut(controller)
668                    .opponents_assigned_damage_this_turn += amount;
669            }
670            if is_combat {
671                self.player_mut(target)
672                    .been_dealt_combat_damage_since_last_turn = true;
673            }
674        }
675    }
676
677    pub fn player_draw_one(&mut self, player: PlayerId) -> Option<CardId> {
678        self.player_draw_one_internal(player, false, None)
679    }
680
681    /// Draw a card for the draw step (sets `is_first_in_draw_step: true`).
682    pub fn player_draw_one_for_turn(&mut self, player: PlayerId) -> Option<CardId> {
683        self.player_draw_one_internal(player, true, None)
684    }
685
686    /// Draw a card for the draw step with agent access for Optional replacement
687    /// effects (Dredge). Mirrors Java's draw path which calls
688    /// `confirmReplacementEffect` through the replacement handler.
689    pub fn player_draw_one_for_turn_with_agents(
690        &mut self,
691        player: PlayerId,
692        agents: &mut [Box<dyn crate::agent::PlayerAgent>],
693    ) -> Option<CardId> {
694        self.player_draw_one_internal(player, true, Some(agents))
695    }
696
697    /// Internal draw implementation shared by normal draws and draw-step draws.
698    /// After the replacement handler runs, extra_draws are consumed by drawing
699    /// additional cards (mirrors Java Draw replacement which increments NumCards).
700    pub(crate) fn player_draw_one_internal(
701        &mut self,
702        player: PlayerId,
703        is_first_in_draw_step: bool,
704        agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
705    ) -> Option<CardId> {
706        if !self.player_can_draw(player) {
707            return None;
708        }
709        let mut event = ReplacementEvent::Draw {
710            player,
711            extra_draws: 0,
712            is_first_in_draw_step,
713        };
714        let result = if let Some(agents) = agents {
715            apply_replacements_with_agents(self, agents, &mut event)
716        } else {
717            apply_replacements(self, &mut event)
718        };
719        if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
720            return None;
721        }
722        // Extract extra_draws set by replacement effects (e.g. Alhammarret's Archive)
723        let extra = if let ReplacementEvent::Draw { extra_draws, .. } = &event {
724            *extra_draws
725        } else {
726            0
727        };
728        let Some(card_id) = self.take_top_card_from_zone(ZoneType::Library, player) else {
729            self.player_mut(player).tried_to_draw_from_empty_library = true;
730            return None;
731        };
732        self.move_card(card_id, ZoneType::Hand, player);
733        self.player_mut(player).drawn_this_turn += 1;
734        // Draw extra cards from replacement effects (each goes through its own
735        // replacement pipeline — is_first_in_draw_step is false for extras).
736        for _ in 0..extra {
737            self.player_draw_one(player);
738        }
739        Some(card_id)
740    }
741
742    pub fn player_can_draw(&self, player: PlayerId) -> bool {
743        crate::staticability::static_ability_cant_draw::can_draw_amount(self, player, 1) > 0
744    }
745
746    pub fn player_can_draw_amount(&self, player: PlayerId, amount: i32) -> bool {
747        crate::staticability::static_ability_cant_draw::can_draw_amount(self, player, amount) > 0
748    }
749
750    pub fn player_draw_cards(&mut self, player: PlayerId, amount: usize) -> Vec<CardId> {
751        let mut drawn = Vec::with_capacity(amount);
752        for _ in 0..amount {
753            if let Some(card_id) = self.player_draw_one(player) {
754                drawn.push(card_id);
755            } else {
756                break;
757            }
758        }
759        drawn
760    }
761
762    pub fn player_shuffle_library(&mut self, player: PlayerId, rng: &mut impl rand::Rng) {
763        self.shuffle_zone_cards_with_rand(ZoneType::Library, player, rng);
764    }
765
766    pub fn player_record_spell_cast(&mut self, player: PlayerId, card_id: CardId) {
767        let p = self.player_mut(player);
768        p.spells_cast_this_turn += 1;
769        p.spells_cast_this_game += 1;
770        p.cards_cast_this_turn.push(card_id);
771        p.statistics.record_spell_cast(card_id);
772    }
773
774    pub fn player_hand_count(&self, player: PlayerId) -> usize {
775        self.cards_in_zone(ZoneType::Hand, player).len()
776    }
777
778    pub fn player_graveyard_count(&self, player: PlayerId) -> usize {
779        self.cards_in_zone(ZoneType::Graveyard, player).len()
780    }
781
782    pub fn player_graveyard_type_count(&self, player: PlayerId) -> usize {
783        let mut types = HashSet::new();
784        for &cid in self.cards_in_zone(ZoneType::Graveyard, player) {
785            let card = self.card(cid);
786            for core_type in &card.type_line.core_types {
787                types.insert(core_type.name());
788            }
789        }
790        types.len()
791    }
792
793    pub fn player_has_hellbent(&self, player: PlayerId) -> bool {
794        self.player_hand_count(player) == 0
795    }
796
797    pub fn player_has_threshold(&self, player: PlayerId) -> bool {
798        self.player_graveyard_count(player) >= 7
799    }
800
801    pub fn player_has_metalcraft(&self, player: PlayerId) -> bool {
802        self.cards_in_zone(ZoneType::Battlefield, player)
803            .iter()
804            .filter(|&&cid| self.card(cid).type_line.is_artifact())
805            .count()
806            >= 3
807    }
808
809    pub fn player_has_delirium(&self, player: PlayerId) -> bool {
810        self.player_graveyard_type_count(player) >= 4
811    }
812
813    pub fn player_has_ferocious(&self, player: PlayerId) -> bool {
814        self.cards_in_zone(ZoneType::Battlefield, player)
815            .iter()
816            .any(|&cid| {
817                let card = self.card(cid);
818                card.is_creature() && card.power() >= 4
819            })
820    }
821
822    pub fn player_has_desert(&self, player: PlayerId) -> bool {
823        self.cards_in_zone(ZoneType::Battlefield, player)
824            .iter()
825            .any(|&cid| self.card(cid).type_line.has_subtype("Desert"))
826    }
827
828    pub fn player_has_blessing(&self, player: PlayerId) -> bool {
829        self.player(player).has_city_blessing
830    }
831
832    pub fn player_has_revolt(&self, player: PlayerId) -> bool {
833        self.player(player).permanents_left_battlefield_this_turn > 0
834    }
835
836    pub fn player_has_landfall(&self, player: PlayerId) -> bool {
837        self.player(player).lands_entered_battlefield_this_turn > 0
838    }
839
840    pub fn player_has_descended(&self, player: PlayerId) -> bool {
841        self.player(player).permanents_put_into_graveyard_this_turn > 0
842    }
843
844    pub fn player_controls_urza_lands(&self, player: PlayerId) -> bool {
845        let battlefield = self.cards_in_zone(ZoneType::Battlefield, player);
846        let has_mine = battlefield
847            .iter()
848            .any(|&cid| self.card(cid).type_line.has_subtype("Mine"));
849        let has_plant = battlefield
850            .iter()
851            .any(|&cid| self.card(cid).type_line.has_subtype("Power-Plant"));
852        let has_tower = battlefield
853            .iter()
854            .any(|&cid| self.card(cid).type_line.has_subtype("Tower"));
855        has_mine && has_plant && has_tower
856    }
857
858    pub fn player_opponents_lost_life_this_turn(&self, player: PlayerId) -> bool {
859        self.player_order
860            .iter()
861            .copied()
862            .filter(|&other| other != player)
863            .any(|other| self.player(other).life_lost_this_turn > 0)
864    }
865
866    pub fn player_has_bloodthirst(&self, player: PlayerId) -> bool {
867        self.player(player).opponents_assigned_damage_this_turn > 0
868    }
869
870    pub fn player_can_surge(&self, player: PlayerId) -> bool {
871        self.player(player).spells_cast_this_turn > 0
872    }
873
874    pub fn player_spells_cast_this_turn(&self, player: PlayerId) -> i32 {
875        self.player(player).spells_cast_this_turn
876    }
877
878    pub fn player_storm_count(&self, _player: PlayerId) -> i32 {
879        (self.stack.spells_cast_this_turn() as i32 - 1).max(0)
880    }
881
882    pub fn player_reset_drawn_this_turn(&mut self, player: PlayerId) {
883        self.player_mut(player).drawn_this_turn = 0;
884    }
885
886    pub fn player_new_turn(&mut self, player: PlayerId) {
887        self.player_mut(player).new_turn();
888    }
889
890    pub fn player_check_lose_condition(&mut self, player: PlayerId) -> bool {
891        if !self.player(player).is_alive() {
892            return true;
893        }
894        if self.player(player).tried_to_draw_from_empty_library {
895            self.player_mut(player).tried_to_draw_from_empty_library = false;
896            let mut event = ReplacementEvent::GameLoss {
897                player,
898                reason: GameLossReason::Milled,
899            };
900            let result = apply_replacements(self, &mut event);
901            if result != ReplacementResult::Replaced {
902                self.player_mark_lost(player, GameLossReason::Milled);
903                return true;
904            }
905        }
906        if self.player(player).life <= 0 {
907            let mut event = ReplacementEvent::GameLoss {
908                player,
909                reason: GameLossReason::LifeReachedZero,
910            };
911            let result = apply_replacements(self, &mut event);
912            if result != ReplacementResult::Replaced {
913                self.player_mark_lost(player, GameLossReason::LifeReachedZero);
914                return true;
915            }
916        }
917        if self.player(player).poison_counters >= 10 {
918            let mut event = ReplacementEvent::GameLoss {
919                player,
920                reason: GameLossReason::Poisoned,
921            };
922            let result = apply_replacements(self, &mut event);
923            if result != ReplacementResult::Replaced {
924                self.player_mark_lost(player, GameLossReason::Poisoned);
925                return true;
926            }
927        }
928        if self.player(player).commander_damage_enabled
929            && self
930                .player(player)
931                .commander_damage_received
932                .values()
933                .any(|&amount| amount >= 21)
934        {
935            let mut event = ReplacementEvent::GameLoss {
936                player,
937                reason: GameLossReason::CommanderDamage,
938            };
939            let result = apply_replacements(self, &mut event);
940            if result != ReplacementResult::Replaced {
941                self.player_mark_lost(player, GameLossReason::CommanderDamage);
942                return true;
943            }
944        }
945        false
946    }
947}
948
949#[cfg(test)]
950mod tests {
951    use super::*;
952    use crate::player::registered_player::RegisteredPlayerVariant;
953
954    #[test]
955    fn draw_from_empty_library_sets_mill_loss_flag() {
956        let mut game = GameState::new(&["Alice", "Bob"], 20);
957        let player = PlayerId(0);
958
959        assert_eq!(game.player_draw_one(player), None);
960        assert!(game.player(player).tried_to_draw_from_empty_library);
961    }
962
963    #[test]
964    fn empty_library_draw_loses_game_as_milled() {
965        let mut game = GameState::new(&["Alice", "Bob"], 20);
966        let player = PlayerId(0);
967
968        assert_eq!(game.player_draw_one(player), None);
969        assert!(game.player_check_lose_condition(player));
970        assert!(game.player(player).has_lost);
971        assert_eq!(
972            game.player(player).outcome,
973            Some(PlayerOutcome::Loss {
974                reason: GameLossReason::Milled,
975                spell_name: None,
976            })
977        );
978        assert!(!game.player(player).tried_to_draw_from_empty_library);
979    }
980
981    #[test]
982    fn brawl_registration_disables_commander_damage_loss() {
983        let players = vec![
984            RegisteredPlayer::for_variant_set(
985                "Alice",
986                &[RegisteredPlayerVariant::Brawl],
987                vec!["Commander".to_string()],
988                vec![],
989                false,
990                vec![],
991                vec![],
992            ),
993            RegisteredPlayer::new("Bob"),
994        ];
995        let mut game = GameState::new_from_registered_players(&players);
996        let commander = CardId(99);
997
998        game.player_add_commander_damage(PlayerId(0), commander, 21);
999
1000        assert!(!game.player_check_lose_condition(PlayerId(0)));
1001        assert!(!game.player(PlayerId(0)).has_lost);
1002    }
1003}