1use forge_foundation::{ManaAtom, ZoneType};
2use manabrew_engine::agent::notification::GameNotification;
3use manabrew_engine::agent::{
4 BinaryChoiceKind, CombatCostAction, GameEntity, ManaCostAction, PlayOption, PlayerAgent,
5 PriorityActionSpace, RollSwapChoice, TargetChoice,
6};
7use manabrew_engine::card::CounterType;
8use manabrew_engine::combat::DefenderId;
9use manabrew_engine::game::GameState;
10use manabrew_engine::ids::{CardId, PlayerId};
11use manabrew_engine::mana::ManaPool;
12use manabrew_engine::player::actions::player_action::AbilityRef;
13use manabrew_engine::player::actions::PlayerAction as EnginePlayerAction;
14
15use crate::game_log_event::GameLogEntryDto;
16use crate::game_snapshot_event::GameSnapshotEventDto;
17use crate::game_view_dto::{GameViewDto, GameViewDtoExt};
18use crate::ids_codec::{card_id_str, parse_card_id, parse_player_id, player_id_str};
19use crate::mana_action_id::{mana_ability_actions, parse_tap_action_id};
20use crate::prompt::*;
21
22mod choices;
23mod combat;
24mod costs;
25mod library;
26mod targeting;
27
28pub(crate) fn find_matching_color<'a>(
32 pending: &str,
33 colors: impl Iterator<Item = &'a String>,
34) -> Option<String> {
35 let mana_to_name: &[(&str, &str)] = &[
36 ("W", "White"),
37 ("U", "Blue"),
38 ("B", "Black"),
39 ("R", "Red"),
40 ("G", "Green"),
41 ("C", "Colorless"),
42 ];
43 colors
44 .into_iter()
45 .find(|c| {
46 c.eq_ignore_ascii_case(pending)
48 || mana_to_name.iter().any(|(sym, name)| {
50 pending.eq_ignore_ascii_case(sym) && c.eq_ignore_ascii_case(name)
51 })
52 || mana_to_name.iter().any(|(sym, name)| {
54 pending.eq_ignore_ascii_case(name) && c.eq_ignore_ascii_case(sym)
55 })
56 })
57 .cloned()
58}
59
60pub(crate) fn parse_express_mana_choice(color: Option<&str>) -> Option<u16> {
61 color
62 .map(|color| ManaAtom::from_name(&color.to_ascii_lowercase()))
63 .filter(|&atom| atom != 0)
64}
65
66pub trait Responder {
69 fn respond(&mut self, prompt: AgentPrompt) -> ClientToServerMessage;
70 fn present(&mut self, _message: &AgentMessage) {}
71 fn await_ack(&mut self) -> ClientToServerMessage {
72 ClientToServerMessage::Response {
73 action: PromptOutput::DiceRolled(DiceRolledOutput::DiceRolledAcknowledged),
74 }
75 }
76 fn send_log(&mut self, _entry: GameLogEntryDto) {}
77 fn send_snapshot(&mut self, _snapshot: GameSnapshotEventDto) {}
78}
79
80pub struct PromptAgent<R: Responder> {
81 pub player_id: PlayerId,
82 pub game_id: String,
83 pub responder: R,
84 pending_prompt: Option<AgentPrompt>,
85 pub(crate) latest_view: Option<GameViewDto>,
86 pub(crate) pending_restore_checkpoint: Option<u64>,
87 pub pass_until: Option<manabrew_engine::agent::PassUntilTarget>,
88 conceded: bool,
89 next_prompt_id: u32,
90}
91
92impl<R: Responder> PromptAgent<R> {
93 pub fn new(player_id: PlayerId, game_id: String, responder: R) -> Self {
94 Self {
95 player_id,
96 game_id,
97 responder,
98 pending_prompt: None,
99 latest_view: None,
100 pending_restore_checkpoint: None,
101 pass_until: None,
102 conceded: false,
103 next_prompt_id: 0,
104 }
105 }
106
107 fn build_prompt(&mut self, inner: PromptInput, source: Option<CardId>) -> AgentPrompt {
108 self.next_prompt_id += 1;
109 AgentPrompt {
110 prompt_id: self.next_prompt_id,
111 deciding_player_id: player_id_str(self.player_id),
112 source_card_id: source.map(card_id_str),
113 input: inner,
114 }
115 }
116
117 pub(crate) fn send_prompt(&mut self, inner: PromptInput, source: Option<CardId>) {
118 let prompt = self.build_prompt(inner, source);
119 self.emit_state();
120 self.responder
121 .present(&AgentMessage::Prompt(prompt.clone()));
122 self.pending_prompt = Some(prompt);
123 }
124
125 pub(crate) fn recv_action(&mut self) -> PromptOutput {
126 let prompt = self
127 .pending_prompt
128 .take()
129 .expect("recv_action called without a pending prompt");
130 if self.conceded {
131 return PromptOutput::ChooseAction(ChooseActionOutput::Pass { until: None });
132 }
133 match self.responder.respond(prompt) {
134 ClientToServerMessage::Response { action } => action,
135 ClientToServerMessage::Directive { directive } => {
136 self.handle_directive(directive);
137 PromptOutput::ChooseAction(ChooseActionOutput::Pass { until: None })
138 }
139 }
140 }
141
142 fn handle_directive(&mut self, directive: DirectiveInput) {
143 match directive {
144 DirectiveInput::Concede => self.conceded = true,
145 }
146 }
147
148 pub(crate) fn present_prompt(&mut self, inner: PromptInput, source: Option<CardId>) {
149 let prompt = self.build_prompt(inner, source);
150 self.emit_state();
151 self.responder.present(&AgentMessage::Prompt(prompt));
152 }
153
154 pub(crate) fn emit_state(&mut self) {
155 let game_view = self.view();
156 self.responder
157 .present(&AgentMessage::State(StateUpdate { game_view }));
158 }
159
160 pub(crate) fn emit_display(&mut self, event: DisplayEvent) {
161 self.responder.present(&AgentMessage::Display(event));
162 }
163
164 pub(crate) fn view(&self) -> GameViewDto {
165 self.latest_view.clone().unwrap_or_else(|| {
166 GameViewDto::empty(self.game_id.clone())
168 })
169 }
170
171 pub(crate) fn card_ids(cards: &[CardId]) -> Vec<String> {
172 cards.iter().map(|&c| card_id_str(c)).collect()
173 }
174
175 pub(crate) fn player_ids(players: &[PlayerId]) -> Vec<String> {
176 players.iter().map(|&p| player_id_str(p)).collect()
177 }
178
179 pub(crate) fn attack_targets_to_dtos(
180 defenders: &[DefenderId],
181 ) -> Vec<crate::prompt::AttackTargetDto> {
182 use crate::prompt::AttackTargetKind;
183 defenders
184 .iter()
185 .map(|d| match d {
186 DefenderId::Player(pid) => crate::prompt::AttackTargetDto {
187 id: format!("player-{}", pid.0),
188 label: format!("Player {}", pid.0),
189 kind: AttackTargetKind::Player,
190 },
191 DefenderId::Permanent(cid) => crate::prompt::AttackTargetDto {
194 id: format!("card-{}", cid.0),
195 label: format!("Permanent {}", cid.0),
196 kind: AttackTargetKind::Planeswalker,
197 },
198 })
199 .collect()
200 }
201
202 fn play_option_to_dto(play: &PlayOption) -> PlayOptionDto {
203 use manabrew_engine::agent::PlayCardMode;
204 let card_id = card_id_str(play.card_id);
205 let (mode, mode_label) = match &play.mode {
206 PlayCardMode::Normal => ("normal".to_string(), "Cast normally".to_string()),
207 PlayCardMode::BackFaceLand => (
208 "backFaceLand".to_string(),
209 "Play back face as land".to_string(),
210 ),
211 PlayCardMode::Alternative(alt) => {
212 let name = format!("{:?}", alt);
213 (
214 format!("alternative:{}", name.to_lowercase()),
215 format!("Cast with {}", name),
216 )
217 }
218 PlayCardMode::StaticAlternative => (
219 "staticAlternative".to_string(),
220 "Cast with alternative cost".to_string(),
221 ),
222 PlayCardMode::ForetellExile => (
223 "foretellExile".to_string(),
224 "Foretell (exile face-down)".to_string(),
225 ),
226 PlayCardMode::UnlockDoor => ("unlockDoor".to_string(), "Unlock door".to_string()),
227 PlayCardMode::RoomRightSplit => {
228 ("roomRightSplit".to_string(), "Cast right room".to_string())
229 }
230 };
231 PlayOptionDto {
232 card_id,
233 mode,
234 mode_label,
235 }
236 }
237
238 fn parse_play_mode(mode_str: &str) -> Option<manabrew_engine::agent::PlayCardMode> {
239 use manabrew_engine::agent::PlayCardMode;
240 use manabrew_engine::spellability::AlternativeCost;
241 match mode_str {
242 "normal" => Some(PlayCardMode::Normal),
243 "backFaceLand" => Some(PlayCardMode::BackFaceLand),
244 "staticAlternative" => Some(PlayCardMode::StaticAlternative),
245 "foretellExile" => Some(PlayCardMode::ForetellExile),
246 "unlockDoor" => Some(PlayCardMode::UnlockDoor),
247 "roomRightSplit" => Some(PlayCardMode::RoomRightSplit),
248 s if s.starts_with("alternative:") => {
249 let alt_name = &s["alternative:".len()..];
250 let alt = match alt_name {
251 "flashback" => AlternativeCost::Flashback,
252 "evoke" => AlternativeCost::Evoke,
253 "dash" => AlternativeCost::Dash,
254 "escape" => AlternativeCost::Escape,
255 "bestow" => AlternativeCost::Bestow,
256 "madness" => AlternativeCost::Madness,
257 "overload" => AlternativeCost::Overload,
258 "spectacle" => AlternativeCost::Spectacle,
259 "emerge" => AlternativeCost::Emerge,
260 "blitz" => AlternativeCost::Blitz,
261 "foretell" => AlternativeCost::Foretell,
262 "suspend" => AlternativeCost::Suspend,
263 _ => return None,
264 };
265 Some(PlayCardMode::Alternative(alt))
266 }
267 _ => None,
268 }
269 }
270
271 pub(crate) fn parse_defender_id(id: &str, possible: &[DefenderId]) -> Option<DefenderId> {
272 if let Some(rest) = id.strip_prefix("player-") {
273 let idx: u32 = rest.parse().ok()?;
274 possible
275 .iter()
276 .find(|d| matches!(d, DefenderId::Player(p) if p.0 == idx))
277 .copied()
278 } else if let Some(rest) = id.strip_prefix("card-") {
279 let idx: u32 = rest.parse().ok()?;
280 possible
281 .iter()
282 .find(|d| matches!(d, DefenderId::Permanent(c) if c.0 == idx))
283 .copied()
284 } else {
285 None
286 }
287 }
288
289 pub(crate) fn recv_card_choice_or_first(&mut self, valid: &[CardId]) -> Option<CardId> {
290 match self.recv_action() {
291 PromptOutput::ChooseBoardTargets(ChooseBoardTargetsOutput::BoardTargets { chosen }) => {
292 chosen.into_iter().find_map(|r| match r.kind {
293 TargetKind::Card => parse_card_id(&r.id),
294 _ => None,
295 })
296 }
297 _ => valid.first().copied(),
298 }
299 }
300
301 pub(crate) fn recv_player_choice_or_first(&mut self, valid: &[PlayerId]) -> Option<PlayerId> {
302 match self.recv_action() {
303 PromptOutput::ChooseBoardTargets(ChooseBoardTargetsOutput::BoardTargets { chosen }) => {
304 chosen.into_iter().find_map(|r| match r.kind {
305 TargetKind::Player => parse_player_id(&r.id),
306 _ => None,
307 })
308 }
309 _ => valid.first().copied(),
310 }
311 }
312
313 pub(crate) fn recv_spell_choice_or_first(&mut self, valid: &[u32]) -> Option<u32> {
314 match self.recv_action() {
315 PromptOutput::ChooseBoardTargets(ChooseBoardTargetsOutput::BoardTargets { chosen }) => {
316 chosen.into_iter().find_map(|r| match r.kind {
317 TargetKind::Spell => crate::ids_codec::parse_stack_id(&r.id),
318 _ => None,
319 })
320 }
321 _ => valid.first().copied(),
322 }
323 }
324}
325
326impl<R: Responder> PlayerAgent for PromptAgent<R> {
327 fn choose_targets_for(
328 &mut self,
329 sa: &mut manabrew_engine::spellability::SpellAbility,
330 game: &GameState,
331 mana_pools: &[ManaPool],
332 ) -> bool {
333 manabrew_engine::spellability::choose_targets_by_kind(self, sa, game, mana_pools)
334 }
335
336 fn get_pass_until(&self) -> Option<manabrew_engine::agent::PassUntilTarget> {
337 self.pass_until
338 }
339
340 fn clear_pass_until(&mut self) {
341 self.pass_until = None;
342 }
343
344 fn snapshot_state(&mut self, game: &GameState, mana_pools: &[ManaPool]) {
345 self.latest_view = Some(GameViewDto::from_engine(
346 game,
347 mana_pools,
348 self.player_id,
349 &self.game_id,
350 ));
351 }
352
353 fn mulligan_decision(
354 &mut self,
355 player: PlayerId,
356 hand: &[CardId],
357 mulligan_count: u32,
358 ) -> bool {
359 choices::mulligan_decision(self, player, hand, mulligan_count)
360 }
361
362 fn mulligan_decision_send(&mut self, player: PlayerId, hand: &[CardId], mulligan_count: u32) {
363 choices::mulligan_decision_send(self, player, hand, mulligan_count);
364 }
365
366 fn mulligan_decision_recv(
367 &mut self,
368 player: PlayerId,
369 hand: &[CardId],
370 mulligan_count: u32,
371 ) -> bool {
372 choices::mulligan_decision_recv(self, player, hand, mulligan_count)
373 }
374
375 fn choose_cards_to_bottom(
376 &mut self,
377 player: PlayerId,
378 hand: &[CardId],
379 count: usize,
380 ) -> Vec<CardId> {
381 choices::choose_cards_to_bottom(self, player, hand, count)
382 }
383
384 fn choose_cards_to_bottom_send(&mut self, player: PlayerId, hand: &[CardId], count: usize) {
385 choices::choose_cards_to_bottom_send(self, player, hand, count);
386 }
387
388 fn choose_cards_to_bottom_recv(
389 &mut self,
390 player: PlayerId,
391 hand: &[CardId],
392 count: usize,
393 ) -> Vec<CardId> {
394 choices::choose_cards_to_bottom_recv(self, player, hand, count)
395 }
396
397 fn choose_action(
398 &mut self,
399 _player: PlayerId,
400 action_space: Option<&PriorityActionSpace>,
401 request_action_space: &mut dyn FnMut() -> PriorityActionSpace,
402 ) -> EnginePlayerAction {
403 if self.conceded {
404 return EnginePlayerAction::Concede;
405 }
406 let requested_action_space;
407 let action_space = match action_space {
408 Some(action_space) => action_space,
409 None => {
410 requested_action_space = request_action_space();
411 &requested_action_space
412 }
413 };
414 let playable = &action_space.playable;
415 let untappable_lands = &action_space.untappable_lands;
416 let _activatable = &action_space.activatable;
417 let playable_options: Vec<PlayOptionDto> = playable
418 .iter()
419 .map(|play| Self::play_option_to_dto(play))
420 .collect();
421 let untappable_land_ids: Vec<String> =
422 untappable_lands.iter().map(|&c| card_id_str(c)).collect();
423
424 let mut actions: Vec<AvailableAction> = Vec::new();
425 for (play, opt) in playable.iter().zip(playable_options.iter()) {
426 let card_id = card_id_str(play.card_id);
427 actions.push(AvailableAction {
428 id: format!("cast:{card_id}:{}", opt.mode),
429 kind: AvailableActionKind::Cast {
430 card_id: card_id.clone(),
431 mode: opt.mode.clone(),
432 mode_label: opt.mode_label.clone(),
433 },
434 });
435 }
436 for a in action_space
437 .activatable
438 .iter()
439 .chain(action_space.mana_abilities.iter())
440 {
441 let card_id = card_id_str(a.card_id);
442 if a.is_mana_ability {
443 actions.extend(mana_ability_actions(
444 &card_id,
445 a.ability_index,
446 &a.description,
447 a.cost.clone(),
448 a.produced_mana.clone(),
449 a.produced_mana_amount,
450 ));
451 } else {
452 actions.push(AvailableAction {
453 id: format!("ability:{card_id}:{}", a.ability_index),
454 kind: AvailableActionKind::ActivateAbility(ActivatableAbilityInfo {
455 card_id,
456 ability_index: a.ability_index,
457 description: a.description.clone(),
458 cost: a.cost.clone(),
459 is_mana_ability: false,
460 produced_mana: None,
461 }),
462 });
463 }
464 }
465 for card_id in &untappable_land_ids {
466 actions.push(AvailableAction {
467 id: format!("untap:{card_id}"),
468 kind: AvailableActionKind::UndoMana {
469 card_id: card_id.clone(),
470 },
471 });
472 }
473
474 self.send_prompt(
475 PromptInput::ChooseAction(
476 manabrew_protocol::prompts::choose_action::ChooseActionInput { actions },
477 ),
478 None,
479 );
480 let prompt = self
481 .pending_prompt
482 .take()
483 .expect("choose_action called without a pending prompt");
484 let action = match self.responder.respond(prompt) {
485 ClientToServerMessage::Response { action } => action,
486 ClientToServerMessage::Directive {
487 directive: DirectiveInput::Concede,
488 } => return EnginePlayerAction::Concede,
489 };
490 match action {
491 PromptOutput::ChooseAction(ChooseActionOutput::Act { action_id }) => {
492 if let Some(rest) = action_id.strip_prefix("cast:") {
493 let (id_part, mode) = rest.split_once(':').unwrap_or((rest, "normal"));
494 let resolved = parse_card_id(id_part).and_then(|cid| {
495 Self::parse_play_mode(mode)
496 .and_then(|m| {
497 playable
498 .iter()
499 .copied()
500 .find(|play| play.card_id == cid && play.mode == m)
501 })
502 .or_else(|| playable.iter().copied().find(|play| play.card_id == cid))
503 });
504 resolved
505 .map(EnginePlayerAction::CastSpell)
506 .unwrap_or(EnginePlayerAction::PassPriority)
507 } else if let Some(rest) = action_id.strip_prefix("tap:") {
508 let tap = parse_tap_action_id(rest);
509 match parse_card_id(tap.card_id) {
510 Some(cid) => EnginePlayerAction::ActivateMana(
511 cid,
512 tap.ability_index,
513 parse_express_mana_choice(tap.color),
514 ),
515 None => EnginePlayerAction::PassPriority,
516 }
517 } else if let Some(rest) = action_id.strip_prefix("ability:") {
518 let (id_part, idx) = rest.split_once(':').unwrap_or((rest, ""));
519 match (parse_card_id(id_part), idx.parse::<usize>()) {
520 (Some(cid), Ok(ability_index)) => {
521 EnginePlayerAction::ActivateAbility(AbilityRef {
522 card_id: cid,
523 ability_index,
524 })
525 }
526 _ => EnginePlayerAction::PassPriority,
527 }
528 } else if let Some(id_part) = action_id.strip_prefix("untap:") {
529 parse_card_id(id_part)
530 .map(EnginePlayerAction::UndoMana)
531 .unwrap_or(EnginePlayerAction::PassPriority)
532 } else {
533 EnginePlayerAction::PassPriority
534 }
535 }
536 PromptOutput::ChooseAction(ChooseActionOutput::Pass { until }) => {
537 self.pass_until = until.and_then(|u| {
538 Some(manabrew_engine::agent::PassUntilTarget {
539 player: crate::ids_codec::parse_player_id(&u.player_id)?,
540 phase: forge_foundation::PhaseType::from_step_string(&u.phase)?,
541 })
542 });
543 EnginePlayerAction::PassPriority
544 }
545 PromptOutput::ChooseAction(ChooseActionOutput::RestoreSnapshot { checkpoint_id }) => {
546 self.pending_restore_checkpoint = Some(checkpoint_id);
547 EnginePlayerAction::PassPriority
548 }
549 _ => EnginePlayerAction::PassPriority,
550 }
551 }
552
553 fn choose_attackers(
554 &mut self,
555 player: PlayerId,
556 available: &[CardId],
557 possible_defenders: &[DefenderId],
558 ) -> Vec<(CardId, DefenderId)> {
559 combat::choose_attackers(self, player, available, possible_defenders)
560 }
561
562 fn choose_blockers(
563 &mut self,
564 player: PlayerId,
565 attackers: &[CardId],
566 available_blockers: &[CardId],
567 max_blockers: Option<usize>,
568 ) -> Vec<(CardId, CardId)> {
569 combat::choose_blockers(self, player, attackers, available_blockers, max_blockers)
570 }
571
572 fn choose_damage_assignment_order(
573 &mut self,
574 player: PlayerId,
575 attacker: CardId,
576 blockers: &[CardId],
577 ) -> Vec<CardId> {
578 combat::choose_damage_assignment_order(self, player, attacker, blockers)
579 }
580
581 fn assign_combat_damage(
582 &mut self,
583 game: &GameState,
584 player: PlayerId,
585 attacker: CardId,
586 blockers_in_order: &[CardId],
587 defender_id: Option<DefenderId>,
588 damage_to_assign: i32,
589 ) -> Vec<(Option<CardId>, i32)> {
590 let attacker_has_deathtouch = game.card(attacker).has_deathtouch();
591 combat::choose_combat_damage_assignment(
592 self,
593 player,
594 attacker,
595 blockers_in_order,
596 defender_id,
597 damage_to_assign,
598 attacker_has_deathtouch,
599 )
600 }
601
602 fn choose_target_player(
603 &mut self,
604 player: PlayerId,
605 valid: &[PlayerId],
606 sa: Option<&manabrew_engine::spellability::SpellAbility>,
607 ) -> Option<PlayerId> {
608 let source = sa.and_then(|s| s.source);
609 let intent = sa
610 .map(crate::game_view_dto::targeting_intent_of)
611 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
612 let hostile = intent.is_hostile();
613 targeting::choose_target_player(self, player, valid, source, hostile, intent)
614 }
615
616 fn choose_target_card(
617 &mut self,
618 player: PlayerId,
619 valid: &[CardId],
620 sa: Option<&manabrew_engine::spellability::SpellAbility>,
621 ) -> Option<CardId> {
622 let source = sa.and_then(|s| s.source);
623 let intent = sa
624 .map(crate::game_view_dto::targeting_intent_of)
625 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
626 let hostile = intent.is_hostile();
627 targeting::choose_target_card(self, player, valid, source, hostile, intent)
628 }
629
630 fn choose_target_card_from_zone(
631 &mut self,
632 player: PlayerId,
633 zone: ZoneType,
634 valid: &[CardId],
635 sa: Option<&manabrew_engine::spellability::SpellAbility>,
636 ) -> Option<CardId> {
637 let source = sa.and_then(|s| s.source);
638 let intent = sa
639 .map(crate::game_view_dto::targeting_intent_of)
640 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
641 let hostile = intent.is_hostile();
642 targeting::choose_target_card_from_zone(self, player, zone, valid, source, hostile, intent)
643 }
644
645 fn choose_target_any(
646 &mut self,
647 player: PlayerId,
648 valid_players: &[PlayerId],
649 valid_cards: &[CardId],
650 sa: Option<&manabrew_engine::spellability::SpellAbility>,
651 ) -> TargetChoice {
652 let source = sa.and_then(|s| s.source);
653 let intent = sa
654 .map(crate::game_view_dto::targeting_intent_of)
655 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
656 let hostile = intent.is_hostile();
657 targeting::choose_target_any(
658 self,
659 player,
660 valid_players,
661 valid_cards,
662 source,
663 hostile,
664 intent,
665 )
666 }
667
668 fn choose_sacrifice(
669 &mut self,
670 player: PlayerId,
671 valid: &[CardId],
672 source: Option<CardId>,
673 ) -> Option<CardId> {
674 targeting::choose_sacrifice(self, player, valid, source)
675 }
676
677 fn reveal_cards(
678 &mut self,
679 game: &GameState,
680 _player: PlayerId,
681 cards: &[CardId],
682 zone: ZoneType,
683 owner: PlayerId,
684 message_prefix: Option<&str>,
685 ) {
686 choices::reveal_cards(self, game, cards, zone, owner, message_prefix)
687 }
688
689 fn choose_scry(
690 &mut self,
691 game: &GameState,
692 player: PlayerId,
693 source: Option<CardId>,
694 cards: &[CardId],
695 ) -> Vec<Vec<CardId>> {
696 library::choose_scry(self, game, player, source, cards)
697 }
698
699 fn choose_surveil(
700 &mut self,
701 game: &GameState,
702 player: PlayerId,
703 source: Option<CardId>,
704 cards: &[CardId],
705 ) -> Vec<Vec<CardId>> {
706 library::choose_surveil(self, game, player, source, cards)
707 }
708
709 fn choose_dig(
710 &mut self,
711 game: &GameState,
712 player: PlayerId,
713 valid: &[CardId],
714 max: usize,
715 optional: bool,
716 ) -> Vec<CardId> {
717 library::choose_dig(self, game, player, valid, max, optional)
718 }
719
720 fn choose_discard(&mut self, player: PlayerId, hand: &[CardId], num: usize) -> Vec<CardId> {
721 choices::choose_discard(self, player, hand, num)
722 }
723
724 fn choose_discard_any_number(
725 &mut self,
726 player: PlayerId,
727 hand: &[CardId],
728 min: usize,
729 max: usize,
730 ) -> Vec<CardId> {
731 choices::choose_discard_any_number(self, player, hand, min, max)
732 }
733
734 fn choose_legend_keep(&mut self, player: PlayerId, duplicates: &[CardId]) -> CardId {
735 choices::choose_legend_keep(self, player, duplicates)
736 }
737
738 fn choose_target_spell(
739 &mut self,
740 player: PlayerId,
741 valid: &[u32],
742 source: Option<CardId>,
743 ) -> Option<u32> {
744 targeting::choose_target_spell(self, player, valid, source)
745 }
746
747 fn choose_mode(
748 &mut self,
749 player: PlayerId,
750 descriptions: &[String],
751 min: usize,
752 max: usize,
753 source_card_id: Option<CardId>,
754 ) -> Vec<usize> {
755 choices::choose_mode(self, player, descriptions, min, max, source_card_id)
756 }
757
758 fn choose_spell_abilities_for_effect(
759 &mut self,
760 player: PlayerId,
761 abilities: &[manabrew_engine::spellability::SpellAbility],
762 num: usize,
763 ) -> Vec<usize> {
764 choices::choose_spell_abilities_for_effect(self, player, abilities, num)
765 }
766
767 fn get_ability_to_play(
768 &mut self,
769 player: PlayerId,
770 abilities: &[manabrew_engine::spellability::SpellAbility],
771 ) -> Option<usize> {
772 choices::get_ability_to_play(self, player, abilities)
773 }
774
775 fn choose_single_entity_for_effect(
776 &mut self,
777 player: PlayerId,
778 valid: &[GameEntity],
779 is_optional: bool,
780 ) -> Option<GameEntity> {
781 choices::choose_single_entity_for_effect(self, player, valid, is_optional)
782 }
783
784 fn choose_entities_for_effect(
785 &mut self,
786 player: PlayerId,
787 candidates: &[GameEntity],
788 min: usize,
789 max: usize,
790 ) -> Vec<GameEntity> {
791 choices::choose_entities_for_effect(self, player, candidates, min, max)
792 }
793
794 fn choose_single_replacement_effect(
795 &mut self,
796 player: PlayerId,
797 descriptions: &[String],
798 ) -> usize {
799 choices::choose_single_replacement_effect(self, player, descriptions)
800 }
801
802 fn confirm_replacement_effect(
803 &mut self,
804 player: PlayerId,
805 question: &str,
806 effect_description: &str,
807 source: Option<CardId>,
808 ) -> bool {
809 choices::confirm_replacement_effect(self, player, question, effect_description, source)
810 }
811
812 fn choose_optional_trigger(
813 &mut self,
814 player: PlayerId,
815 description: &str,
816 source: Option<CardId>,
817 api: Option<manabrew_engine::ability::api_type::ApiType>,
818 ) -> bool {
819 choices::choose_optional_trigger(self, player, description, source, api)
820 }
821
822 fn confirm_action(
823 &mut self,
824 player: PlayerId,
825 mode: Option<&str>,
826 message: &str,
827 options: &[String],
828 source: Option<CardId>,
829 api: Option<manabrew_engine::ability::api_type::ApiType>,
830 ) -> bool {
831 choices::confirm_action(self, player, mode, message, options, source, api)
832 }
833
834 fn confirm_payment(
835 &mut self,
836 player: PlayerId,
837 cost_kind: &str,
838 message: &str,
839 source: Option<CardId>,
840 api: Option<manabrew_engine::ability::api_type::ApiType>,
841 ) -> bool {
842 choices::confirm_payment(self, player, cost_kind, message, source, api)
843 }
844
845 fn pay_cost_to_prevent_effect(
846 &mut self,
847 player: PlayerId,
848 cost_kind: &str,
849 message: &str,
850 source: Option<CardId>,
851 api: Option<manabrew_engine::ability::api_type::ApiType>,
852 can_pay: bool,
853 targets: &[manabrew_engine::agent::GameEntity],
854 effect_text: &str,
855 ) -> bool {
856 choices::pay_cost_to_prevent_effect(
857 self,
858 player,
859 cost_kind,
860 message,
861 source,
862 api,
863 can_pay,
864 targets,
865 effect_text,
866 )
867 }
868
869 fn choose_binary(
870 &mut self,
871 player: PlayerId,
872 question: &str,
873 kind: BinaryChoiceKind,
874 default_choice: Option<bool>,
875 source: Option<CardId>,
876 api: Option<manabrew_engine::ability::api_type::ApiType>,
877 ) -> bool {
878 choices::choose_binary(self, player, question, kind, default_choice, source, api)
879 }
880
881 fn choose_phyrexian_pay_life(
882 &mut self,
883 player: PlayerId,
884 color: &str,
885 source: Option<CardId>,
886 ) -> bool {
887 costs::choose_phyrexian_pay_life(self, player, color, source)
888 }
889
890 fn choose_kicker(
891 &mut self,
892 player: PlayerId,
893 kicker_cost: &str,
894 source: Option<CardId>,
895 ) -> bool {
896 costs::choose_kicker(self, player, kicker_cost, source)
897 }
898
899 fn choose_buyback(
900 &mut self,
901 player: PlayerId,
902 buyback_cost: &str,
903 source: Option<CardId>,
904 ) -> bool {
905 costs::choose_buyback(self, player, buyback_cost, source)
906 }
907
908 fn choose_multikicker(
909 &mut self,
910 player: PlayerId,
911 cost: &str,
912 max_kicks: u32,
913 source: Option<CardId>,
914 ) -> u32 {
915 costs::choose_multikicker(self, player, cost, max_kicks, source)
916 }
917
918 fn choose_replicate(
919 &mut self,
920 player: PlayerId,
921 cost: &str,
922 max_replicates: u32,
923 source: Option<CardId>,
924 ) -> u32 {
925 costs::choose_replicate(self, player, cost, max_replicates, source)
926 }
927
928 fn choose_color(&mut self, player: PlayerId, valid_colors: &[String]) -> Option<String> {
929 choices::choose_color(self, player, valid_colors)
930 }
931
932 fn choose_colors(
933 &mut self,
934 player: PlayerId,
935 valid_colors: &[String],
936 min: usize,
937 max: usize,
938 ) -> Vec<String> {
939 choices::choose_colors(self, player, valid_colors, min, max)
940 }
941
942 fn choose_cards_for_effect(
943 &mut self,
944 player: PlayerId,
945 valid: &[CardId],
946 min: usize,
947 max: usize,
948 ) -> Vec<CardId> {
949 choices::choose_cards_for_effect(self, player, valid, min, max)
950 }
951
952 fn choose_single_card_for_zone_change(
953 &mut self,
954 game: &GameState,
955 player: PlayerId,
956 valid: &[CardId],
957 select_prompt: &str,
958 is_optional: bool,
959 ) -> Option<CardId> {
960 choices::choose_single_card_for_zone_change(
961 self,
962 game,
963 player,
964 valid,
965 select_prompt,
966 is_optional,
967 )
968 }
969
970 fn choose_cards_for_zone_change(
971 &mut self,
972 game: &GameState,
973 player: PlayerId,
974 valid: &[CardId],
975 min: usize,
976 max: usize,
977 select_prompt: &str,
978 ) -> Vec<CardId> {
979 choices::choose_cards_for_zone_change(self, game, player, valid, min, max, select_prompt)
980 }
981
982 fn choose_type(
983 &mut self,
984 player: PlayerId,
985 type_category: &str,
986 valid_types: &[String],
987 ) -> Option<String> {
988 choices::choose_type(self, player, type_category, valid_types)
989 }
990
991 fn choose_counter_type(
992 &mut self,
993 player: PlayerId,
994 options: &[CounterType],
995 prompt: &str,
996 ) -> Option<CounterType> {
997 choices::choose_counter_type(self, player, options, prompt)
998 }
999
1000 fn choose_card_name(&mut self, player: PlayerId, valid_names: &[String]) -> Option<String> {
1001 choices::choose_card_name(self, player, valid_names)
1002 }
1003
1004 fn choose_number(
1005 &mut self,
1006 player: PlayerId,
1007 source: Option<CardId>,
1008 title: &str,
1009 description: Option<&str>,
1010 min: i32,
1011 max: i32,
1012 ) -> Option<i32> {
1013 choices::choose_number(self, player, source, title, description, min, max)
1014 }
1015
1016 fn choose_number_from_list(
1017 &mut self,
1018 player: PlayerId,
1019 choices: &[i32],
1020 message: &str,
1021 source_card_id: Option<CardId>,
1022 ) -> Option<i32> {
1023 choices::choose_number_from_list(self, player, choices, message, source_card_id)
1024 }
1025
1026 fn choose_roll_to_ignore(
1027 &mut self,
1028 player: PlayerId,
1029 rolls: &[i32],
1030 source: Option<CardId>,
1031 ) -> Option<i32> {
1032 choices::choose_roll_to_ignore(self, player, rolls, source)
1033 }
1034
1035 fn choose_roll_to_swap(
1036 &mut self,
1037 player: PlayerId,
1038 rolls: &[i32],
1039 source: Option<CardId>,
1040 ) -> Option<i32> {
1041 choices::choose_roll_to_swap(self, player, rolls, source)
1042 }
1043
1044 fn choose_dice_to_reroll(
1045 &mut self,
1046 player: PlayerId,
1047 rolls: &[i32],
1048 source: Option<CardId>,
1049 ) -> Vec<i32> {
1050 choices::choose_dice_to_reroll(self, player, rolls, source)
1051 }
1052
1053 fn choose_roll_to_modify(
1054 &mut self,
1055 player: PlayerId,
1056 rolls: &[i32],
1057 source: Option<CardId>,
1058 ) -> Option<i32> {
1059 choices::choose_roll_to_modify(self, player, rolls, source)
1060 }
1061
1062 fn choose_roll_swap_value(
1063 &mut self,
1064 player: PlayerId,
1065 current_result: i32,
1066 power: i32,
1067 toughness: i32,
1068 source: Option<CardId>,
1069 ) -> Option<RollSwapChoice> {
1070 choices::choose_roll_swap_value(self, player, current_result, power, toughness, source)
1071 }
1072
1073 fn flip_coin_call(&mut self, player: PlayerId) -> bool {
1074 choices::flip_coin_call(self, player)
1075 }
1076
1077 fn pay_combat_cost(
1078 &mut self,
1079 player: PlayerId,
1080 attacker: CardId,
1081 cost: i32,
1082 description: &str,
1083 mana_ability_options: &[manabrew_engine::agent::ManaAbilityOption],
1084 tappable_lands: &[CardId],
1085 untappable_lands: &[CardId],
1086 mana_pool_total: i32,
1087 ) -> CombatCostAction {
1088 combat::pay_combat_cost(
1089 self,
1090 player,
1091 attacker,
1092 cost,
1093 description,
1094 mana_ability_options,
1095 tappable_lands,
1096 untappable_lands,
1097 mana_pool_total,
1098 )
1099 }
1100
1101 fn choose_improvise(
1102 &mut self,
1103 player: PlayerId,
1104 untapped_artifacts: &[CardId],
1105 remaining_cost: &forge_foundation::ManaCost,
1106 source: Option<CardId>,
1107 ) -> Vec<CardId> {
1108 costs::choose_improvise(self, player, untapped_artifacts, remaining_cost, source)
1109 }
1110
1111 fn choose_convoke(
1112 &mut self,
1113 player: PlayerId,
1114 untapped_creatures: &[CardId],
1115 remaining_cost: &forge_foundation::ManaCost,
1116 source: Option<CardId>,
1117 ) -> Vec<CardId> {
1118 costs::choose_convoke(self, player, untapped_creatures, remaining_cost, source)
1119 }
1120
1121 fn pay_mana_cost(
1122 &mut self,
1123 player: PlayerId,
1124 card_id: CardId,
1125 card_name: &str,
1126 mana_cost: &str,
1127 mana_cost_display: &str,
1128 _mana_cost_checkpoint: &str,
1129 can_confirm_from_pool: bool,
1130 _allow_reserved_source_reuse: bool,
1131 _reserved_sacrifices: &[CardId],
1132 mana_ability_options: &[manabrew_engine::agent::ManaAbilityOption],
1133 tappable_lands: &[CardId],
1134 untappable_lands: &[CardId],
1135 mana_pool: &ManaPool,
1136 ) -> ManaCostAction {
1137 costs::pay_mana_cost(
1138 self,
1139 player,
1140 card_id,
1141 card_name,
1142 mana_cost,
1143 mana_cost_display,
1144 can_confirm_from_pool,
1145 mana_ability_options,
1146 tappable_lands,
1147 untappable_lands,
1148 mana_pool,
1149 )
1150 }
1151
1152 fn await_display_ack(&mut self) {
1153 if self.conceded {
1154 return;
1155 }
1156 if let ClientToServerMessage::Directive { directive } = self.responder.await_ack() {
1157 self.handle_directive(directive);
1158 }
1159 }
1160
1161 fn specify_mana_combo(
1162 &mut self,
1163 player: PlayerId,
1164 available_colors: &[String],
1165 amount: usize,
1166 source: Option<CardId>,
1167 express_choice: Option<u16>,
1168 ) -> Vec<String> {
1169 costs::specify_mana_combo(
1170 self,
1171 player,
1172 available_colors,
1173 amount,
1174 source,
1175 express_choice,
1176 )
1177 }
1178
1179 fn exert_attackers(&mut self, player: PlayerId, attackers: &[CardId]) -> Vec<CardId> {
1180 combat::exert_attackers(self, player, attackers)
1181 }
1182
1183 fn enlist_attackers(&mut self, player: PlayerId, attackers: &[CardId]) -> Vec<CardId> {
1184 combat::enlist_attackers(self, player, attackers)
1185 }
1186
1187 fn choose_reorder_library(
1188 &mut self,
1189 game: &GameState,
1190 player: PlayerId,
1191 cards: &[CardId],
1192 ) -> Vec<CardId> {
1193 library::choose_reorder_library(self, game, player, cards)
1194 }
1195
1196 fn help_pay_assist(&mut self, player: PlayerId, card_name: &str, max_generic: u32) -> u32 {
1197 choices::help_pay_assist(self, player, card_name, max_generic)
1198 }
1199
1200 fn choose_random_discard(
1201 &mut self,
1202 player: PlayerId,
1203 hand: &[CardId],
1204 num: usize,
1205 ) -> Vec<CardId> {
1206 choices::choose_random_discard(self, player, hand, num)
1207 }
1208
1209 fn choose_land_or_spell(&mut self, player: PlayerId) -> Option<bool> {
1210 choices::choose_land_or_spell(self, player)
1211 }
1212
1213 fn notify(&mut self, event: GameNotification) {
1214 match event {
1215 GameNotification::Event(log_event) => {
1216 self.responder
1217 .send_log(GameLogEntryDto::from_event(log_event));
1218 }
1219 GameNotification::CardPlayed {
1220 player,
1221 card_id,
1222 card_name,
1223 set_code,
1224 } => {
1225 self.emit_display(DisplayEvent::CardPlayed {
1226 card_id: card_id_str(card_id),
1227 card_name,
1228 set_code,
1229 player_id: player_id_str(player),
1230 });
1231 self.emit_state();
1232 }
1233 GameNotification::TurnChanged {
1234 active_player,
1235 turn_number,
1236 } => {
1237 let player_id = player_id_str(active_player);
1238 let active_player_name = self
1239 .latest_view
1240 .as_ref()
1241 .and_then(|v| v.players.iter().find(|p| p.id == player_id))
1242 .map(|p| p.name.clone())
1243 .unwrap_or_else(|| format!("Player {}", active_player.0));
1244 self.responder.send_log(GameLogEntryDto::from_event(
1245 manabrew_engine::agent::GameLogEvent::rule(format!(
1246 "TURN {} — {}",
1247 turn_number, active_player_name
1248 ))
1249 .with_player(active_player),
1250 ));
1251 self.emit_display(DisplayEvent::TurnChanged {
1252 active_player_id: player_id,
1253 active_player_name,
1254 turn_number,
1255 });
1256 self.emit_state();
1257 }
1258 GameNotification::PhaseChanged { .. } | GameNotification::StateChanged => {
1259 self.emit_state();
1260 }
1261 GameNotification::PriorityChanged { .. } => {
1262 self.emit_state();
1263 }
1264 GameNotification::FirstPlayerRoll {
1265 sides,
1266 rolls,
1267 winner,
1268 } => {
1269 let view = self.view();
1270 let winner_id = player_id_str(winner);
1271 let entries = rolls
1272 .into_iter()
1273 .map(|(pid, value)| {
1274 let id = player_id_str(pid);
1275 let name = view
1276 .players
1277 .iter()
1278 .find(|p| p.id == id)
1279 .map(|p| p.name.clone())
1280 .unwrap_or_else(|| id.clone());
1281 manabrew_protocol::prompts::dice_rolled::DiceRollEntry {
1282 label: Some(name),
1283 highlighted: id == winner_id,
1284 player_id: Some(id),
1285 natural_results: vec![value],
1286 final_results: vec![value],
1287 ignored_rolls: vec![],
1288 }
1289 })
1290 .collect();
1291 self.present_prompt(
1292 PromptInput::DiceRolled(
1293 manabrew_protocol::prompts::dice_rolled::DiceRolledInput {
1294 sides,
1295 rolls: entries,
1296 title: Some("Roll for first player".to_string()),
1297 source_card_name: None,
1298 },
1299 ),
1300 None,
1301 );
1302 }
1305 GameNotification::DiceRolled {
1306 player,
1307 sides,
1308 natural_results,
1309 final_results,
1310 ignored_rolls,
1311 source_card_name,
1312 } => {
1313 self.present_prompt(
1319 PromptInput::DiceRolled(
1320 manabrew_protocol::prompts::dice_rolled::DiceRolledInput {
1321 sides,
1322 rolls: vec![manabrew_protocol::prompts::dice_rolled::DiceRollEntry {
1323 label: None,
1324 player_id: Some(player_id_str(player)),
1325 natural_results,
1326 final_results,
1327 ignored_rolls,
1328 highlighted: false,
1329 }],
1330 title: None,
1331 source_card_name,
1332 },
1333 ),
1334 None,
1335 );
1336 }
1337 GameNotification::SnapshotCreated {
1338 checkpoint_id,
1339 label,
1340 } => {
1341 if let Some(view) = self.latest_view.clone() {
1342 self.responder.send_snapshot(GameSnapshotEventDto::new(
1343 checkpoint_id,
1344 label,
1345 view,
1346 ));
1347 }
1348 }
1349 GameNotification::GameOver => {
1350 self.emit_state();
1351 self.present_prompt(
1352 PromptInput::GameOver(manabrew_protocol::prompts::game_over::GameOverInput {}),
1353 None,
1354 );
1355 }
1356 GameNotification::ManaPaymentResolved { .. } => {}
1357 GameNotification::ActivatedAbilityPaymentFailed { .. } => {
1358 self.emit_state();
1359 }
1360 }
1361 }
1362
1363 fn take_restore_request(&mut self) -> Option<u64> {
1364 self.pending_restore_checkpoint.take()
1365 }
1366}