1use super::{resolve_defined_players, resolve_numeric_svar, EffectContext};
2use std::collections::{HashMap, HashSet};
3
4use crate::agent::notification::GameNotification;
5use crate::agent::GameLogEvent;
6use crate::cost::{parse_cost, Cost, CostPart};
7use crate::event::RunParams;
8use crate::game::GameState;
9use crate::game_rng::GameRng;
10use crate::ids::PlayerId;
11use crate::mana;
12use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
13use crate::replacement::ReplacementResult;
14use crate::spellability::{build_spell_ability, SpellAbility};
15use crate::trigger::handler::TriggerHandler;
16use crate::trigger::TriggerType;
17use forge_foundation::ZoneType;
18
19pub fn make_formated_description(game: &GameState, sa: &SpellAbility) -> String {
25 let source_id = match sa.source {
26 Some(id) => id,
27 None => return "Roll a die.".to_string(),
28 };
29
30 let sides = sa.ir.sides.unwrap_or(6);
31 let card_name = game.card(source_id).card_name.clone();
32
33 let mut desc = format!("{card_name} — Roll a d{sides}.");
34
35 if let Some(result_str) = sa.ir.result_sub_abilities_text.as_deref() {
36 desc.push('\n');
37 for entry in result_str.split(',') {
38 let parts: Vec<&str> = entry.splitn(2, ':').collect();
39 if parts.len() == 2 {
40 let threshold = parts[0].trim();
41 let svar_name = parts[1].trim();
42 if let Some(svar_text) = game.card(source_id).get_s_var(svar_name) {
43 let params = crate::parsing::Params::from_raw(svar_text);
44 let effect_desc = params
45 .get_cloned(crate::parsing::keys::SPELL_DESCRIPTION)
46 .unwrap_or_else(|| svar_name.to_string());
47 desc.push_str(&format!(" {threshold}: {effect_desc}\n"));
48 }
49 }
50 }
51 }
52
53 desc
54}
55
56pub fn get_results_list(natural_results: &[i32]) -> Vec<DieRollResult> {
59 natural_results
60 .iter()
61 .map(|&r| DieRollResult {
62 natural_value: r,
63 modified_value: r,
64 })
65 .collect()
66}
67
68pub fn get_natural_results(results: &[DieRollResult]) -> Vec<i32> {
71 results.iter().map(|r| r.natural_value).collect()
72}
73
74pub fn get_final_results(results: &[DieRollResult]) -> Vec<i32> {
77 results.iter().map(|r| r.modified_value).collect()
78}
79
80pub fn get_increment_cards(
84 game: &GameState,
85 player: PlayerId,
86 xeno_keyword: &str,
87 night_shift_keyword: &str,
88) -> Vec<crate::ids::CardId> {
89 let mut out = Vec::new();
90 let battlefield: Vec<_> = game.cards_in_zone(ZoneType::Battlefield, player).to_vec();
91
92 for cid in battlefield.iter().copied() {
93 let card = game.card(cid);
94 if card.has_keyword(xeno_keyword)
95 && card.counter_count(&crate::card::counter_type::CounterType::P1P1) > 0
96 {
97 out.push(cid);
98 }
99 }
100 for cid in battlefield {
101 let card = game.card(cid);
102 if !card.has_keyword(night_shift_keyword) {
103 continue;
104 }
105 if game.player(player).life >= 1 {
107 out.push(cid);
108 }
109 }
110 out
111}
112
113pub fn roll_dice_for_player(
118 ctx: &mut EffectContext,
119 sa: &SpellAbility,
120 source_id: crate::ids::CardId,
121 player: PlayerId,
122 sides: i32,
123 amount: i32,
124) -> i32 {
125 roll_for_player(ctx, sa, source_id, player, sides, amount)
126}
127
128pub fn roll_dice_for_player_to_visit_attractions(
134 game: &mut GameState,
135 trigger_handler: &mut TriggerHandler,
136 rng: &mut (impl crate::game_rng::GameRng + ?Sized),
137 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
138 mana_pools: &mut [crate::mana::ManaPool],
139 player: PlayerId,
140) {
141 roll_to_visit_attractions(game, trigger_handler, rng, agents, mana_pools, player);
142}
143
144#[manabrew_engine_macros::spell_effect(RollDiceEffect)]
160fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
161 let controller = sa.activating_player;
162 let source_id = match sa.source {
163 Some(id) => id,
164 None => return,
165 };
166 let sides = sa.ir.sides.unwrap_or(6);
167 let amount = resolve_numeric_svar(ctx.game, sa, "Amount", 1).max(0);
168 let players = if let Some(player) = sa.target_chosen.target_player {
169 vec![player]
170 } else if let Some(defined) = sa.defined() {
171 let resolved = resolve_defined_players(defined, controller, ctx.game);
172 if resolved.is_empty() {
173 vec![controller]
174 } else {
175 resolved
176 }
177 } else {
178 vec![controller]
179 };
180
181 let mut results = Vec::new();
182 for player in players.iter().copied() {
183 let final_result = roll_for_player(ctx, sa, source_id, player, sides, amount);
184 if sa.param_is_true("ToVisitYourAttractions") {
185 visit_attractions(ctx.game, ctx.trigger_handler, player, final_result);
186 }
187 results.push((player, final_result));
188 }
189
190 if sa.param_is_true("RememberHighestPlayer") {
191 if let Some(highest) = results.iter().map(|(_, result)| *result).max() {
192 for (player, result) in results {
193 if result == highest {
194 ctx.game.card_mut(source_id).add_remembered_player(player);
195 }
196 }
197 }
198 }
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub struct DieRollResult {
205 pub natural_value: i32,
206 pub modified_value: i32,
207}
208
209fn roll_for_player(
210 ctx: &mut EffectContext,
211 sa: &SpellAbility,
212 source_id: crate::ids::CardId,
213 player: PlayerId,
214 sides: i32,
215 amount: i32,
216) -> i32 {
217 let modifier = resolve_numeric_svar(ctx.game, sa, "Modifier", 0);
218 let rolled_to_visit_attractions = sa.param_is_true("ToVisitYourAttractions");
219 let ignore = resolve_numeric_svar(ctx.game, sa, "IgnoreLower", 0);
220 let mut ignored_rolls = Vec::new();
221 let mut dice_pt_exchanges = HashSet::new();
222 let source_name = ctx.game.card(source_id).card_name.clone();
223 let mut natural_rolls = roll_action(
224 ctx.game,
225 ctx.rng,
226 ctx.agents,
227 player,
228 sides,
229 amount,
230 ignore,
231 &mut ignored_rolls,
232 &mut dice_pt_exchanges,
233 Some(source_id),
234 );
235
236 if sa.param_is_true("UseHighestRoll") && natural_rolls.len() > 1 {
237 let highest = natural_rolls.last().copied().unwrap_or(0);
238 natural_rolls.clear();
239 natural_rolls.push(highest);
240 }
241
242 apply_keyword_roll_rerolls(
243 ctx,
244 sa,
245 player,
246 sides,
247 &mut natural_rolls,
248 &mut ignored_rolls,
249 &mut dice_pt_exchanges,
250 );
251
252 let mut results_list = apply_keyword_roll_modifiers(ctx, sa, player, &mut natural_rolls);
253 for unmodified in natural_rolls {
254 results_list.push(DieRollResult {
255 natural_value: unmodified,
256 modified_value: unmodified,
257 });
258 }
259
260 apply_dice_pt_exchanges(
261 ctx.agents,
262 ctx.game,
263 player,
264 &mut results_list,
265 &dice_pt_exchanges,
266 );
267
268 let kept_natural_rolls: Vec<i32> = results_list.iter().map(|roll| roll.natural_value).collect();
269 let kept_rolls: Vec<i32> = results_list
270 .iter()
271 .map(|roll| roll.modified_value + modifier)
272 .collect();
273
274 let final_result = if sa.param_is_true("UseDifferenceBetweenRolls") && kept_rolls.len() >= 2 {
275 kept_rolls.last().copied().unwrap_or(0) - kept_rolls.first().copied().unwrap_or(0)
276 } else if sa.param_is_true("UseHighestRoll") {
277 kept_rolls.iter().copied().max().unwrap_or(0)
278 } else {
279 kept_rolls.iter().sum()
280 };
281
282 if let Some(result_svar) = sa.ir.result_svar_text.as_deref() {
283 ctx.game
284 .card_mut(source_id)
285 .set_s_var(result_svar, format!("Number${final_result}"));
286 }
287 if sa.param_is_true("StoreResults") {
288 for roll in &kept_rolls {
289 ctx.game.card_mut(source_id).add_stored_rolls(*roll);
290 }
291 }
292 if sa.param_is_true("EvenOddResults") {
293 let even = kept_rolls.iter().filter(|roll| **roll % 2 == 0).count();
294 let odd = kept_rolls.len().saturating_sub(even);
295 let source = ctx.game.card_mut(source_id);
296 source.set_s_var("EvenResults", format!("Number${even}"));
297 source.set_s_var("OddResults", format!("Number${odd}"));
298 }
299 if sa.param_is_true("DifferentResults") {
300 let mut distinct = kept_rolls.clone();
301 distinct.sort();
302 distinct.dedup();
303 ctx.game
304 .card_mut(source_id)
305 .set_s_var("DifferentResults", format!("Number${}", distinct.len()));
306 }
307 if sa.param_is_true("MaxRollsResults") {
308 let max_rolls = kept_natural_rolls
309 .iter()
310 .filter(|roll| **roll == sides)
311 .count();
312 ctx.game
313 .card_mut(source_id)
314 .set_s_var("MaxRolls", format!("Number${max_rolls}"));
315 }
316 if let Some(chosen_svar) = sa.ir.chosen_svar_text.as_deref() {
317 if !kept_rolls.is_empty() {
318 let chosen = ctx.agents[player.index()]
319 .choose_number_from_list(player, &kept_rolls, "Choose a result", Some(source_id))
320 .unwrap_or(kept_rolls[0]);
321 ctx.game
322 .card_mut(source_id)
323 .set_s_var(chosen_svar, format!("Number${chosen}"));
324 if let Some(other_svar) = sa.ir.other_svar_text.as_deref() {
325 let other = kept_rolls
326 .iter()
327 .copied()
328 .find(|roll| *roll != chosen)
329 .unwrap_or(chosen);
330 ctx.game
331 .card_mut(source_id)
332 .set_s_var(other_svar, format!("Number${other}"));
333 }
334 }
335 }
336 if sa.param_is_true("NoteDoubles") {
337 let mut unique = std::collections::HashSet::new();
338 if kept_rolls.iter().any(|roll| !unique.insert(*roll)) {
339 ctx.game.card_mut(source_id).set_s_var("Doubles", "1");
340 }
341 }
342
343 let roll_start_number = ctx.game.player(player).num_rolls_this_turn;
344 let roll_number_base = roll_start_number - amount;
345 for (idx, roll) in results_list.iter().enumerate() {
346 let result = roll.modified_value + modifier;
347 ctx.trigger_handler.run_trigger(
348 TriggerType::RolledDie,
349 RunParams {
350 player: Some(player),
351 die_result: Some(result),
352 natural_result: Some(roll.natural_value),
353 die_sides: Some(sides),
354 number: Some(roll_number_base + idx as i32 + 1),
355 rolled_to_visit_attractions: Some(rolled_to_visit_attractions),
356 ..Default::default()
357 },
358 false,
359 );
360 }
361 ctx.trigger_handler.run_trigger(
362 TriggerType::RolledDieOnce,
363 RunParams {
364 player: Some(player),
365 die_result: Some(final_result),
366 die_results: Some(kept_rolls.clone()),
367 die_sides: Some(sides),
368 rolled_to_visit_attractions: Some(rolled_to_visit_attractions),
369 ..Default::default()
370 },
371 false,
372 );
373
374 let rolled_text = kept_rolls
376 .iter()
377 .map(i32::to_string)
378 .collect::<Vec<_>>()
379 .join(", ");
380 crate::agent::notify_all_agents(
381 ctx.agents,
382 GameLogEvent::rule(format!("Rolled {rolled_text} (d{sides})")).with_player(player),
383 );
384 if !ignored_rolls.is_empty() {
385 let ignored_text = ignored_rolls
386 .iter()
387 .map(i32::to_string)
388 .collect::<Vec<_>>()
389 .join(", ");
390 crate::agent::notify_all_agents(
391 ctx.agents,
392 GameLogEvent::rule(format!("Ignored rolls: {ignored_text}")).with_player(player),
393 );
394 }
395 crate::agent::game_log::broadcast_notification(
396 ctx.agents,
397 GameNotification::DiceRolled {
398 player,
399 sides,
400 natural_results: kept_natural_rolls.clone(),
401 final_results: kept_rolls.clone(),
402 ignored_rolls: ignored_rolls.clone(),
403 source_card_name: Some(source_name.clone()),
404 },
405 );
406 for agent in ctx.agents.iter_mut() {
407 agent.await_display_ack();
408 }
409
410 if sa.param_is_true("SubsForEach") {
411 if let Some(result_str) = sa.ir.result_sub_abilities_text.as_deref() {
412 for roll in &kept_rolls {
413 resolve_result_sub_ability(ctx, sa, source_id, player, *roll, result_str);
414 }
415 }
416 } else if let Some(result_str) = sa.ir.result_sub_abilities_text.as_deref() {
417 resolve_result_sub_ability(ctx, sa, source_id, player, final_result, result_str);
418 }
419
420 if sa.param_is_true("RerollResults") {
421 let stored_rolls = ctx.game.card(source_id).remembered_cmc.clone();
422 let mut replacements = Vec::new();
423 for old_roll in stored_rolls {
424 if ctx.agents[player.index()].confirm_action(
425 player,
426 Some("RerollResult"),
427 &format!("Reroll result {old_roll}?"),
428 &[],
429 Some(source_id),
430 sa.api,
431 ) {
432 let new_roll = roll_for_player(ctx, sa, source_id, player, sides, 1);
433 replacements.push((old_roll, new_roll));
434 }
435 }
436 for (old_roll, new_roll) in replacements {
437 ctx.game
438 .card_mut(source_id)
439 .replace_stored_roll(old_roll, new_roll);
440 }
441 }
442
443 final_result
445}
446
447pub fn roll_to_visit_attractions(
448 game: &mut GameState,
449 trigger_handler: &mut TriggerHandler,
450 rng: &mut (impl GameRng + ?Sized),
451 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
452 mana_pools: &mut [crate::mana::ManaPool],
453 player: PlayerId,
454) {
455 let mut event = ReplacementEvent::RollDice {
456 player,
457 sides: 6,
458 number: 1,
459 ignore: 0,
460 ignore_chosen: HashMap::new(),
461 dice_pt_exchanges: HashSet::new(),
462 };
463 let result = apply_replacements(game, &mut event);
464 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
465 return;
466 }
467
468 let (roll_count, ignore_lowest, ignore_chosen, dice_pt_exchanges) = match event {
469 ReplacementEvent::RollDice {
470 number,
471 ignore,
472 ignore_chosen,
473 dice_pt_exchanges,
474 ..
475 } => (
476 number.max(0),
477 ignore.max(0),
478 ignore_chosen.clone(),
479 dice_pt_exchanges.clone(),
480 ),
481 _ => (1, 0, HashMap::new(), HashSet::new()),
482 };
483 if roll_count == 0 {
484 return;
485 }
486
487 let mut natural_rolls = Vec::new();
488 for _ in 0..roll_count {
489 natural_rolls.push(rng.next_int(6) + 1);
490 }
491 natural_rolls.sort();
492
493 let ignore_count = ignore_lowest.min(natural_rolls.len() as i32) as usize;
494 let ignored_rolls: Vec<i32> = natural_rolls.drain(..ignore_count).collect();
495 let (ignored_by_choice, _) = apply_chosen_ignores(
496 agents,
497 "Attraction roll",
498 &mut natural_rolls,
499 &ignore_chosen,
500 );
501 let mut ignored_rolls = ignored_rolls;
502 ignored_rolls.extend(ignored_by_choice);
503 let mut results_list = apply_keyword_roll_modifiers_for_attraction(
504 game,
505 trigger_handler,
506 agents,
507 mana_pools,
508 player,
509 &mut natural_rolls,
510 );
511 for unmodified in natural_rolls {
512 results_list.push(DieRollResult {
513 natural_value: unmodified,
514 modified_value: unmodified,
515 });
516 }
517 apply_dice_pt_exchanges(agents, game, player, &mut results_list, &dice_pt_exchanges);
518 let kept_rolls: Vec<i32> = results_list
519 .iter()
520 .map(|roll| roll.modified_value)
521 .collect();
522 let final_result: i32 = kept_rolls.iter().sum();
523
524 let roll_start_number = game.player(player).num_rolls_this_turn;
525 let roll_number_base = roll_start_number - 1;
526 for (idx, result) in kept_rolls.iter().copied().enumerate() {
527 trigger_handler.run_trigger(
528 TriggerType::RolledDie,
529 RunParams {
530 player: Some(player),
531 die_result: Some(result),
532 natural_result: Some(results_list[idx].natural_value),
533 die_sides: Some(6),
534 number: Some(roll_number_base + idx as i32 + 1),
535 rolled_to_visit_attractions: Some(true),
536 ..Default::default()
537 },
538 false,
539 );
540 }
541 trigger_handler.run_trigger(
542 TriggerType::RolledDieOnce,
543 RunParams {
544 player: Some(player),
545 die_result: Some(final_result),
546 die_results: Some(kept_rolls.clone()),
547 die_sides: Some(6),
548 rolled_to_visit_attractions: Some(true),
549 ..Default::default()
550 },
551 false,
552 );
553
554 let rolled_text = kept_rolls
555 .iter()
556 .map(i32::to_string)
557 .collect::<Vec<_>>()
558 .join(", ");
559 crate::agent::notify_all_agents(
560 agents,
561 GameLogEvent::rule(format!("Rolled {rolled_text} (d6)")).with_player(player),
562 );
563 if !ignored_rolls.is_empty() {
564 let ignored_text = ignored_rolls
565 .iter()
566 .map(i32::to_string)
567 .collect::<Vec<_>>()
568 .join(", ");
569 crate::agent::notify_all_agents(
570 agents,
571 GameLogEvent::rule(format!("Ignored rolls: {ignored_text}")).with_player(player),
572 );
573 }
574 let natural_results: Vec<i32> = results_list.iter().map(|r| r.natural_value).collect();
575 crate::agent::game_log::broadcast_notification(
576 agents,
577 GameNotification::DiceRolled {
578 player,
579 sides: 6,
580 natural_results,
581 final_results: kept_rolls.clone(),
582 ignored_rolls: ignored_rolls.clone(),
583 source_card_name: Some("Attraction roll".to_string()),
584 },
585 );
586 for agent in agents.iter_mut() {
587 agent.await_display_ack();
588 }
589
590 visit_attractions(game, trigger_handler, player, final_result);
591}
592
593fn apply_chosen_ignores(
594 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
595 _card_name: &str,
596 natural_rolls: &mut Vec<i32>,
597 ignore_chosen: &HashMap<PlayerId, i32>,
598) -> (Vec<i32>, Vec<i32>) {
599 let mut ignored = Vec::new();
600 for (&chooser, &count) in ignore_chosen {
601 for _ in 0..count.max(0) {
602 if natural_rolls.is_empty() {
603 break;
604 }
605 let choice = agents[chooser.index()]
606 .choose_roll_to_ignore(chooser, natural_rolls, None)
607 .unwrap_or(natural_rolls[0]);
608 let position = natural_rolls
609 .iter()
610 .position(|roll| *roll == choice)
611 .unwrap_or(0);
612 ignored.push(natural_rolls.remove(position));
613 }
614 }
615 (ignored, natural_rolls.clone())
616}
617
618fn roll_action(
619 game: &mut GameState,
620 rng: &mut (impl GameRng + ?Sized),
621 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
622 player: PlayerId,
623 sides: i32,
624 amount: i32,
625 ignore: i32,
626 ignored_rolls: &mut Vec<i32>,
627 dice_pt_exchanges: &mut HashSet<crate::ids::CardId>,
628 _source: Option<crate::ids::CardId>,
629) -> Vec<i32> {
630 let mut event = ReplacementEvent::RollDice {
631 player,
632 sides,
633 number: amount,
634 ignore,
635 ignore_chosen: HashMap::new(),
636 dice_pt_exchanges: HashSet::new(),
637 };
638 let result = apply_replacements(game, &mut event);
639 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
640 return Vec::new();
641 }
642
643 let (roll_count, ignore_lowest, ignore_chosen, new_pt_exchanges) = match event {
644 ReplacementEvent::RollDice {
645 number,
646 ignore,
647 ignore_chosen,
648 dice_pt_exchanges,
649 ..
650 } => (
651 number.max(0),
652 ignore.max(0),
653 ignore_chosen,
654 dice_pt_exchanges,
655 ),
656 _ => (amount.max(0), ignore.max(0), HashMap::new(), HashSet::new()),
657 };
658 dice_pt_exchanges.extend(new_pt_exchanges);
659
660 let mut natural_rolls = Vec::new();
661 for _ in 0..roll_count {
662 natural_rolls.push(rng.next_int(sides) + 1);
663 game.player_record_roll(player, None);
664 }
665 natural_rolls.sort();
666
667 let ignore_count = ignore_lowest.min(natural_rolls.len() as i32) as usize;
668 ignored_rolls.extend(natural_rolls.drain(..ignore_count));
669
670 let (ignored_by_choice, _) =
671 apply_chosen_ignores(agents, "Roll", &mut natural_rolls, &ignore_chosen);
672 ignored_rolls.extend(ignored_by_choice);
673 natural_rolls
674}
675
676fn apply_dice_pt_exchanges(
677 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
678 game: &mut GameState,
679 player: PlayerId,
680 results_list: &mut [DieRollResult],
681 dice_pt_exchanges: &HashSet<crate::ids::CardId>,
682) {
683 let mut available_cards: Vec<_> = dice_pt_exchanges.iter().copied().collect();
684 while !available_cards.is_empty() && !results_list.is_empty() {
685 let current_rolls: Vec<i32> = results_list
686 .iter()
687 .map(|roll| roll.modified_value)
688 .collect();
689 let roll = agents[player.index()].choose_roll_to_swap(player, ¤t_rolls, None);
690 let Some(roll_value) = roll else { break };
691 let Some(roll_index) = results_list
692 .iter()
693 .position(|roll| roll.modified_value == roll_value)
694 else {
695 break;
696 };
697 let card_id = available_cards.remove(0);
698 let current_power = game.card(card_id).power();
699 let current_toughness = game.card(card_id).toughness();
700 let choice = agents[player.index()].choose_roll_swap_value(
701 player,
702 roll_value,
703 current_power,
704 current_toughness,
705 Some(card_id),
706 );
707 let Some(choice) = choice else { continue };
708 match choice {
709 crate::agent::RollSwapChoice::Power => {
710 results_list[roll_index].modified_value = current_power;
711 game.card_mut(card_id)
712 .add_new_pt(roll_value, current_toughness);
713 }
714 crate::agent::RollSwapChoice::Toughness => {
715 results_list[roll_index].modified_value = current_toughness;
716 game.card_mut(card_id).add_new_pt(current_power, roll_value);
717 }
718 }
719 }
720}
721
722fn apply_keyword_roll_modifiers(
723 ctx: &mut EffectContext,
724 sa: &SpellAbility,
725 player: PlayerId,
726 natural_rolls: &mut Vec<i32>,
727) -> Vec<DieRollResult> {
728 apply_simple_roll_modifiers(
729 ctx.game,
730 ctx.trigger_handler,
731 ctx.agents,
732 ctx.mana_pools,
733 player,
734 natural_rolls,
735 Some(sa),
736 )
737}
738
739fn apply_keyword_roll_modifiers_for_attraction(
740 game: &mut GameState,
741 trigger_handler: &mut TriggerHandler,
742 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
743 mana_pools: &mut [crate::mana::ManaPool],
744 player: PlayerId,
745 natural_rolls: &mut Vec<i32>,
746) -> Vec<DieRollResult> {
747 apply_simple_roll_modifiers(
748 game,
749 trigger_handler,
750 agents,
751 mana_pools,
752 player,
753 natural_rolls,
754 None,
755 )
756}
757
758fn apply_simple_roll_modifiers(
759 game: &mut GameState,
760 trigger_handler: &mut TriggerHandler,
761 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
762 mana_pools: &mut [crate::mana::ManaPool],
763 player: PlayerId,
764 natural_rolls: &mut Vec<i32>,
765 source_sa: Option<&SpellAbility>,
766) -> Vec<DieRollResult> {
767 const XENO_KEYWORD: &str =
768 "After you roll a die, you may remove a +1/+1 counter from Xenosquirrels. If you do, increase or decrease the result by 1.";
769 const NIGHT_SHIFT_KEYWORD: &str =
770 "After you roll a die, you may pay 1 life. If you do, increase or decrease the result by 1. Do this only once each turn.";
771 let mut results_list = Vec::new();
772
773 loop {
774 if natural_rolls.is_empty() {
775 break;
776 }
777
778 let modifiers: Vec<_> = game
779 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
780 .iter()
781 .copied()
782 .filter(|&card_id| {
783 can_modify_roll(game, player, card_id, XENO_KEYWORD, NIGHT_SHIFT_KEYWORD)
784 })
785 .collect();
786 if modifiers.is_empty() {
787 break;
788 }
789
790 let roll = agents[player.index()].choose_roll_to_modify(player, natural_rolls, None);
791 let Some(mut roll_value) = roll else { break };
792 let Some(roll_index) = natural_rolls.iter().position(|value| *value == roll_value) else {
793 break;
794 };
795
796 let mut available = modifiers;
797 let mut modified = false;
798 let natural_value = roll_value;
799 while !available.is_empty() {
800 let chosen = agents[player.index()].choose_single_card_for_zone_change(
801 game,
802 player,
803 &available,
804 "Choose a roll modifier",
805 true,
806 );
807 let Some(card_id) = chosen else { break };
808 let roll_modify_cost = game
809 .card(card_id)
810 .svars
811 .get("RollModifyCost")
812 .cloned()
813 .unwrap_or_default();
814 let can_pay = pay_roll_cost(
815 game,
816 trigger_handler,
817 agents,
818 mana_pools,
819 player,
820 card_id,
821 &roll_modify_cost,
822 source_sa,
823 );
824 available.retain(|candidate| *candidate != card_id);
825 if !can_pay {
826 continue;
827 }
828 let increase = agents[player.index()].choose_binary(
829 player,
830 "Increase or decrease the roll?",
831 crate::agent::BinaryChoiceKind::IncreaseOrDecrease,
832 Some(true),
833 Some(card_id),
834 source_sa.and_then(|sa| sa.api),
835 );
836 roll_value += if increase { 1 } else { -1 };
837 increment_roll_modifications(game, card_id);
838 modified = true;
839 }
840
841 if modified {
842 natural_rolls.remove(roll_index);
843 results_list.push(DieRollResult {
844 natural_value,
845 modified_value: roll_value,
846 });
847 } else {
848 break;
849 }
850 }
851 results_list
852}
853
854fn can_modify_roll(
855 game: &GameState,
856 player: PlayerId,
857 card_id: crate::ids::CardId,
858 xeno_keyword: &str,
859 night_shift_keyword: &str,
860) -> bool {
861 let card = game.card(card_id);
862 if card.has_keyword(xeno_keyword) {
863 return card.counter_count(&crate::card::CounterType::P1P1) > 0;
864 }
865 if card.has_keyword(night_shift_keyword) {
866 let limit = card
867 .svars
868 .get("RollModificationsLimit")
869 .map(|value| value.trim())
870 .unwrap_or("None");
871 let used = roll_modifications_used(card);
872 let under_limit = limit.eq_ignore_ascii_case("None")
873 || limit.parse::<i32>().ok().is_some_and(|max| used < max);
874 return under_limit
875 && game.player(player).life > 1
876 && !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_pay_life(
877 game, player, true, None,
878 );
879 }
880 false
881}
882
883fn roll_modifications_used(card: &crate::card::Card) -> i32 {
884 card.svars
885 .get("ModsThisTurn")
886 .and_then(|value| value.strip_prefix("Number$"))
887 .and_then(|value| value.parse::<i32>().ok())
888 .unwrap_or(0)
889}
890
891fn increment_roll_modifications(game: &mut GameState, card_id: crate::ids::CardId) {
892 let used = roll_modifications_used(game.card(card_id)) + 1;
893 game.card_mut(card_id)
894 .set_s_var("ModsThisTurn", format!("Number${used}"));
895}
896
897fn can_pay_roll_cost(
898 game: &GameState,
899 mana_pools: &[crate::mana::ManaPool],
900 player: PlayerId,
901 card_id: crate::ids::CardId,
902 cost: &Cost,
903 source_sa: Option<&SpellAbility>,
904) -> bool {
905 let available = mana::calculate_available_mana(&mana_pools[player.index()], game, player);
906 crate::cost::can_pay_with_ability(cost, game, &available, card_id, player, source_sa)
907}
908
909fn pay_roll_cost(
910 game: &mut GameState,
911 trigger_handler: &mut TriggerHandler,
912 agents: &mut [Box<dyn crate::agent::PlayerAgent>],
913 mana_pools: &mut [crate::mana::ManaPool],
914 player: PlayerId,
915 card_id: crate::ids::CardId,
916 cost_raw: &str,
917 source_sa: Option<&SpellAbility>,
918) -> bool {
919 let cost = parse_cost(cost_raw);
920 if !can_pay_roll_cost(game, mana_pools, player, card_id, &cost, source_sa) {
921 return false;
922 }
923
924 for part in &cost.parts {
925 let should_ask = matches!(
926 part,
927 CostPart::DamageYou(_)
928 | CostPart::PayLife(_)
929 | CostPart::Draw(_)
930 | CostPart::Mill(_)
931 | CostPart::AddMana { .. }
932 | CostPart::Discard { .. }
933 | CostPart::Sacrifice { .. }
934 );
935 if should_ask
936 && !agents[player.index()].confirm_payment(
937 player,
938 match part {
939 CostPart::DamageYou(_) => "DamageYou",
940 CostPart::PayLife(_) => "PayLife",
941 CostPart::Draw(_) => "Draw",
942 CostPart::Mill(_) => "Mill",
943 CostPart::AddMana { .. } => "AddMana",
944 CostPart::Discard { .. } => "Discard",
945 CostPart::Sacrifice { .. } => "Sacrifice",
946 _ => "Cost",
947 },
948 "Pay roll modification cost?",
949 Some(card_id),
950 source_sa.and_then(|sa| sa.api),
951 )
952 {
953 return false;
954 }
955 }
956
957 for part in &cost.parts {
958 match part {
959 CostPart::Mana {
960 cost: mana_cost, ..
961 } => {
962 let game_ptr: *mut GameState = game;
963 let trigger_handler_ptr = std::ptr::from_mut(trigger_handler);
964 let mut callback = |kind: mana::ManaPayCallback<'_>| -> Option<crate::ids::CardId> {
965 match kind {
966 mana::ManaPayCallback::ChooseSacrifice(valid) => {
967 agents[player.index()].choose_sacrifice(player, valid, Some(card_id))
968 }
969 mana::ManaPayCallback::ChooseColor(valid_colors) => {
970 let _ = agents[player.index()].choose_color(player, valid_colors);
974 None
975 }
976 mana::ManaPayCallback::ChooseTapType { .. } => None,
977 mana::ManaPayCallback::ConfirmSelfSacrifice(sacrifice_id) => {
978 if agents[player.index()].confirm_payment(
979 player,
980 "Sacrifice",
981 "Sacrifice for mana",
982 Some(sacrifice_id),
983 Some(crate::ability::api_type::ApiType::Mana),
984 ) {
985 Some(sacrifice_id)
986 } else {
987 None
988 }
989 }
990 mana::ManaPayCallback::ConfirmSubCounter(source_id) => {
991 if agents[player.index()].confirm_payment(
992 player,
993 "SubCounter",
994 "Remove counter for mana",
995 Some(source_id),
996 Some(crate::ability::api_type::ApiType::Mana),
997 ) {
998 Some(source_id)
999 } else {
1000 None
1001 }
1002 }
1003 mana::ManaPayCallback::ConfirmSourceExile(source_id) => {
1004 if agents[player.index()].confirm_payment(
1005 player,
1006 "Exile",
1007 "Exile for mana",
1008 Some(source_id),
1009 Some(crate::ability::api_type::ApiType::Mana),
1010 ) {
1011 Some(source_id)
1012 } else {
1013 None
1014 }
1015 }
1016 mana::ManaPayCallback::ConfirmPayLife(source_id) => {
1017 if agents[player.index()].confirm_payment(
1018 player,
1019 "PayLife",
1020 "Pay life for mana",
1021 Some(source_id),
1022 Some(crate::ability::api_type::ApiType::Mana),
1023 ) {
1024 Some(source_id)
1025 } else {
1026 None
1027 }
1028 }
1029 mana::ManaPayCallback::NotifySacrificeForMana(sacrificed_id) => unsafe {
1030 let game = &mut *game_ptr;
1031 let trigger_handler = &mut *trigger_handler_ptr;
1032 let owner = game.card(sacrificed_id).owner;
1033 let controller = game.card(sacrificed_id).controller;
1034 let lki_counters = game.card(sacrificed_id).counters.clone();
1035 let lki_power = game.card(sacrificed_id).power();
1036 let lki_toughness = game.card(sacrificed_id).toughness();
1037 let lki_p1p1 = *lki_counters
1038 .get(&crate::card::CounterType::P1P1)
1039 .unwrap_or(&0);
1040 {
1041 let card = game.card_mut(sacrificed_id);
1042 card.lki_counters = Some(lki_counters);
1043 card.set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
1044 }
1045 game.last_sacrificed_card = Some(sacrificed_id);
1046 trigger_handler.run_trigger(
1047 TriggerType::Sacrificed,
1048 RunParams {
1049 card: Some(sacrificed_id),
1050 player: Some(controller),
1051 ..Default::default()
1052 },
1053 false,
1054 );
1055 crate::ability::effects::emit_zone_trigger_with_lki_counters(
1056 trigger_handler,
1057 sacrificed_id,
1058 ZoneType::Battlefield,
1059 ZoneType::Graveyard,
1060 lki_p1p1,
1061 lki_power,
1062 lki_toughness,
1063 );
1064 trigger_handler.flush_waiting_triggers(game);
1065 game.move_card(sacrificed_id, ZoneType::Graveyard, owner);
1066 let mut by_controller = std::collections::BTreeMap::new();
1067 by_controller.insert(controller, vec![sacrificed_id]);
1068 crate::game_loop::fire_sacrificed_once_for_batch(
1069 game,
1070 trigger_handler,
1071 &by_controller,
1072 );
1073 Some(sacrificed_id)
1074 },
1075 mana::ManaPayCallback::ApplyProduceManaReplacement {
1076 activator,
1077 source_card,
1078 mana,
1079 } => unsafe {
1080 let game = &mut *game_ptr;
1081 let mut event =
1082 crate::replacement::replacement_handler::ReplacementEvent::ProduceMana {
1083 source: source_card,
1084 activator,
1085 mana: mana.clone(),
1086 };
1087 let result =
1088 crate::replacement::replacement_handler::apply_replacements_with_agents(
1089 game, agents, &mut event,
1090 );
1091 if result == crate::replacement::ReplacementResult::Updated {
1092 if let crate::replacement::replacement_handler::ReplacementEvent::ProduceMana {
1093 mana: new_mana,
1094 ..
1095 } = event
1096 {
1097 *mana = new_mana;
1098 }
1099 }
1100 None
1101 },
1102 }
1103 };
1104 let tapped = mana::auto_tap_lands_with_callbacks(
1105 game,
1106 &mut mana_pools[player.index()],
1107 player,
1108 mana_cost,
1109 Some(card_id),
1110 &mut callback,
1111 );
1112 for &land_id in &tapped {
1113 trigger_handler.run_trigger(
1114 TriggerType::Taps,
1115 RunParams {
1116 card: Some(land_id),
1117 player: Some(player),
1118 ..Default::default()
1119 },
1120 false,
1121 );
1122 trigger_handler.run_trigger(
1123 TriggerType::TapsForMana,
1124 RunParams {
1125 card: Some(land_id),
1126 player: Some(player),
1127 ..Default::default()
1128 },
1129 false,
1130 );
1131 }
1132 if !mana_pools[player.index()].try_pay(mana_cost) {
1133 return false;
1134 }
1135 }
1136 CostPart::PayLife(amount) => {
1137 game.player_lose_life(player, amount.resolve(game, card_id, player));
1138 trigger_handler.run_trigger(
1139 TriggerType::LifeLost,
1140 RunParams {
1141 player: Some(player),
1142 life_amount: Some(amount.resolve(game, card_id, player)),
1143 first_time: Some(false),
1144 ..Default::default()
1145 },
1146 false,
1147 );
1148 }
1149 CostPart::SubCounter {
1150 amount,
1151 counter_type,
1152 ..
1153 } => {
1154 let amount_n = amount.resolve(game, card_id, player);
1155 if game.card(card_id).counter_count(counter_type) < amount_n {
1156 return false;
1157 }
1158 game.card_mut(card_id)
1159 .remove_counter(counter_type, amount_n);
1160 }
1161 _ => return false,
1162 }
1163 }
1164
1165 true
1166}
1167
1168fn apply_keyword_roll_rerolls(
1169 ctx: &mut EffectContext,
1170 sa: &SpellAbility,
1171 player: PlayerId,
1172 sides: i32,
1173 natural_rolls: &mut Vec<i32>,
1174 ignored_rolls: &mut Vec<i32>,
1175 dice_pt_exchanges: &mut HashSet<crate::ids::CardId>,
1176) {
1177 const MONITOR_KEYWORD: &str =
1178 "Once each turn, you may pay {1} to reroll one or more dice you rolled.";
1179
1180 loop {
1181 let reroll_cards = get_reroll_cards(ctx.game, player, MONITOR_KEYWORD);
1182 if reroll_cards.is_empty() || natural_rolls.is_empty() {
1183 break;
1184 }
1185
1186 let dice_to_reroll = ctx.agents[player.index()].choose_dice_to_reroll(
1187 player,
1188 natural_rolls,
1189 Some(sa.source.unwrap_or(reroll_cards[0])),
1190 );
1191 if dice_to_reroll.is_empty() {
1192 break;
1193 }
1194
1195 let chosen = ctx.agents[player.index()].choose_single_card_for_zone_change(
1196 ctx.game,
1197 player,
1198 &reroll_cards,
1199 "Choose a card to reroll dice",
1200 true,
1201 );
1202 let Some(card_id) = chosen else { break };
1203 let reroll_cost = ctx
1204 .game
1205 .card(card_id)
1206 .svars
1207 .get("RollRerollCost")
1208 .cloned()
1209 .unwrap_or_default();
1210 if !pay_roll_cost(
1211 ctx.game,
1212 ctx.trigger_handler,
1213 ctx.agents,
1214 ctx.mana_pools,
1215 player,
1216 card_id,
1217 &reroll_cost,
1218 Some(sa),
1219 ) {
1220 continue;
1221 }
1222
1223 for reroll in &dice_to_reroll {
1224 if let Some(position) = natural_rolls.iter().position(|roll| roll == reroll) {
1225 natural_rolls.remove(position);
1226 }
1227 }
1228
1229 let _reroll_card_name = ctx.game.card(card_id).card_name.clone();
1230 let rerolled = roll_action(
1231 ctx.game,
1232 ctx.rng,
1233 ctx.agents,
1234 player,
1235 sides,
1236 dice_to_reroll.len() as i32,
1237 0,
1238 ignored_rolls,
1239 dice_pt_exchanges,
1240 sa.source,
1241 );
1242 natural_rolls.extend(rerolled);
1243 natural_rolls.sort();
1244 increment_roll_modifications(ctx.game, card_id);
1245 }
1246}
1247
1248pub fn get_reroll_cards(
1251 game: &GameState,
1252 player: PlayerId,
1253 monitor_keyword: &str,
1254) -> Vec<crate::ids::CardId> {
1255 game.cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
1256 .iter()
1257 .copied()
1258 .filter(|&card_id| {
1259 let card = game.card(card_id);
1260 if !card.has_keyword(monitor_keyword) {
1261 return false;
1262 }
1263 let limit = card
1264 .svars
1265 .get("RollModificationsLimit")
1266 .map(String::as_str)
1267 .unwrap_or("None");
1268 let used = roll_modifications_used(card);
1269 limit.eq_ignore_ascii_case("None")
1270 || limit.parse::<i32>().ok().is_some_and(|max| used < max)
1271 })
1272 .collect()
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277 use super::*;
1278 use crate::agent::PlayerAgent;
1279 use crate::card::Card;
1280 use crate::game_rng::GameRng;
1281 use crate::ids::{CardId, PlayerId};
1282 use crate::mana::ManaPool;
1283 use crate::spellability::SpellAbility;
1284 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
1285 use std::collections::VecDeque;
1286
1287 struct FixedRng {
1288 values: VecDeque<i32>,
1289 }
1290
1291 impl FixedRng {
1292 fn new(values: &[i32]) -> Self {
1293 Self {
1294 values: values.iter().copied().collect(),
1295 }
1296 }
1297 }
1298
1299 impl GameRng for FixedRng {
1300 fn shuffle_cards(&mut self, _cards: &mut [CardId]) {}
1301
1302 fn next_int(&mut self, bound: i32) -> i32 {
1303 let value = self.values.pop_front().unwrap_or(0);
1304 assert!(
1305 value >= 0 && value < bound,
1306 "fixed rng value {value} out of range for {bound}"
1307 );
1308 value
1309 }
1310 }
1311
1312 struct SwapAgent;
1313
1314 impl PlayerAgent for SwapAgent {
1315 fn mulligan_decision(
1316 &mut self,
1317 _player: PlayerId,
1318 _hand: &[CardId],
1319 _mulligan_count: u32,
1320 ) -> bool {
1321 true
1322 }
1323
1324 fn choose_action(
1325 &mut self,
1326 player: PlayerId,
1327 action_space: Option<&crate::agent::PriorityActionSpace>,
1328 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
1329 ) -> crate::player::actions::PlayerAction {
1330 crate::player::actions::PlayerAction::PassPriority
1331 }
1332
1333 fn choose_attackers(
1334 &mut self,
1335 _player: PlayerId,
1336 _available: &[CardId],
1337 _possible_defenders: &[crate::combat::DefenderId],
1338 ) -> Vec<(CardId, crate::combat::DefenderId)> {
1339 vec![]
1340 }
1341
1342 fn choose_blockers(
1343 &mut self,
1344 _player: PlayerId,
1345 _attackers: &[CardId],
1346 _available_blockers: &[CardId],
1347 _max_blockers: Option<usize>,
1348 ) -> Vec<(CardId, CardId)> {
1349 vec![]
1350 }
1351
1352 fn choose_target_player(
1353 &mut self,
1354 _player: PlayerId,
1355 _valid: &[PlayerId],
1356 _sa: Option<&crate::spellability::SpellAbility>,
1357 ) -> Option<PlayerId> {
1358 None
1359 }
1360
1361 fn choose_target_card(
1362 &mut self,
1363 _player: PlayerId,
1364 _valid: &[CardId],
1365 _sa: Option<&crate::spellability::SpellAbility>,
1366 ) -> Option<CardId> {
1367 None
1368 }
1369
1370 fn choose_target_any(
1371 &mut self,
1372 _player: PlayerId,
1373 _valid_players: &[PlayerId],
1374 _valid_cards: &[CardId],
1375 _sa: Option<&crate::spellability::SpellAbility>,
1376 ) -> crate::agent::TargetChoice {
1377 crate::agent::TargetChoice::None
1378 }
1379
1380 fn choose_roll_to_swap(
1381 &mut self,
1382 _player: PlayerId,
1383 rolls: &[i32],
1384 _source: Option<crate::ids::CardId>,
1385 ) -> Option<i32> {
1386 rolls.first().copied()
1387 }
1388
1389 fn choose_roll_swap_value(
1390 &mut self,
1391 _player: PlayerId,
1392 _current_result: i32,
1393 _power: i32,
1394 _toughness: i32,
1395 _source: Option<crate::ids::CardId>,
1396 ) -> Option<crate::agent::RollSwapChoice> {
1397 Some(crate::agent::RollSwapChoice::Power)
1398 }
1399
1400 fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1401 Some(true)
1402 }
1403
1404 fn choose_targets_for(
1405 &mut self,
1406 _sa: &mut SpellAbility,
1407 _game: &GameState,
1408 _mana_pools: &[ManaPool],
1409 ) -> bool {
1410 false
1411 }
1412 }
1413
1414 struct ModifyAgent;
1415
1416 impl PlayerAgent for ModifyAgent {
1417 fn mulligan_decision(
1418 &mut self,
1419 _player: PlayerId,
1420 _hand: &[CardId],
1421 _mulligan_count: u32,
1422 ) -> bool {
1423 true
1424 }
1425
1426 fn choose_action(
1427 &mut self,
1428 player: PlayerId,
1429 action_space: Option<&crate::agent::PriorityActionSpace>,
1430 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
1431 ) -> crate::player::actions::PlayerAction {
1432 crate::player::actions::PlayerAction::PassPriority
1433 }
1434
1435 fn choose_attackers(
1436 &mut self,
1437 _player: PlayerId,
1438 _available: &[CardId],
1439 _possible_defenders: &[crate::combat::DefenderId],
1440 ) -> Vec<(CardId, crate::combat::DefenderId)> {
1441 vec![]
1442 }
1443
1444 fn choose_blockers(
1445 &mut self,
1446 _player: PlayerId,
1447 _attackers: &[CardId],
1448 _available_blockers: &[CardId],
1449 _max_blockers: Option<usize>,
1450 ) -> Vec<(CardId, CardId)> {
1451 vec![]
1452 }
1453
1454 fn choose_target_player(
1455 &mut self,
1456 _player: PlayerId,
1457 _valid: &[PlayerId],
1458 _sa: Option<&crate::spellability::SpellAbility>,
1459 ) -> Option<PlayerId> {
1460 None
1461 }
1462
1463 fn choose_target_card(
1464 &mut self,
1465 _player: PlayerId,
1466 _valid: &[CardId],
1467 _sa: Option<&crate::spellability::SpellAbility>,
1468 ) -> Option<CardId> {
1469 None
1470 }
1471
1472 fn choose_target_any(
1473 &mut self,
1474 _player: PlayerId,
1475 _valid_players: &[PlayerId],
1476 _valid_cards: &[CardId],
1477 _sa: Option<&crate::spellability::SpellAbility>,
1478 ) -> crate::agent::TargetChoice {
1479 crate::agent::TargetChoice::None
1480 }
1481
1482 fn choose_roll_to_modify(
1483 &mut self,
1484 _player: PlayerId,
1485 rolls: &[i32],
1486 _source: Option<crate::ids::CardId>,
1487 ) -> Option<i32> {
1488 rolls.first().copied()
1489 }
1490
1491 fn choose_single_card_for_zone_change(
1492 &mut self,
1493 _game: &crate::game::GameState,
1494 _player: PlayerId,
1495 valid: &[CardId],
1496 _select_prompt: &str,
1497 _is_optional: bool,
1498 ) -> Option<CardId> {
1499 valid.first().copied()
1500 }
1501
1502 fn choose_binary(
1503 &mut self,
1504 _player: PlayerId,
1505 _question: &str,
1506 _kind: crate::agent::BinaryChoiceKind,
1507 _default: Option<bool>,
1508 _source: Option<crate::ids::CardId>,
1509 _api: Option<crate::ability::api_type::ApiType>,
1510 ) -> bool {
1511 true
1512 }
1513
1514 fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1515 Some(true)
1516 }
1517
1518 fn choose_targets_for(
1519 &mut self,
1520 _sa: &mut SpellAbility,
1521 _game: &GameState,
1522 _mana_pools: &[ManaPool],
1523 ) -> bool {
1524 false
1525 }
1526 }
1527
1528 struct RerollAgent;
1529
1530 impl PlayerAgent for RerollAgent {
1531 fn mulligan_decision(
1532 &mut self,
1533 _player: PlayerId,
1534 _hand: &[CardId],
1535 _mulligan_count: u32,
1536 ) -> bool {
1537 true
1538 }
1539
1540 fn choose_action(
1541 &mut self,
1542 player: PlayerId,
1543 action_space: Option<&crate::agent::PriorityActionSpace>,
1544 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
1545 ) -> crate::player::actions::PlayerAction {
1546 crate::player::actions::PlayerAction::PassPriority
1547 }
1548
1549 fn choose_attackers(
1550 &mut self,
1551 _player: PlayerId,
1552 _available: &[CardId],
1553 _possible_defenders: &[crate::combat::DefenderId],
1554 ) -> Vec<(CardId, crate::combat::DefenderId)> {
1555 vec![]
1556 }
1557
1558 fn choose_blockers(
1559 &mut self,
1560 _player: PlayerId,
1561 _attackers: &[CardId],
1562 _available_blockers: &[CardId],
1563 _max_blockers: Option<usize>,
1564 ) -> Vec<(CardId, CardId)> {
1565 vec![]
1566 }
1567
1568 fn choose_target_player(
1569 &mut self,
1570 _player: PlayerId,
1571 _valid: &[PlayerId],
1572 _sa: Option<&crate::spellability::SpellAbility>,
1573 ) -> Option<PlayerId> {
1574 None
1575 }
1576
1577 fn choose_target_card(
1578 &mut self,
1579 _player: PlayerId,
1580 _valid: &[CardId],
1581 _sa: Option<&crate::spellability::SpellAbility>,
1582 ) -> Option<CardId> {
1583 None
1584 }
1585
1586 fn choose_target_any(
1587 &mut self,
1588 _player: PlayerId,
1589 _valid_players: &[PlayerId],
1590 _valid_cards: &[CardId],
1591 _sa: Option<&crate::spellability::SpellAbility>,
1592 ) -> crate::agent::TargetChoice {
1593 crate::agent::TargetChoice::None
1594 }
1595
1596 fn choose_dice_to_reroll(
1597 &mut self,
1598 _player: PlayerId,
1599 rolls: &[i32],
1600 _source: Option<crate::ids::CardId>,
1601 ) -> Vec<i32> {
1602 rolls.first().copied().into_iter().collect()
1603 }
1604
1605 fn choose_single_card_for_zone_change(
1606 &mut self,
1607 _game: &crate::game::GameState,
1608 _player: PlayerId,
1609 valid: &[CardId],
1610 _select_prompt: &str,
1611 _is_optional: bool,
1612 ) -> Option<CardId> {
1613 valid.first().copied()
1614 }
1615
1616 fn choose_land_or_spell(&mut self, _player: PlayerId) -> Option<bool> {
1617 Some(true)
1618 }
1619
1620 fn choose_targets_for(
1621 &mut self,
1622 _sa: &mut SpellAbility,
1623 _game: &GameState,
1624 _mana_pools: &[ManaPool],
1625 ) -> bool {
1626 false
1627 }
1628 }
1629
1630 #[test]
1631 fn dice_pt_exchange_swaps_roll_with_power() {
1632 let mut game = GameState::new(&["Alice", "Bob"], 20);
1633 let player = PlayerId(0);
1634 let mut card = Card::new(
1635 CardId(0),
1636 "Vedalken Squirrel-Whacker".to_string(),
1637 player,
1638 CardTypeLine::parse("Creature Vedalken"),
1639 ManaCost::parse("3 U"),
1640 ColorSet::BLUE,
1641 Some(2),
1642 Some(5),
1643 vec![],
1644 vec![],
1645 );
1646 card.zone = forge_foundation::ZoneType::Battlefield;
1647 let card_id = game.create_card(card);
1648 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, card_id);
1649
1650 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(SwapAgent), Box::new(SwapAgent)];
1651 let mut rolls = vec![
1652 DieRollResult {
1653 natural_value: 4,
1654 modified_value: 4,
1655 },
1656 DieRollResult {
1657 natural_value: 6,
1658 modified_value: 6,
1659 },
1660 ];
1661 let mut swaps = HashSet::new();
1662 swaps.insert(card_id);
1663
1664 apply_dice_pt_exchanges(&mut agents, &mut game, player, &mut rolls, &swaps);
1665
1666 assert_eq!(
1667 rolls
1668 .iter()
1669 .map(|roll| roll.modified_value)
1670 .collect::<Vec<_>>(),
1671 vec![2, 6]
1672 );
1673 assert_eq!(game.card(card_id).base_power, Some(4));
1674 assert_eq!(game.card(card_id).base_toughness, Some(5));
1675 }
1676
1677 #[test]
1678 fn xenosquirrels_can_increase_roll_by_removing_counter() {
1679 let mut game = GameState::new(&["Alice", "Bob"], 20);
1680 let player = PlayerId(0);
1681 let mut card = Card::new(
1682 CardId(0),
1683 "Xenosquirrels".to_string(),
1684 player,
1685 CardTypeLine::parse("Creature Alien Squirrel"),
1686 ManaCost::parse("1 B"),
1687 ColorSet::BLACK,
1688 Some(0),
1689 Some(0),
1690 vec!["After you roll a die, you may remove a +1/+1 counter from Xenosquirrels. If you do, increase or decrease the result by 1.".to_string()],
1691 vec![],
1692 );
1693 card.set_s_var("RollModifyCost", "SubCounter<1/P1P1>");
1694 card.set_s_var("RollModificationsLimit", "None");
1695 card.set_s_var("ModsThisTurn", "Number$0");
1696 card.add_counter(&crate::card::CounterType::P1P1, 2);
1697 card.zone = forge_foundation::ZoneType::Battlefield;
1698 let card_id = game.create_card(card);
1699 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, card_id);
1700
1701 let mut agents: Vec<Box<dyn PlayerAgent>> =
1702 vec![Box::new(ModifyAgent), Box::new(ModifyAgent)];
1703 let mut trigger_handler = TriggerHandler::new();
1704 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
1705 let mut rolls = vec![4];
1706
1707 let results = apply_simple_roll_modifiers(
1708 &mut game,
1709 &mut trigger_handler,
1710 &mut agents,
1711 &mut mana_pools,
1712 player,
1713 &mut rolls,
1714 None,
1715 );
1716
1717 assert_eq!(
1718 results
1719 .iter()
1720 .map(|roll| roll.modified_value)
1721 .collect::<Vec<_>>(),
1722 vec![5]
1723 );
1724 assert_eq!(
1725 game.card(card_id)
1726 .counter_count(&crate::card::CounterType::P1P1),
1727 1
1728 );
1729 assert_eq!(
1730 game.card(card_id).svars.get("ModsThisTurn"),
1731 Some(&"Number$1".to_string())
1732 );
1733 }
1734
1735 #[test]
1736 fn night_shift_can_increase_roll_by_paying_life() {
1737 let mut game = GameState::new(&["Alice", "Bob"], 20);
1738 let player = PlayerId(0);
1739 let mut card = Card::new(
1740 CardId(0),
1741 "Night Shift of the Living Dead".to_string(),
1742 player,
1743 CardTypeLine::parse("Creature Zombie Employee"),
1744 ManaCost::parse("2 B"),
1745 ColorSet::BLACK,
1746 Some(2),
1747 Some(3),
1748 vec!["After you roll a die, you may pay 1 life. If you do, increase or decrease the result by 1. Do this only once each turn.".to_string()],
1749 vec![],
1750 );
1751 card.set_s_var("RollModifyCost", "PayLife<1>");
1752 card.set_s_var("RollModificationsLimit", "1");
1753 card.set_s_var("ModsThisTurn", "Number$0");
1754 card.zone = forge_foundation::ZoneType::Battlefield;
1755 let card_id = game.create_card(card);
1756 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, card_id);
1757
1758 let mut agents: Vec<Box<dyn PlayerAgent>> =
1759 vec![Box::new(ModifyAgent), Box::new(ModifyAgent)];
1760 let mut trigger_handler = TriggerHandler::new();
1761 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
1762 let mut rolls = vec![3];
1763
1764 let results = apply_simple_roll_modifiers(
1765 &mut game,
1766 &mut trigger_handler,
1767 &mut agents,
1768 &mut mana_pools,
1769 player,
1770 &mut rolls,
1771 None,
1772 );
1773
1774 assert_eq!(
1775 results
1776 .iter()
1777 .map(|roll| roll.modified_value)
1778 .collect::<Vec<_>>(),
1779 vec![4]
1780 );
1781 assert_eq!(game.player(player).life, 19);
1782 assert_eq!(
1783 game.card(card_id).svars.get("ModsThisTurn"),
1784 Some(&"Number$1".to_string())
1785 );
1786 }
1787
1788 #[test]
1789 fn monitor_monitor_can_pay_to_reroll() {
1790 let mut game = GameState::new(&["Alice", "Bob"], 20);
1791 let player = PlayerId(0);
1792
1793 let mut source = Card::new(
1794 CardId(0),
1795 "Dice Source".to_string(),
1796 player,
1797 CardTypeLine::parse("Artifact"),
1798 ManaCost::parse(""),
1799 ColorSet::COLORLESS,
1800 None,
1801 None,
1802 vec![],
1803 vec![],
1804 );
1805 source.zone = forge_foundation::ZoneType::Battlefield;
1806 let source_id = game.create_card(source);
1807 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, source_id);
1808
1809 let mut monitor = Card::new(
1810 CardId(0),
1811 "Monitor Monitor".to_string(),
1812 player,
1813 CardTypeLine::parse("Creature Human Employee"),
1814 ManaCost::parse("2 U U"),
1815 ColorSet::BLUE,
1816 Some(2),
1817 Some(5),
1818 vec![
1819 "Once each turn, you may pay {1} to reroll one or more dice you rolled."
1820 .to_string(),
1821 ],
1822 vec![],
1823 );
1824 monitor.set_s_var("RollModificationsLimit", "1");
1825 monitor.set_s_var("ModsThisTurn", "Number$0");
1826 monitor.set_s_var("RollRerollCost", "1");
1827 monitor.zone = forge_foundation::ZoneType::Battlefield;
1828 let monitor_id = game.create_card(monitor);
1829 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, monitor_id);
1830
1831 let mut plains = Card::new(
1832 CardId(0),
1833 "Plains".to_string(),
1834 player,
1835 CardTypeLine::parse("Basic Land Plains"),
1836 ManaCost::parse(""),
1837 ColorSet::COLORLESS,
1838 None,
1839 None,
1840 vec![],
1841 vec![],
1842 );
1843 plains.zone = forge_foundation::ZoneType::Battlefield;
1844 let plains_id = game.create_card(plains);
1845 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, plains_id);
1846
1847 let sa = SpellAbility::new_empty(Some(source_id), player);
1848 let mut trigger_handler = TriggerHandler::new();
1849 let mut agents: Vec<Box<dyn PlayerAgent>> =
1850 vec![Box::new(RerollAgent), Box::new(RerollAgent)];
1851 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
1852 let mut rng = FixedRng::new(&[0, 5]);
1853 let token_templates = HashMap::new();
1854 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
1855 let token_fallback: HashMap<String, String> = HashMap::new();
1856 let edition_dates: HashMap<String, String> = HashMap::new();
1857 let mut ctx = EffectContext {
1858 game: &mut game,
1859 combat: None,
1860 agents: &mut agents,
1861 trigger_handler: &mut trigger_handler,
1862 token_templates: &token_templates,
1863 token_art_variants: &templates_variants,
1864 token_fallback: &token_fallback,
1865 edition_dates: &edition_dates,
1866 mana_pools: &mut mana_pools,
1867 parent_target_card: None,
1868 rng: &mut rng,
1869 };
1870
1871 let result = roll_for_player(&mut ctx, &sa, source_id, player, 6, 1);
1872
1873 assert_eq!(result, 6);
1874 assert_eq!(
1875 ctx.game.card(monitor_id).svars.get("ModsThisTurn"),
1876 Some(&"Number$1".to_string())
1877 );
1878 assert!(ctx.game.card(plains_id).tapped);
1879 assert_eq!(ctx.game.player(player).num_rolls_this_turn, 2);
1880 }
1881
1882 #[test]
1883 fn ignore_lower_preserves_natural_result_for_trigger() {
1884 let mut game = GameState::new(&["Alice", "Bob"], 20);
1885 let player = PlayerId(0);
1886 let mut source = Card::new(
1887 CardId(0),
1888 "Dice Source".to_string(),
1889 player,
1890 CardTypeLine::parse("Artifact"),
1891 ManaCost::parse(""),
1892 ColorSet::COLORLESS,
1893 None,
1894 None,
1895 vec![],
1896 vec![],
1897 );
1898 source.zone = forge_foundation::ZoneType::Battlefield;
1899 let source_id = game.create_card(source);
1900 game.add_card_to_zone(forge_foundation::ZoneType::Battlefield, player, source_id);
1901
1902 let sa = SpellAbility::new_simple(
1903 Some(source_id),
1904 player,
1905 "DB$ Internal | IgnoreLower$ 1 | Modifier$ 2",
1906 );
1907 let mut trigger_handler = TriggerHandler::new();
1908 let mut agents: Vec<Box<dyn PlayerAgent>> =
1909 vec![Box::new(ModifyAgent), Box::new(ModifyAgent)];
1910 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
1911 let mut rng = FixedRng::new(&[0, 4]);
1912 let token_templates = HashMap::new();
1913 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
1914 let token_fallback: HashMap<String, String> = HashMap::new();
1915 let edition_dates: HashMap<String, String> = HashMap::new();
1916 let mut ctx = EffectContext {
1917 game: &mut game,
1918 combat: None,
1919 agents: &mut agents,
1920 trigger_handler: &mut trigger_handler,
1921 token_templates: &token_templates,
1922 token_art_variants: &templates_variants,
1923 token_fallback: &token_fallback,
1924 edition_dates: &edition_dates,
1925 mana_pools: &mut mana_pools,
1926 parent_target_card: None,
1927 rng: &mut rng,
1928 };
1929
1930 let result = roll_for_player(&mut ctx, &sa, source_id, player, 6, 2);
1931
1932 assert_eq!(result, 7);
1933 assert_eq!(ctx.game.player(player).num_rolls_this_turn, 2);
1934 }
1935}
1936
1937pub fn visit_attractions(
1938 game: &mut GameState,
1939 trigger_handler: &mut TriggerHandler,
1940 player: PlayerId,
1941 light: i32,
1942) {
1943 if light <= 0 {
1944 return;
1945 }
1946 let attractions: Vec<_> = game
1947 .cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
1948 .iter()
1949 .copied()
1950 .filter(|&card_id| {
1951 let card = game.card(card_id);
1952 card.type_line
1953 .subtypes
1954 .iter()
1955 .any(|s| s.eq_ignore_ascii_case("Attraction"))
1956 && card.has_attraction_light(light)
1957 })
1958 .collect();
1959
1960 for card_id in attractions {
1961 let first_visit = !game.card(card_id).was_visited_this_turn();
1962 game.card_mut(card_id).visit_attraction();
1963 if first_visit {
1964 game.player_record_attraction_visit(player, 1);
1965 }
1966 trigger_handler.run_trigger(
1967 TriggerType::VisitAttraction,
1968 RunParams {
1969 card: Some(card_id),
1970 player: Some(player),
1971 ..Default::default()
1972 },
1973 false,
1974 );
1975 }
1976}
1977
1978fn resolve_result_sub_ability(
1979 ctx: &mut EffectContext,
1980 _sa: &SpellAbility,
1981 source_id: crate::ids::CardId,
1982 player: PlayerId,
1983 result: i32,
1984 result_str: &str,
1985) {
1986 for entry in result_str.split(',') {
1987 let parts: Vec<&str> = entry.splitn(2, ':').collect();
1988 if parts.len() != 2 {
1989 continue;
1990 }
1991 let selector = parts[0].trim();
1992 let svar_name = parts[1].trim();
1993 let matches = if selector.eq_ignore_ascii_case("Else") {
1994 true
1995 } else if let Some((start, end)) = selector.split_once('-') {
1996 let low = start.trim().parse::<i32>().ok();
1997 let high = end.trim().parse::<i32>().ok();
1998 matches!(low.zip(high), Some((low, high)) if result >= low && result <= high)
1999 } else {
2000 selector
2001 .parse::<i32>()
2002 .ok()
2003 .map(|value| result == value)
2004 .unwrap_or(false)
2005 };
2006 if matches {
2007 if let Some(sub_text) = ctx
2008 .game
2009 .card(source_id)
2010 .get_s_var(svar_name)
2011 .map(str::to_string)
2012 {
2013 let mut sub_sa = build_spell_ability(ctx.game, source_id, &sub_text, player);
2014 sub_sa.activating_player = player;
2015 resolve_sub_chain(ctx, sub_sa);
2016 }
2017 break;
2018 }
2019 }
2020}
2021
2022fn resolve_sub_chain(ctx: &mut EffectContext, initial: SpellAbility) {
2023 let mut cur_opt: Option<SpellAbility> = Some(initial);
2024 while let Some(cur_sa) = cur_opt {
2025 super::resolve_effect(ctx, &cur_sa);
2026 cur_opt = cur_sa.sub_ability.map(|b| *b);
2027 if ctx.game.game_over {
2028 break;
2029 }
2030 }
2031}