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