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