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}