Skip to main content

manabrew_engine/player/
mod.rs

1pub mod actions;
2pub mod commander;
3pub mod delayed_reveal;
4pub mod game_loss_reason;
5pub mod player_action_confirm_mode;
6pub mod player_collection;
7pub mod player_controller;
8pub mod player_factory_util;
9pub mod player_outcome;
10pub mod player_predicates;
11pub mod player_property;
12pub mod player_statistics;
13pub mod registered_player;
14pub mod service;
15pub mod state;
16
17use crate::game::GameState;
18use crate::ids::{CardId, PlayerId};
19use crate::player::player_factory_util::{
20    add_static_ability, add_trigger_ability, new_player_effect_card,
21};
22use crate::spellability::SpellAbility;
23use crate::trigger::handler::TriggerHandler;
24use forge_foundation::ZoneType;
25
26pub use delayed_reveal::DelayedReveal;
27pub use game_loss_reason::GameLossReason;
28pub use player_collection::PlayerCollection;
29pub use player_controller::PlayerController;
30pub use player_outcome::PlayerOutcome;
31pub use player_property::player_has_property;
32pub use player_statistics::PlayerStatistics;
33pub use registered_player::RegisteredPlayer;
34pub use state::PlayerState;
35
36pub fn gain_life(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
37    game.player_gain_life(player, amount)
38}
39
40pub fn can_gain_life(game: &GameState, player: PlayerId) -> bool {
41    game.player_can_gain_life(player)
42}
43
44pub fn lose_life(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
45    game.player_lose_life(player, amount)
46}
47
48pub fn can_lose_life(game: &GameState, player: PlayerId) -> bool {
49    game.player_can_lose_life(player)
50}
51
52pub fn can_pay_life(game: &GameState, player: PlayerId, amount: i32) -> bool {
53    game.player_can_pay_life(player, amount)
54}
55
56pub fn pay_life(game: &mut GameState, player: PlayerId, amount: i32) -> bool {
57    game.player_pay_life(player, amount)
58}
59
60pub fn can_pay_energy(game: &GameState, player: PlayerId, amount: i32) -> bool {
61    game.player_can_pay_energy(player, amount)
62}
63
64pub fn lose_energy(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
65    game.player_lose_energy(player, amount)
66}
67
68pub fn pay_energy(game: &mut GameState, player: PlayerId, amount: i32) -> bool {
69    game.player_pay_energy(player, amount)
70}
71
72pub fn can_pay_shards(game: &GameState, player: PlayerId, amount: i32) -> bool {
73    game.player_can_pay_shards(player, amount)
74}
75
76pub fn lose_shards(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
77    game.player_lose_shards(player, amount)
78}
79
80pub fn pay_shards(game: &mut GameState, player: PlayerId, amount: i32) -> bool {
81    game.player_pay_shards(player, amount)
82}
83
84pub fn add_rad_counters(game: &mut GameState, player: PlayerId, amount: i32) {
85    game.player_add_radiation(player, amount);
86}
87
88pub fn remove_rad_counters(game: &mut GameState, player: PlayerId, amount: i32) {
89    game.player_remove_radiation(player, amount);
90}
91
92pub fn add_poison_counters(game: &mut GameState, player: PlayerId, amount: i32) {
93    game.player_add_poison(player, amount);
94}
95
96pub fn remove_poison_counters(game: &mut GameState, player: PlayerId, amount: i32) {
97    game.player_remove_poison(player, amount);
98}
99
100pub fn can_draw(game: &GameState, player: PlayerId) -> bool {
101    game.player_can_draw(player)
102}
103
104pub fn can_draw_amount(game: &GameState, player: PlayerId, amount: i32) -> bool {
105    game.player_can_draw_amount(player, amount)
106}
107
108pub fn draw_card(game: &mut GameState, player: PlayerId) -> Option<CardId> {
109    game.player_draw_one(player)
110}
111
112pub fn draw_cards(game: &mut GameState, player: PlayerId, amount: usize) -> Vec<CardId> {
113    game.player_draw_cards(player, amount)
114}
115
116pub fn reset_num_drawn_this_draw_step(game: &mut GameState, player: PlayerId) {
117    game.player_mut(player).drawn_this_draw_step = 0;
118}
119
120pub fn reset_num_drawn_this_turn(game: &mut GameState, player: PlayerId) {
121    game.player_reset_drawn_this_turn(player);
122}
123
124pub fn num_drawn_this_draw_step(game: &GameState, player: PlayerId) -> i32 {
125    game.player(player).drawn_this_draw_step
126}
127
128pub fn shuffle(game: &mut GameState, player: PlayerId, rng: &mut impl rand::Rng) {
129    game.player_shuffle_library(player, rng);
130}
131
132pub fn play_land(game: &mut GameState, player: PlayerId) {
133    game.player_record_land_play(player);
134}
135
136pub fn play_land_no_check(game: &mut GameState, player: PlayerId) {
137    game.player_record_land_play(player);
138}
139
140pub fn can_play_land(game: &GameState, player: PlayerId) -> bool {
141    game.player(player).can_play_land()
142}
143
144pub fn add_max_land_plays(game: &mut GameState, player: PlayerId, amount: i32) {
145    game.player_mut(player).max_land_plays_per_turn += amount;
146}
147
148pub fn remove_max_land_plays(game: &mut GameState, player: PlayerId, amount: i32) {
149    game.player_mut(player).max_land_plays_per_turn =
150        (game.player(player).max_land_plays_per_turn - amount).max(0);
151}
152
153pub fn add_max_land_plays_infinite(game: &mut GameState, player: PlayerId) {
154    game.player_mut(player).unlimited_land_plays = true;
155}
156
157pub fn remove_max_land_plays_infinite(game: &mut GameState, player: PlayerId) {
158    game.player_mut(player).unlimited_land_plays = false;
159}
160
161pub fn add_land_played_this_turn(game: &mut GameState, player: PlayerId) {
162    game.player_record_land_play(player);
163}
164
165pub fn reset_lands_played_this_turn(game: &mut GameState, player: PlayerId) {
166    game.player_mut(player).lands_played_this_turn = 0;
167}
168
169pub fn add_spell_cast_this_turn(game: &mut GameState, player: PlayerId, card_id: CardId) {
170    game.player_record_spell_cast(player, card_id);
171}
172
173pub fn reset_spells_cast_this_turn(game: &mut GameState, player: PlayerId) {
174    game.player_mut(player).spells_cast_this_turn = 0;
175    game.player_mut(player).cards_cast_this_turn.clear();
176}
177
178pub fn reset_spell_cast_this_game(game: &mut GameState, player: PlayerId) {
179    game.player_mut(player).spells_cast_this_game = 0;
180}
181
182pub fn add_life_gained_by_team_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
183    game.player_add_team_life_gained(player, amount);
184}
185
186pub fn add_explored_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
187    game.player_record_explore(player, amount);
188}
189
190pub fn reset_num_explored_this_turn(game: &mut GameState, player: PlayerId) {
191    game.player_mut(player).explored_this_turn = 0;
192}
193
194pub fn reset_discarded_this_turn(game: &mut GameState, player: PlayerId) {
195    game.player_mut(player).discarded_this_turn = 0;
196}
197
198pub fn surveil(game: &mut GameState, player: PlayerId, amount: i32) {
199    game.player_mut(player).surveilled_this_turn += amount.max(0);
200}
201
202pub fn reset_surveil_this_turn(game: &mut GameState, player: PlayerId) {
203    game.player_mut(player).surveilled_this_turn = 0;
204}
205
206pub fn reset_num_rolls_this_turn(game: &mut GameState, player: PlayerId) {
207    game.player_mut(player).num_rolls_this_turn = 0;
208    game.player_mut(player).dice_rolls_this_turn.clear();
209}
210
211pub fn roll(game: &mut GameState, player: PlayerId, result: i32) {
212    game.player_record_roll(player, Some(result));
213}
214
215pub fn reset_num_flips_this_turn(game: &mut GameState, player: PlayerId) {
216    game.player_mut(player).num_flips_this_turn = 0;
217}
218
219pub fn flip(game: &mut GameState, player: PlayerId) {
220    game.player_mut(player).num_flips_this_turn += 1;
221}
222
223pub fn discard(game: &mut GameState, player: PlayerId, amount: i32) {
224    game.player_record_discard(player, amount);
225}
226
227pub fn add_tokens_created_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
228    game.player_mut(player).tokens_created_this_turn += amount.max(0);
229}
230
231pub fn reset_num_token_created_this_turn(game: &mut GameState, player: PlayerId) {
232    game.player_mut(player).tokens_created_this_turn = 0;
233}
234
235pub fn add_foretold_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
236    game.player_mut(player).foretold_this_turn += amount.max(0);
237}
238
239pub fn reset_num_foretold_this_turn(game: &mut GameState, player: PlayerId) {
240    game.player_mut(player).foretold_this_turn = 0;
241}
242
243pub fn add_note_for_name(game: &mut GameState, player: PlayerId, name: &str, note: String) {
244    game.player_mut(player)
245        .notes
246        .entry(name.to_string())
247        .or_default()
248        .push(note);
249}
250
251pub fn clear_notes_for_name(game: &mut GameState, player: PlayerId, name: &str) {
252    game.player_mut(player).notes.remove(name);
253}
254
255pub fn note_number_for_name(game: &mut GameState, player: PlayerId, name: &str, value: i32) {
256    game.player_mut(player)
257        .noted_num
258        .insert(name.to_string(), value);
259}
260
261pub fn mill(game: &mut GameState, player: PlayerId, amount: usize) -> Vec<CardId> {
262    let mut milled = Vec::with_capacity(amount);
263    for _ in 0..amount {
264        let Some(card_id) = game.take_top_card_from_zone(ZoneType::Library, player) else {
265            break;
266        };
267        let owner = game.card(card_id).owner;
268        game.move_card(card_id, ZoneType::Graveyard, owner);
269        milled.push(card_id);
270    }
271    milled
272}
273
274pub fn increase_speed(
275    game: &mut GameState,
276    player: PlayerId,
277    trigger_handler: Option<&mut TriggerHandler>,
278) {
279    game.increase_player_speed(player, trigger_handler);
280}
281
282pub fn decrease_speed(
283    game: &mut GameState,
284    player: PlayerId,
285    trigger_handler: Option<&mut TriggerHandler>,
286) {
287    game.decrease_player_speed(player, trigger_handler);
288}
289
290pub fn no_speed(game: &GameState, player: PlayerId) -> bool {
291    game.player(player).speed == 0
292}
293
294pub fn max_speed(game: &GameState, player: PlayerId) -> bool {
295    game.player(player).speed == 4
296}
297
298pub fn alt_win_by_spell_effect(
299    game: &mut GameState,
300    player: PlayerId,
301    source_name: Option<String>,
302) {
303    game.player_alt_win_by_spell_effect(player, source_name);
304}
305
306pub fn concede(game: &mut GameState, player: PlayerId) {
307    game.player_concede(player);
308}
309
310pub fn check_lose_condition(game: &mut GameState, player: PlayerId) -> bool {
311    game.player_check_lose_condition(player)
312}
313
314pub fn has_lost(game: &GameState, player: PlayerId) -> bool {
315    game.player(player).has_lost
316}
317
318pub fn has_won(game: &GameState, player: PlayerId) -> bool {
319    game.player(player).has_won
320}
321
322pub fn has_metalcraft(game: &GameState, player: PlayerId) -> bool {
323    game.player_has_metalcraft(player)
324}
325
326pub fn has_desert(game: &GameState, player: PlayerId) -> bool {
327    game.player_has_desert(player)
328}
329
330pub fn has_threshold(game: &GameState, player: PlayerId) -> bool {
331    game.player_has_threshold(player)
332}
333
334pub fn has_hellbent(game: &GameState, player: PlayerId) -> bool {
335    game.player_has_hellbent(player)
336}
337
338pub fn has_revolt(game: &GameState, player: PlayerId) -> bool {
339    game.player_has_revolt(player)
340}
341
342pub fn descend(game: &mut GameState, player: PlayerId) {
343    game.player_record_permanent_put_into_graveyard(player);
344}
345
346pub fn has_delirium(game: &GameState, player: PlayerId) -> bool {
347    game.player_has_delirium(player)
348}
349
350pub fn has_landfall(game: &GameState, player: PlayerId) -> bool {
351    game.player_has_landfall(player)
352}
353
354pub fn has_ferocious(game: &GameState, player: PlayerId) -> bool {
355    game.player_has_ferocious(player)
356}
357
358pub fn has_surge(game: &GameState, player: PlayerId) -> bool {
359    game.player_can_surge(player)
360}
361
362pub fn has_bloodthirst(game: &GameState, player: PlayerId) -> bool {
363    game.player_has_bloodthirst(player)
364}
365
366pub fn has_property(
367    game: &GameState,
368    player: PlayerId,
369    property: &str,
370    source_id: CardId,
371    controller: PlayerId,
372    sa: &SpellAbility,
373) -> bool {
374    player_property::player_has_property(player, property, game, source_id, controller, sa)
375}
376
377pub fn has_keyword(game: &GameState, player: PlayerId, keyword: &str) -> bool {
378    player_predicates::has_keyword(game, player, keyword)
379}
380
381pub fn can_be_targeted_by(game: &GameState, player: PlayerId, _source: CardId) -> bool {
382    game.player(player).is_alive() && !has_keyword(game, player, "Shroud")
383}
384
385pub fn can_mulligan(game: &GameState, player: PlayerId) -> bool {
386    !game.cards_in_zone(ZoneType::Library, player).is_empty()
387}
388
389pub fn on_cleanup_phase(game: &mut GameState, player: PlayerId) {
390    game.player_cleanup_turn_state(player);
391}
392
393pub fn update_zone_for_view(game: &GameState, player: PlayerId, zone: ZoneType) -> usize {
394    game.cards_in_zone(zone, player).len()
395}
396
397pub fn update_all_zones_for_view(game: &GameState, player: PlayerId) -> Vec<(ZoneType, usize)> {
398    [
399        ZoneType::Battlefield,
400        ZoneType::Hand,
401        ZoneType::Graveyard,
402        ZoneType::Library,
403        ZoneType::Exile,
404        ZoneType::Command,
405    ]
406    .into_iter()
407    .map(|zone| (zone, game.cards_in_zone(zone, player).len()))
408    .collect()
409}
410
411pub fn reset_extra_zones(game: &mut GameState, player: PlayerId) {
412    let cards = game.cards_in_zone(ZoneType::ExtraHand, player).to_vec();
413    for card_id in cards {
414        game.remove_card_from_zone(ZoneType::ExtraHand, player, card_id);
415        game.card_mut(card_id).zone = ZoneType::None;
416    }
417}
418
419pub fn can_cast_sorcery(game: &GameState, player: PlayerId) -> bool {
420    game.turn.active_player == player && game.turn.phase.is_main() && game.stack.is_empty()
421}
422
423pub fn create_speed_effect(
424    game: &mut GameState,
425    player: PlayerId,
426    trigger_handler: Option<&mut TriggerHandler>,
427) {
428    let speed = game.player(player).speed.max(1);
429    game.player_set_speed(player, speed, trigger_handler);
430}
431
432pub fn lose_condition_met(game: &mut GameState, player: PlayerId) -> bool {
433    check_lose_condition(game, player)
434}
435
436pub fn intentional_draw(game: &mut GameState, player: PlayerId) {
437    game.player_mut(player).outcome = Some(PlayerOutcome::Draw);
438}
439
440pub fn conceded(game: &GameState, player: PlayerId) -> bool {
441    game.player(player).has_conceded
442}
443
444pub fn cant_lose(game: &GameState, player: PlayerId) -> bool {
445    !game.player_can_lose_life(player)
446}
447
448pub fn cant_lose_for_zero_or_less_life(game: &GameState, player: PlayerId) -> bool {
449    crate::staticability::static_ability_cant_gain_lose_pay_life::cant_lose_life(game, player)
450}
451
452pub fn cant_lose_check(game: &GameState, player: PlayerId) -> bool {
453    cant_lose(game, player) || cant_lose_for_zero_or_less_life(game, player)
454}
455
456pub fn cant_win(game: &GameState, player: PlayerId) -> bool {
457    has_keyword(game, player, "CantWin")
458}
459
460pub fn add_commander(game: &mut GameState, player: PlayerId, commander: CardId) {
461    game.player_register_commander(player, commander);
462}
463
464pub fn remove_commander(game: &mut GameState, player: PlayerId, commander: CardId) {
465    game.player_remove_commander(player, commander);
466}
467
468pub fn add_commander_damage(
469    game: &mut GameState,
470    player: PlayerId,
471    commander: CardId,
472    amount: i32,
473) {
474    game.player_add_commander_damage(player, commander, amount);
475}
476
477pub fn inc_commander_cast(game: &mut GameState, player: PlayerId, commander: CardId) {
478    game.player_increment_commander_cast(player, commander);
479}
480
481pub fn reset_commander_stats(game: &mut GameState, player: PlayerId) {
482    game.player_reset_commander_state(player);
483}
484
485pub fn create_commander_effect(
486    game: &mut GameState,
487    player: PlayerId,
488    trigger_handler: Option<&mut TriggerHandler>,
489) -> Option<CardId> {
490    game.player_create_commander_effect(player, trigger_handler)
491}
492
493pub fn create_monarch_effect(
494    game: &mut GameState,
495    player: PlayerId,
496    trigger_handler: Option<&mut TriggerHandler>,
497) {
498    game.player_set_monarch(player, trigger_handler);
499}
500
501pub fn remove_monarch_effect(game: &mut GameState, player: PlayerId) {
502    if let Some(effect_id) = game.player(player).monarch_effect_card {
503        if game.card(effect_id).zone == ZoneType::Command {
504            game.remove_card_from_zone(ZoneType::Command, player, effect_id);
505        }
506        game.card_mut(effect_id).zone = ZoneType::None;
507    }
508    if game.monarch == Some(player) {
509        game.monarch = None;
510    }
511    game.player_mut(player).monarch_effect_card = None;
512}
513
514pub fn can_become_monarch(game: &GameState, player: PlayerId) -> bool {
515    game.player(player).is_alive() && game.monarch != Some(player)
516}
517
518pub fn create_initiative_effect(
519    game: &mut GameState,
520    player: PlayerId,
521    trigger_handler: Option<&mut TriggerHandler>,
522) {
523    game.player_take_initiative(player, trigger_handler);
524}
525
526pub fn has_initiative(game: &GameState, player: PlayerId) -> bool {
527    game.initiative_holder == Some(player)
528}
529
530pub fn create_radiation_effect(
531    game: &mut GameState,
532    player: PlayerId,
533    trigger_handler: &mut TriggerHandler,
534) {
535    game.player_register_radiation_effect(player, trigger_handler);
536}
537
538pub fn remove_initiative_effect(game: &mut GameState, player: PlayerId) {
539    if let Some(effect_id) = game.player(player).initiative_effect_card {
540        if game.card(effect_id).zone == ZoneType::Command {
541            game.remove_card_from_zone(ZoneType::Command, player, effect_id);
542        }
543        game.card_mut(effect_id).zone = ZoneType::None;
544    }
545    if game.initiative_holder == Some(player) {
546        game.initiative_holder = None;
547    }
548    game.player_mut(player).initiative_effect_card = None;
549}
550
551pub fn remove_radiation_effect(game: &mut GameState, player: PlayerId) {
552    if let Some(effect_id) = game.player(player).radiation_effect_card {
553        if game.card(effect_id).zone == ZoneType::Command {
554            game.remove_card_from_zone(ZoneType::Command, player, effect_id);
555        }
556        game.card_mut(effect_id).zone = ZoneType::None;
557    }
558    game.player_mut(player).radiation_effect_card = None;
559}
560
561pub fn has_radiation_effect(game: &GameState, player: PlayerId) -> bool {
562    game.player(player).radiation_effect_card.is_some()
563}
564
565pub fn has_blessing(game: &GameState, player: PlayerId) -> bool {
566    game.player_has_blessing(player)
567}
568
569pub fn same_team(game: &GameState, player: PlayerId, other: PlayerId) -> bool {
570    player_predicates::same_team(game, player, other)
571}
572
573pub fn can_discard_by(game: &GameState, player: PlayerId, other: PlayerId) -> bool {
574    player_predicates::can_discard_by(game, player, other)
575}
576
577pub fn add_cycled(game: &mut GameState, player: PlayerId, card_id: CardId) {
578    game.player_record_spell_cast(player, card_id);
579}
580
581pub fn has_urza_lands(game: &GameState, player: PlayerId) -> bool {
582    game.player_controls_urza_lands(player)
583}
584
585pub fn commit_crime(game: &mut GameState, player: PlayerId) {
586    game.player_mut(player).committed_crime_this_turn += 1;
587}
588
589pub fn add_die_roll_this_turn(game: &mut GameState, player: PlayerId, roll: i32) {
590    game.player_record_roll(player, Some(roll));
591}
592
593pub fn visit_attractions(game: &mut GameState, player: PlayerId, amount: i32) {
594    game.player_record_attraction_visit(player, amount);
595}
596
597pub fn add_creatures_attacked_this_turn(
598    game: &mut GameState,
599    player: PlayerId,
600    defender: PlayerId,
601) {
602    game.player_record_attacked_player(player, defender);
603}
604
605pub fn clear_attacked_players_my_combat(game: &mut GameState, player: PlayerId) {
606    game.player_attack_combat_reset(player);
607}
608
609pub fn increment_ring_tempted_you(game: &mut GameState, player: PlayerId) {
610    game.player_ring_tempt(player);
611}
612
613pub fn reset_ring_tempted_you(game: &mut GameState, player: PlayerId) {
614    game.player_mut(player).ring_level = 0;
615    game.player_set_ring_bearer(player, None);
616}
617
618pub fn on_game_over(game: &mut GameState, player: PlayerId) {
619    let face_down = reveal_face_down_cards(game, player);
620    for card_id in face_down {
621        game.card_mut(card_id).face_down = false;
622    }
623}
624
625pub fn clear_the_ring(game: &mut GameState, player: PlayerId) {
626    game.player_mut(player).ring_level = 0;
627}
628
629pub fn clear_ring_bearer(game: &mut GameState, player: PlayerId) {
630    game.player_set_ring_bearer(player, None);
631}
632
633pub fn increment_turn(game: &mut GameState, player: PlayerId) {
634    game.player_new_turn(player);
635}
636
637pub fn has_tapped_land_for_mana_this_turn(game: &GameState, player: PlayerId) -> bool {
638    game.player(player).tapped_land_for_mana_this_turn
639}
640
641pub fn has_been_dealt_combat_damage_since_last_turn(game: &GameState, player: PlayerId) -> bool {
642    game.player(player).been_dealt_combat_damage_since_last_turn
643}
644
645pub fn clear_attacked_my_turn(game: &mut GameState, player: PlayerId) {
646    game.player_mut(player).attacked_players_this_turn.clear();
647}
648
649pub fn increment_ventured_this_turn(game: &mut GameState, player: PlayerId) {
650    game.player_mut(player).ventured_this_turn += 1;
651}
652
653pub fn reset_ventured_this_turn(game: &mut GameState, player: PlayerId) {
654    game.player_mut(player).ventured_this_turn = 0;
655}
656
657pub fn add_completed_dungeon(game: &mut GameState, player: PlayerId, dungeon: CardId) {
658    game.player_mut(player).completed_dungeons.push(dungeon);
659}
660
661pub fn reset_completed_dungeons(game: &mut GameState, player: PlayerId) {
662    game.player_mut(player).completed_dungeons.clear();
663}
664
665pub fn has_prowl(game: &GameState, player: PlayerId) -> bool {
666    game.player_has_bloodthirst(player)
667}
668
669pub fn has_freerunning(game: &GameState, player: PlayerId) -> bool {
670    game.player_has_bloodthirst(player)
671}
672
673pub fn inc_library_searched(game: &mut GameState, player: PlayerId) {
674    game.player_mut(player).library_searched_this_turn += 1;
675}
676
677pub fn add_investigated_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
678    game.player_mut(player).investigated_this_turn += amount.max(0);
679}
680
681pub fn reset_investigated_this_turn(game: &mut GameState, player: PlayerId) {
682    game.player_mut(player).investigated_this_turn = 0;
683}
684
685pub fn add_sacrificed_this_turn(game: &mut GameState, player: PlayerId, amount: i32) {
686    game.player_mut(player).sacrificed_this_turn += amount.max(0);
687}
688
689pub fn reset_sacrificed_this_turn(game: &mut GameState, player: PlayerId) {
690    game.player_mut(player).sacrificed_this_turn = 0;
691}
692
693pub fn reset_spell_cast_since_beg_of_your_last_turn(game: &mut GameState, player: PlayerId) {
694    game.player_mut(player).cards_cast_this_turn.clear();
695}
696
697pub fn add_spell_cast_since_beg_of_your_last_turn(
698    game: &mut GameState,
699    player: PlayerId,
700    card_id: CardId,
701) {
702    game.player_mut(player).cards_cast_this_turn.push(card_id);
703}
704
705pub fn add_damage_after_prevention(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
706    let prevented = amount.min(game.player(player).damage_prevention);
707    let final_amount = amount - prevented;
708    game.player_mut(player).damage_prevention -= prevented;
709    if final_amount > 0 {
710        game.player_deal_damage(player, final_amount);
711    }
712    final_amount
713}
714
715pub fn static_replace_damage(_game: &GameState, _player: PlayerId, amount: i32) -> i32 {
716    amount.max(0)
717}
718
719pub fn process_damage(game: &mut GameState, player: PlayerId, amount: i32) -> i32 {
720    add_damage_after_prevention(game, player, amount)
721}
722
723pub fn can_receive_counters(game: &GameState, player: PlayerId, amount: i32) -> bool {
724    amount > 0 && game.player(player).is_alive()
725}
726
727pub fn can_remove_counters(game: &GameState, player: PlayerId, counter: &str, amount: i32) -> bool {
728    if !game.player(player).is_alive() {
729        return false;
730    }
731    let current = match counter {
732        "Poison" => game.player(player).poison_counters,
733        "Energy" => game.player(player).energy_counters,
734        "Rad" | "Radiation" => game.player(player).radiation_counters,
735        _ => 0,
736    };
737    current >= amount.max(0)
738}
739
740pub fn add_counter_internal(game: &mut GameState, player: PlayerId, counter: &str, amount: i32) {
741    match counter {
742        "Poison" => add_poison_counters(game, player, amount),
743        "Energy" => game.player_add_energy(player, amount),
744        "Rad" | "Radiation" => add_rad_counters(game, player, amount),
745        _ => {}
746    }
747}
748
749pub fn subtract_counter(game: &mut GameState, player: PlayerId, counter: &str, amount: i32) {
750    match counter {
751        "Poison" => remove_poison_counters(game, player, amount),
752        "Energy" => {
753            game.player_lose_energy(player, amount);
754        }
755        "Rad" | "Radiation" => remove_rad_counters(game, player, amount),
756        _ => {}
757    }
758}
759
760pub fn clear_counters(game: &mut GameState, player: PlayerId) {
761    game.player_mut(player).poison_counters = 0;
762    game.player_mut(player).energy_counters = 0;
763    game.player_set_radiation(player, 0);
764}
765
766pub fn add_pump_keyword_with_duration(
767    game: &mut GameState,
768    player: PlayerId,
769    keyword: String,
770    duration: Option<&crate::spellability::AbilityDuration>,
771) {
772    match duration {
773        Some(crate::spellability::AbilityDuration::UntilYourNextTurn) => {
774            game.player_mut(player)
775                .keywords_until_my_next_turn
776                .push(keyword.clone());
777        }
778        Some(crate::spellability::AbilityDuration::Permanent)
779        | Some(crate::spellability::AbilityDuration::Perpetual) => {}
780        _ => {
781            game.player_mut(player)
782                .keywords_until_end_of_turn
783                .push(keyword.clone());
784        }
785    }
786    add_changed_keywords(game, player, keyword);
787    update_keyword_card_ability_text(game, player);
788}
789
790pub fn expire_until_your_next_turn_keywords(game: &mut GameState, player: PlayerId) {
791    let expired = std::mem::take(&mut game.player_mut(player).keywords_until_my_next_turn);
792    for keyword in expired {
793        remove_changed_keywords(game, player, &keyword);
794    }
795    update_keyword_card_ability_text(game, player);
796}
797
798pub fn expire_end_of_turn_keywords(game: &mut GameState, player: PlayerId) {
799    let expired = std::mem::take(&mut game.player_mut(player).keywords_until_end_of_turn);
800    for keyword in expired {
801        remove_changed_keywords(game, player, &keyword);
802    }
803    update_keyword_card_ability_text(game, player);
804}
805
806pub fn add_changed_keywords(game: &mut GameState, player: PlayerId, keyword: String) {
807    if !game.player(player).changed_keywords.contains(&keyword) {
808        game.player_mut(player).changed_keywords.push(keyword);
809    }
810}
811
812pub fn remove_changed_keywords(game: &mut GameState, player: PlayerId, keyword: &str) {
813    game.player_mut(player)
814        .changed_keywords
815        .retain(|candidate| candidate != keyword);
816}
817
818pub fn add_maingame_card_mapping(
819    game: &mut GameState,
820    player: PlayerId,
821    original: CardId,
822    mapped: CardId,
823) {
824    game.player_mut(player)
825        .maingame_card_mapping
826        .insert(original, mapped);
827}
828
829pub fn update_mana_for_view(game: &GameState, player: PlayerId) -> i32 {
830    game.player(player).mana_shards
831}
832
833pub fn add_controller(game: &mut GameState, player: PlayerId, controller: PlayerId) {
834    game.player_set_controlled_by(player, Some(controller));
835}
836
837pub fn remove_controller(game: &mut GameState, player: PlayerId, controller: PlayerId) {
838    if game.player(player).controlled_by == Some(controller) {
839        game.player_set_controlled_by(player, None);
840    }
841}
842
843pub fn clear_controller(game: &mut GameState, player: PlayerId) {
844    game.player_set_controlled_by(player, None);
845}
846
847pub fn add_controlled_while_searching(
848    game: &mut GameState,
849    player: PlayerId,
850    controller: PlayerId,
851) {
852    game.player_mut(player)
853        .controlled_while_searching
854        .insert(controller);
855}
856
857pub fn remove_controlled_while_searching(
858    game: &mut GameState,
859    player: PlayerId,
860    controller: PlayerId,
861) {
862    game.player_mut(player)
863        .controlled_while_searching
864        .remove(&controller);
865}
866
867pub fn dangerously_set_controller(
868    game: &mut GameState,
869    player: PlayerId,
870    controller: Option<PlayerId>,
871) {
872    game.player_set_controlled_by(player, controller);
873}
874
875pub fn update_avatar(game: &mut GameState, player: PlayerId, avatar_index: i32) {
876    game.player_mut(player).avatar_index = avatar_index;
877}
878
879pub fn update_sleeve(game: &mut GameState, player: PlayerId, sleeve_index: i32) {
880    game.player_mut(player).sleeve_index = sleeve_index;
881}
882
883pub fn run_with_controller<R>(
884    game: &mut GameState,
885    player: PlayerId,
886    controller: Option<PlayerId>,
887    f: impl FnOnce(&mut GameState) -> R,
888) -> R {
889    let previous = game.player(player).controlled_by;
890    game.player_set_controlled_by(player, controller);
891    let result = f(game);
892    game.player_set_controlled_by(player, previous);
893    result
894}
895
896pub fn add_inbound_token(game: &mut GameState, player: PlayerId, card: CardId) {
897    if !game.player(player).inbound_tokens.contains(&card) {
898        game.player_mut(player).inbound_tokens.push(card);
899    }
900}
901
902pub fn remove_inbound_token(game: &mut GameState, player: PlayerId, card: CardId) {
903    game.player_mut(player)
904        .inbound_tokens
905        .retain(|&c| c != card);
906}
907
908pub fn on_mulliganned(game: &mut GameState, player: PlayerId) {
909    game.player_mut(player)
910        .num_cards_in_hand_started_this_turn_with =
911        game.cards_in_zone(ZoneType::Hand, player).len() as i32;
912}
913
914pub fn copy_commanders_to_snapshot(game: &GameState, player: PlayerId) -> Vec<CardId> {
915    game.player_registered_commanders(player).to_vec()
916}
917
918pub fn update_merged_commander_info(game: &mut GameState, player: PlayerId) {
919    let commanders = game.player_registered_commanders(player).to_vec();
920    for commander in commanders {
921        game.card_mut(commander).is_commander = true;
922    }
923}
924
925pub fn init_variants_zones(game: &mut GameState, player: PlayerId) {
926    for zone in [
927        ZoneType::ExtraHand,
928        ZoneType::SchemeDeck,
929        ZoneType::PlanarDeck,
930        ZoneType::AttractionDeck,
931        ZoneType::ContraptionDeck,
932        ZoneType::Junkyard,
933    ] {
934        let _ = game.zone(zone, player);
935    }
936}
937
938pub fn init_commander_color(game: &mut GameState, player: PlayerId) {
939    let commanders = game.player_registered_commanders(player).to_vec();
940    for commander in commanders {
941        let has_choose_color = game.card(commander).static_abilities.iter().any(|st_ab| {
942            st_ab
943                .ir
944                .description_text
945                .as_deref()
946                .map(|desc| {
947                    desc.contains(
948                        "If CARDNAME is your commander, choose a color before the game begins.",
949                    )
950                })
951                .unwrap_or(false)
952        });
953        if has_choose_color && game.card(commander).chosen_colors.is_empty() {
954            game.card_mut(commander).add_chosen_color("White");
955        }
956    }
957}
958
959pub fn all_cards_unique_mana_symbols(game: &GameState, player: PlayerId) -> bool {
960    use std::collections::HashSet;
961
962    let mut colored = HashSet::new();
963    let mut generic = HashSet::new();
964    for &card_id in game.cards_in_zone(ZoneType::Library, player) {
965        let cost = &game.card(card_id).mana_cost;
966        for shard in cost.shards() {
967            if !colored.insert(*shard) {
968                return false;
969            }
970        }
971        let generic_cost = cost.generic_cost();
972        if (generic_cost > 0 || cost.cmc() == 0) && !generic.insert(generic_cost) {
973            return false;
974        }
975    }
976    true
977}
978
979pub fn assign_companion(game: &mut GameState, player: PlayerId, card: CardId) {
980    if game.card(card).zone != ZoneType::Sideboard {
981        return;
982    }
983    create_companion_effect(game, player);
984    game.move_card(card, ZoneType::Command, player);
985}
986
987pub fn deck_matches_deck_restriction(
988    game: &GameState,
989    player: PlayerId,
990    restriction: &str,
991) -> bool {
992    let restrictions: Vec<&str> = restriction
993        .split(',')
994        .map(str::trim)
995        .filter(|s| !s.is_empty())
996        .collect();
997    if restrictions.is_empty() {
998        return true;
999    }
1000    game.cards_in_zone(ZoneType::Library, player)
1001        .iter()
1002        .copied()
1003        .all(|card_id| {
1004            restrictions
1005                .iter()
1006                .all(|restriction| game.card(card_id).has_property(restriction))
1007        })
1008}
1009
1010pub fn create_companion_effect(game: &mut GameState, player: PlayerId) {
1011    if game.player(player).companion_effect_card.is_some() {
1012        return;
1013    }
1014    let mut effect = new_player_effect_card(player, "Companion Effect", None);
1015    add_static_ability(
1016        &mut effect,
1017        "Mode$ Continuous | EffectZone$ Command | Affected$ Card.YouOwn+EffectSource | AffectedZone$ Command | AddAbility$ MoveToHand",
1018    );
1019    effect.set_s_var(
1020        "MoveToHand",
1021        "ST$ ChangeZone | Cost$ 3 | Defined$ Self | Origin$ Command | Destination$ Hand | SorcerySpeed$ True | ActivationZone$ Command | SpellDescription$ Companion - Put CARDNAME in to your hand",
1022    );
1023    let effect_id = game.create_card(effect);
1024    game.move_card(effect_id, ZoneType::Command, player);
1025    game.player_mut(player).companion_effect_card = Some(effect_id);
1026}
1027
1028pub fn create_planechase_effects(game: &mut GameState, player: PlayerId) {
1029    if game.player(player).planar_dice_effect_card.is_some() {
1030        return;
1031    }
1032    let mut effect = new_player_effect_card(player, "Planar Dice", None);
1033    add_trigger_ability(
1034        &mut effect,
1035        "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | ValidPlayer$ You | Secondary$ True | Execute$ RolledPlaneswalk | TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk.",
1036        [("RolledPlaneswalk", "DB$ Planeswalk | Cause$ PlanarDie")],
1037    );
1038    effect.set_s_var(
1039        "RollPlanarDice",
1040        "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of times you have previously taken this action this turn. | CostDesc$ {X}: ",
1041    );
1042    let effect_id = game.create_card(effect);
1043    game.move_card(effect_id, ZoneType::Command, player);
1044    game.player_mut(player).planar_dice_effect_card = Some(effect_id);
1045}
1046
1047pub fn create_the_ring(game: &mut GameState, player: PlayerId) {
1048    if game.player(player).ring_effect_card.is_some() {
1049        return;
1050    }
1051    let effect = new_player_effect_card(player, "The Ring", None);
1052    let effect_id = game.create_card(effect);
1053    game.move_card(effect_id, ZoneType::Command, player);
1054    game.player_mut(player).ring_effect_card = Some(effect_id);
1055}
1056
1057pub fn change_ownership(game: &mut GameState, card_id: CardId, new_owner: PlayerId) {
1058    let old_owner = game.card(card_id).owner;
1059    if old_owner == new_owner {
1060        return;
1061    }
1062    game.card_mut(card_id).owner = new_owner;
1063    if !game.card(card_id).is_token {
1064        let old_player = game.player_mut(old_owner);
1065        if let Some(pos) = old_player
1066            .gained_ownership
1067            .iter()
1068            .position(|&c| c == card_id)
1069        {
1070            old_player.gained_ownership.remove(pos);
1071        } else if !old_player.lost_ownership.contains(&card_id) {
1072            old_player.lost_ownership.push(card_id);
1073        }
1074        let new_player = game.player_mut(new_owner);
1075        if let Some(pos) = new_player.lost_ownership.iter().position(|&c| c == card_id) {
1076            new_player.lost_ownership.remove(pos);
1077        } else if !new_player.gained_ownership.contains(&card_id) {
1078            new_player.gained_ownership.push(card_id);
1079        }
1080    }
1081}
1082
1083pub fn destroy_physical_card(game: &mut GameState, card_id: CardId) {
1084    if !game.card(card_id).is_token {
1085        let owner = game.card(card_id).owner;
1086        if !game.player(owner).lost_ownership.contains(&card_id) {
1087            game.player_mut(owner).lost_ownership.push(card_id);
1088        }
1089    }
1090    let zone = game.card(card_id).zone;
1091    let controller = game.card(card_id).controller;
1092    if zone != ZoneType::None {
1093        game.remove_card_from_zone(zone, controller, card_id);
1094    }
1095    game.card_mut(card_id).zone = ZoneType::None;
1096}
1097
1098pub fn push_paid_for_sa(game: &mut GameState, player: PlayerId, sa: &SpellAbility) {
1099    game.player_mut(player).paid_for_stack.push(sa.clone());
1100}
1101
1102pub fn pop_paid_for_sa(game: &mut GameState, player: PlayerId) {
1103    game.player_mut(player).paid_for_stack.pop();
1104}
1105
1106pub fn clear_paid_for_sa(game: &mut GameState, player: PlayerId) {
1107    game.player_mut(player).paid_for_stack.clear();
1108}
1109
1110pub fn update_keyword_card_ability_text(game: &mut GameState, player: PlayerId) {
1111    trim_keywords(game, player);
1112    let has_keywords = !game.player(player).changed_keywords.is_empty();
1113    match (game.player(player).keyword_effect_card, has_keywords) {
1114        (Some(effect_id), false) => {
1115            if game.card(effect_id).zone != ZoneType::None {
1116                game.remove_card_from_zone(ZoneType::Command, player, effect_id);
1117                game.card_mut(effect_id).zone = ZoneType::None;
1118            }
1119            game.player_mut(player).keyword_effect_card = None;
1120        }
1121        (None, true) => {
1122            let effect = new_player_effect_card(player, "Keyword Effects", None);
1123            let effect_id = game.create_card(effect);
1124            game.move_card(effect_id, ZoneType::Command, player);
1125            game.player_mut(player).keyword_effect_card = Some(effect_id);
1126        }
1127        _ => {}
1128    }
1129}
1130
1131pub fn trim_keywords(game: &mut GameState, player: PlayerId) {
1132    game.player_mut(player).changed_keywords.sort();
1133    game.player_mut(player).changed_keywords.dedup();
1134}
1135
1136pub fn check_keyword_card(game: &GameState, player: PlayerId, keyword: &str) -> bool {
1137    game.player(player)
1138        .changed_keywords
1139        .iter()
1140        .any(|candidate| candidate == keyword)
1141}
1142
1143pub fn can_search_library_with(game: &GameState, player: PlayerId, source: CardId) -> bool {
1144    if has_keyword(game, player, "CantSearchLibrary") {
1145        return false;
1146    }
1147    let source_controller = game.card(source).controller;
1148    !(source_controller == player
1149        && has_keyword(
1150            game,
1151            player,
1152            "Spells and abilities you control can't cause you to search your library.",
1153        ))
1154}
1155
1156pub fn add_additional_vote(game: &mut GameState, player: PlayerId, key: i64, amount: i32) {
1157    game.player_mut(player).additional_votes.insert(key, amount);
1158}
1159
1160pub fn remove_additional_vote(game: &mut GameState, player: PlayerId, key: i64) {
1161    game.player_mut(player).additional_votes.remove(&key);
1162}
1163
1164pub fn add_additional_optional_vote(game: &mut GameState, player: PlayerId, key: i64, amount: i32) {
1165    game.player_mut(player)
1166        .additional_optional_votes
1167        .insert(key, amount);
1168}
1169
1170pub fn remove_additional_optional_vote(game: &mut GameState, player: PlayerId, key: i64) {
1171    game.player_mut(player)
1172        .additional_optional_votes
1173        .remove(&key);
1174}
1175
1176pub fn add_control_vote(game: &mut GameState, player: PlayerId, key: i64) {
1177    game.player_mut(player).control_votes.insert(key);
1178}
1179
1180pub fn remove_control_vote(game: &mut GameState, player: PlayerId, key: i64) {
1181    game.player_mut(player).control_votes.remove(&key);
1182}
1183
1184pub fn add_additional_villainous_choices(
1185    game: &mut GameState,
1186    player: PlayerId,
1187    key: i64,
1188    amount: i32,
1189) {
1190    game.player_mut(player)
1191        .additional_villainous_choices
1192        .insert(key, amount);
1193}
1194
1195pub fn remove_additional_villainous_choices(game: &mut GameState, player: PlayerId, key: i64) {
1196    game.player_mut(player)
1197        .additional_villainous_choices
1198        .remove(&key);
1199}
1200
1201pub fn learn_lesson(game: &mut GameState, player: PlayerId) {
1202    if game.player(player).has_lost {
1203        return;
1204    }
1205    let lesson = game
1206        .cards_in_zone(ZoneType::Sideboard, player)
1207        .iter()
1208        .copied()
1209        .find(|&card_id| game.card(card_id).type_line.has_subtype("Lesson"));
1210    if let Some(card_id) = lesson {
1211        game.move_card(card_id, ZoneType::Hand, player);
1212    }
1213}
1214
1215pub fn roll_to_visit_attractions(game: &mut GameState, player: PlayerId, result: i32) {
1216    roll(game, player, result);
1217    if result >= 1 {
1218        visit_attractions(game, player, 1);
1219    }
1220}
1221
1222pub fn advance_crank_counter(game: &mut GameState, player: PlayerId) -> i32 {
1223    let next = (game.player(player).crank_counter % 3) + 1;
1224    game.player_mut(player).crank_counter = next;
1225    next
1226}
1227
1228pub fn create_contraption_sprockets(game: &mut GameState, player: PlayerId) {
1229    if game
1230        .player(player)
1231        .contraption_sprocket_effect_card
1232        .is_some()
1233    {
1234        return;
1235    }
1236    let effect = new_player_effect_card(player, "Contraption Sprockets", None);
1237    let effect_id = game.create_card(effect);
1238    game.move_card(effect_id, ZoneType::Command, player);
1239    game.player_mut(player).contraption_sprocket_effect_card = Some(effect_id);
1240}
1241
1242pub fn add_declares_attackers(game: &mut GameState, player: PlayerId, declarer: PlayerId) {
1243    game.player_mut(player).declares_attackers.insert(declarer);
1244}
1245
1246pub fn remove_declares_attackers(game: &mut GameState, player: PlayerId, declarer: PlayerId) {
1247    game.player_mut(player).declares_attackers.remove(&declarer);
1248}
1249
1250pub fn add_declares_blockers(game: &mut GameState, player: PlayerId, declarer: PlayerId) {
1251    game.player_mut(player).declares_blockers.insert(declarer);
1252}
1253
1254pub fn remove_declares_blockers(game: &mut GameState, player: PlayerId, declarer: PlayerId) {
1255    game.player_mut(player).declares_blockers.remove(&declarer);
1256}
1257
1258pub fn after_static_ability_layer(game: &mut GameState, player: PlayerId) {
1259    game.player_mut(player).devotion_mod = 0;
1260}
1261
1262pub fn trigger_elemental_bend(game: &mut GameState, player: PlayerId, trigger: &str) {
1263    game.player_mut(player)
1264        .elemental_bend_triggers
1265        .insert(trigger.to_string());
1266}
1267
1268pub fn has_all_element_bend(game: &GameState, player: PlayerId, triggers: &[&str]) -> bool {
1269    triggers.iter().all(|trigger| {
1270        game.player(player)
1271            .elemental_bend_triggers
1272            .contains(*trigger)
1273    })
1274}
1275
1276pub fn planeswalk(game: &mut GameState, player: PlayerId) -> Option<CardId> {
1277    let next_plane = game
1278        .cards_in_zone(ZoneType::PlanarDeck, player)
1279        .first()
1280        .copied()?;
1281    planeswalk_to(game, player, next_plane);
1282    Some(next_plane)
1283}
1284
1285pub fn planeswalk_to(game: &mut GameState, player: PlayerId, plane: CardId) {
1286    if game
1287        .player(player)
1288        .planeswalked_to_this_turn
1289        .contains(&plane)
1290    {
1291        return;
1292    }
1293    leave_current_plane(game, player);
1294    game.move_card(plane, ZoneType::Command, player);
1295    game.player_mut(player)
1296        .planeswalked_to_this_turn
1297        .push(plane);
1298}
1299
1300pub fn leave_current_plane(game: &mut GameState, player: PlayerId) {
1301    if let Some(current) = game
1302        .cards_in_zone(ZoneType::Command, player)
1303        .iter()
1304        .copied()
1305        .find(|&card_id| {
1306            let card = game.card(card_id);
1307            card.type_line.has_subtype("Plane") || card.type_line.has_subtype("Phenomenon")
1308        })
1309    {
1310        game.move_card(current, ZoneType::PlanarDeck, player);
1311    }
1312}
1313
1314pub fn remove_current_plane(game: &mut GameState, player: PlayerId) -> Option<CardId> {
1315    let current = game
1316        .cards_in_zone(ZoneType::Command, player)
1317        .iter()
1318        .copied()
1319        .find(|&card_id| {
1320            let card = game.card(card_id);
1321            card.type_line.has_subtype("Plane") || card.type_line.has_subtype("Phenomenon")
1322        })?;
1323    game.remove_card_from_zone(ZoneType::Command, player, current);
1324    game.card_mut(current).zone = ZoneType::None;
1325    Some(current)
1326}
1327
1328pub fn init_plane(game: &mut GameState, player: PlayerId, plane: CardId) {
1329    game.move_card(plane, ZoneType::Command, player);
1330}
1331
1332pub fn reveal_face_down_cards(game: &GameState, player: PlayerId) -> Vec<CardId> {
1333    game.cards_in_zone(ZoneType::Battlefield, player)
1334        .iter()
1335        .copied()
1336        .filter(|&card_id| game.card(card_id).face_down)
1337        .collect()
1338}