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 produced_mana: None,
585 }),
586 });
587 }
588 }
589 for card_id in &untappable_land_ids {
590 actions.push(AvailableAction {
591 id: format!("untap:{card_id}"),
592 kind: AvailableActionKind::UndoMana {
593 card_id: card_id.clone(),
594 },
595 });
596 }
597
598 self.send_prompt(
599 PromptInput::ChooseAction(
600 manabrew_protocol::prompts::choose_action::ChooseActionInput { actions },
601 ),
602 None,
603 );
604 let prompt = self
605 .pending_prompt
606 .take()
607 .expect("choose_action called without a pending prompt");
608 let action = match self.responder.respond(prompt) {
609 ClientToServerMessage::Response { action, .. } => action,
610 ClientToServerMessage::Directive {
611 directive: DirectiveInput::Concede,
612 } => return EnginePlayerAction::Concede,
613 };
614 match action {
615 PromptOutput::ChooseAction(ChooseActionOutput::Act { action_id }) => {
616 if let Some(rest) = action_id.strip_prefix("cast:") {
617 let (id_part, mode) = rest.split_once(':').unwrap_or((rest, "normal"));
618 let resolved = parse_card_id(id_part).and_then(|cid| {
619 Self::parse_play_mode(mode)
620 .and_then(|m| {
621 playable
622 .iter()
623 .copied()
624 .find(|play| play.card_id == cid && play.mode == m)
625 })
626 .or_else(|| playable.iter().copied().find(|play| play.card_id == cid))
627 });
628 resolved
629 .map(EnginePlayerAction::CastSpell)
630 .unwrap_or(EnginePlayerAction::PassPriority)
631 } else if let Some(rest) = action_id.strip_prefix("tap:") {
632 let tap = parse_tap_action_id(rest);
633 match parse_card_id(tap.card_id) {
634 Some(cid) => EnginePlayerAction::ActivateMana(
635 cid,
636 tap.ability_index,
637 parse_express_mana_choice(tap.color),
638 ),
639 None => EnginePlayerAction::PassPriority,
640 }
641 } else if let Some(rest) = action_id.strip_prefix("ability:") {
642 let (id_part, idx) = rest.split_once(':').unwrap_or((rest, ""));
643 match (parse_card_id(id_part), idx.parse::<usize>()) {
644 (Some(cid), Ok(ability_index)) => {
645 EnginePlayerAction::ActivateAbility(AbilityRef {
646 card_id: cid,
647 ability_index,
648 })
649 }
650 _ => EnginePlayerAction::PassPriority,
651 }
652 } else if let Some(id_part) = action_id.strip_prefix("untap:") {
653 parse_card_id(id_part)
654 .map(EnginePlayerAction::UndoMana)
655 .unwrap_or(EnginePlayerAction::PassPriority)
656 } else {
657 EnginePlayerAction::PassPriority
658 }
659 }
660 PromptOutput::ChooseAction(ChooseActionOutput::Pass { until, .. }) => {
661 self.pass_until = until.and_then(|u| {
662 Some(manabrew_engine::agent::PassUntilTarget {
663 player: crate::ids_codec::parse_player_id(&u.player_id)?,
664 phase: crate::game_view_dto::step_to_phase(u.phase),
665 })
666 });
667 EnginePlayerAction::PassPriority
668 }
669 PromptOutput::ChooseAction(ChooseActionOutput::RestoreSnapshot { checkpoint_id }) => {
670 self.pending_restore_checkpoint = Some(checkpoint_id);
671 EnginePlayerAction::PassPriority
672 }
673 _ => EnginePlayerAction::PassPriority,
674 }
675 }
676
677 fn choose_attackers(
678 &mut self,
679 player: PlayerId,
680 available: &[CardId],
681 possible_defenders: &[DefenderId],
682 ) -> Vec<(CardId, DefenderId)> {
683 combat::choose_attackers(self, player, available, possible_defenders)
684 }
685
686 fn choose_blockers(
687 &mut self,
688 player: PlayerId,
689 attackers: &[CardId],
690 available_blockers: &[CardId],
691 max_blockers: Option<usize>,
692 ) -> Vec<(CardId, CardId)> {
693 combat::choose_blockers(self, player, attackers, available_blockers, max_blockers)
694 }
695
696 fn choose_damage_assignment_order(
697 &mut self,
698 player: PlayerId,
699 attacker: CardId,
700 blockers: &[CardId],
701 ) -> Vec<CardId> {
702 combat::choose_damage_assignment_order(self, player, attacker, blockers)
703 }
704
705 fn assign_combat_damage(
706 &mut self,
707 game: &GameState,
708 player: PlayerId,
709 attacker: CardId,
710 blockers_in_order: &[CardId],
711 defender_id: Option<DefenderId>,
712 damage_to_assign: i32,
713 ) -> Vec<(Option<CardId>, i32)> {
714 let attacker_has_deathtouch = game.card(attacker).has_deathtouch();
715 combat::choose_combat_damage_assignment(
716 self,
717 player,
718 attacker,
719 blockers_in_order,
720 defender_id,
721 damage_to_assign,
722 attacker_has_deathtouch,
723 )
724 }
725
726 fn choose_target_player(
727 &mut self,
728 player: PlayerId,
729 valid: &[PlayerId],
730 sa: Option<&manabrew_engine::spellability::SpellAbility>,
731 ) -> Option<PlayerId> {
732 let source = sa.and_then(|s| s.source);
733 let intent = sa
734 .map(crate::game_view_dto::targeting_intent_of)
735 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
736 let hostile = crate::game_view_dto::intent_is_hostile(intent);
737 targeting::choose_target_player(self, player, valid, source, hostile, intent)
738 }
739
740 fn choose_target_card(
741 &mut self,
742 player: PlayerId,
743 valid: &[CardId],
744 sa: Option<&manabrew_engine::spellability::SpellAbility>,
745 ) -> Option<CardId> {
746 let source = sa.and_then(|s| s.source);
747 let intent = sa
748 .map(crate::game_view_dto::targeting_intent_of)
749 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
750 let hostile = crate::game_view_dto::intent_is_hostile(intent);
751 targeting::choose_target_card(self, player, valid, source, hostile, intent)
752 }
753
754 fn choose_target_card_from_zone(
755 &mut self,
756 player: PlayerId,
757 zone: ZoneType,
758 valid: &[CardId],
759 sa: Option<&manabrew_engine::spellability::SpellAbility>,
760 ) -> Option<CardId> {
761 let source = sa.and_then(|s| s.source);
762 let intent = sa
763 .map(crate::game_view_dto::targeting_intent_of)
764 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
765 let hostile = crate::game_view_dto::intent_is_hostile(intent);
766 targeting::choose_target_card_from_zone(self, player, zone, valid, source, hostile, intent)
767 }
768
769 fn choose_target_any(
770 &mut self,
771 player: PlayerId,
772 valid_players: &[PlayerId],
773 valid_cards: &[CardId],
774 sa: Option<&manabrew_engine::spellability::SpellAbility>,
775 ) -> TargetChoice {
776 let source = sa.and_then(|s| s.source);
777 let intent = sa
778 .map(crate::game_view_dto::targeting_intent_of)
779 .unwrap_or(crate::game_view_dto::TargetingIntent::Hostile);
780 let hostile = crate::game_view_dto::intent_is_hostile(intent);
781 targeting::choose_target_any(
782 self,
783 player,
784 valid_players,
785 valid_cards,
786 source,
787 hostile,
788 intent,
789 )
790 }
791
792 fn choose_sacrifice(
793 &mut self,
794 player: PlayerId,
795 valid: &[CardId],
796 source: Option<CardId>,
797 ) -> Option<CardId> {
798 targeting::choose_sacrifice(self, player, valid, source)
799 }
800
801 fn reveal_cards(
802 &mut self,
803 game: &GameState,
804 _player: PlayerId,
805 cards: &[CardId],
806 zone: ZoneType,
807 owner: PlayerId,
808 message_prefix: Option<&str>,
809 ) {
810 choices::reveal_cards(self, game, cards, zone, owner, message_prefix)
811 }
812
813 fn choose_scry(
814 &mut self,
815 game: &GameState,
816 player: PlayerId,
817 source: Option<CardId>,
818 cards: &[CardId],
819 ) -> Vec<Vec<CardId>> {
820 library::choose_scry(self, game, player, source, cards)
821 }
822
823 fn choose_surveil(
824 &mut self,
825 game: &GameState,
826 player: PlayerId,
827 source: Option<CardId>,
828 cards: &[CardId],
829 ) -> Vec<Vec<CardId>> {
830 library::choose_surveil(self, game, player, source, cards)
831 }
832
833 fn choose_dig(
834 &mut self,
835 game: &GameState,
836 player: PlayerId,
837 valid: &[CardId],
838 max: usize,
839 optional: bool,
840 ) -> Vec<CardId> {
841 library::choose_dig(self, game, player, valid, max, optional)
842 }
843
844 fn choose_discard(&mut self, player: PlayerId, hand: &[CardId], num: usize) -> Vec<CardId> {
845 choices::choose_discard(self, player, hand, num)
846 }
847
848 fn choose_discard_any_number(
849 &mut self,
850 player: PlayerId,
851 hand: &[CardId],
852 min: usize,
853 max: usize,
854 ) -> Vec<CardId> {
855 choices::choose_discard_any_number(self, player, hand, min, max)
856 }
857
858 fn choose_legend_keep(&mut self, player: PlayerId, duplicates: &[CardId]) -> CardId {
859 choices::choose_legend_keep(self, player, duplicates)
860 }
861
862 fn choose_target_spell(
863 &mut self,
864 player: PlayerId,
865 valid: &[u32],
866 source: Option<CardId>,
867 ) -> Option<u32> {
868 targeting::choose_target_spell(self, player, valid, source)
869 }
870
871 fn choose_mode(
872 &mut self,
873 player: PlayerId,
874 descriptions: &[String],
875 min: usize,
876 max: usize,
877 source_card_id: Option<CardId>,
878 ) -> Vec<usize> {
879 choices::choose_mode(self, player, descriptions, min, max, source_card_id)
880 }
881
882 fn choose_spell_abilities_for_effect(
883 &mut self,
884 player: PlayerId,
885 abilities: &[manabrew_engine::spellability::SpellAbility],
886 num: usize,
887 ) -> Vec<usize> {
888 choices::choose_spell_abilities_for_effect(self, player, abilities, num)
889 }
890
891 fn get_ability_to_play(
892 &mut self,
893 player: PlayerId,
894 abilities: &[manabrew_engine::spellability::SpellAbility],
895 ) -> Option<usize> {
896 choices::get_ability_to_play(self, player, abilities)
897 }
898
899 fn choose_single_entity_for_effect(
900 &mut self,
901 player: PlayerId,
902 valid: &[GameEntity],
903 is_optional: bool,
904 ) -> Option<GameEntity> {
905 choices::choose_single_entity_for_effect(self, player, valid, is_optional)
906 }
907
908 fn choose_entities_for_effect(
909 &mut self,
910 player: PlayerId,
911 candidates: &[GameEntity],
912 min: usize,
913 max: usize,
914 ) -> Vec<GameEntity> {
915 choices::choose_entities_for_effect(self, player, candidates, min, max)
916 }
917
918 fn choose_single_replacement_effect(
919 &mut self,
920 player: PlayerId,
921 descriptions: &[String],
922 ) -> usize {
923 choices::choose_single_replacement_effect(self, player, descriptions)
924 }
925
926 fn confirm_replacement_effect(
927 &mut self,
928 player: PlayerId,
929 question: &str,
930 effect_description: &str,
931 source: Option<CardId>,
932 ) -> bool {
933 choices::confirm_replacement_effect(self, player, question, effect_description, source)
934 }
935
936 fn choose_optional_trigger(
937 &mut self,
938 player: PlayerId,
939 description: &str,
940 source: Option<CardId>,
941 api: Option<manabrew_engine::ability::api_type::ApiType>,
942 ) -> bool {
943 choices::choose_optional_trigger(self, player, description, source, api)
944 }
945
946 fn confirm_action(
947 &mut self,
948 player: PlayerId,
949 mode: Option<&str>,
950 message: &str,
951 options: &[String],
952 source: Option<CardId>,
953 api: Option<manabrew_engine::ability::api_type::ApiType>,
954 ) -> bool {
955 choices::confirm_action(self, player, mode, message, options, source, api)
956 }
957
958 fn confirm_payment(
959 &mut self,
960 player: PlayerId,
961 cost_kind: &str,
962 message: &str,
963 source: Option<CardId>,
964 api: Option<manabrew_engine::ability::api_type::ApiType>,
965 ) -> bool {
966 choices::confirm_payment(self, player, cost_kind, message, source, api)
967 }
968
969 fn pay_cost_to_prevent_effect(
970 &mut self,
971 player: PlayerId,
972 cost_kind: &str,
973 message: &str,
974 source: Option<CardId>,
975 api: Option<manabrew_engine::ability::api_type::ApiType>,
976 can_pay: bool,
977 targets: &[manabrew_engine::agent::GameEntity],
978 effect_text: &str,
979 ) -> bool {
980 choices::pay_cost_to_prevent_effect(
981 self,
982 player,
983 cost_kind,
984 message,
985 source,
986 api,
987 can_pay,
988 targets,
989 effect_text,
990 )
991 }
992
993 fn choose_binary(
994 &mut self,
995 player: PlayerId,
996 question: &str,
997 kind: BinaryChoiceKind,
998 default_choice: Option<bool>,
999 source: Option<CardId>,
1000 api: Option<manabrew_engine::ability::api_type::ApiType>,
1001 ) -> bool {
1002 choices::choose_binary(self, player, question, kind, default_choice, source, api)
1003 }
1004
1005 fn choose_phyrexian_pay_life(
1006 &mut self,
1007 player: PlayerId,
1008 color: &str,
1009 source: Option<CardId>,
1010 ) -> bool {
1011 costs::choose_phyrexian_pay_life(self, player, color, source)
1012 }
1013
1014 fn choose_kicker(
1015 &mut self,
1016 player: PlayerId,
1017 kicker_cost: &str,
1018 source: Option<CardId>,
1019 ) -> bool {
1020 costs::choose_kicker(self, player, kicker_cost, source)
1021 }
1022
1023 fn choose_buyback(
1024 &mut self,
1025 player: PlayerId,
1026 buyback_cost: &str,
1027 source: Option<CardId>,
1028 ) -> bool {
1029 costs::choose_buyback(self, player, buyback_cost, source)
1030 }
1031
1032 fn choose_multikicker(
1033 &mut self,
1034 player: PlayerId,
1035 cost: &str,
1036 max_kicks: u32,
1037 source: Option<CardId>,
1038 ) -> u32 {
1039 costs::choose_multikicker(self, player, cost, max_kicks, source)
1040 }
1041
1042 fn choose_replicate(
1043 &mut self,
1044 player: PlayerId,
1045 cost: &str,
1046 max_replicates: u32,
1047 source: Option<CardId>,
1048 ) -> u32 {
1049 costs::choose_replicate(self, player, cost, max_replicates, source)
1050 }
1051
1052 fn choose_color(&mut self, player: PlayerId, valid_colors: &[String]) -> Option<String> {
1053 choices::choose_color(self, player, valid_colors)
1054 }
1055
1056 fn choose_colors(
1057 &mut self,
1058 player: PlayerId,
1059 valid_colors: &[String],
1060 min: usize,
1061 max: usize,
1062 ) -> Vec<String> {
1063 choices::choose_colors(self, player, valid_colors, min, max)
1064 }
1065
1066 fn choose_cards_for_effect(
1067 &mut self,
1068 player: PlayerId,
1069 valid: &[CardId],
1070 min: usize,
1071 max: usize,
1072 ) -> Vec<CardId> {
1073 choices::choose_cards_for_effect(self, player, valid, min, max)
1074 }
1075
1076 fn choose_single_card_for_zone_change(
1077 &mut self,
1078 game: &GameState,
1079 player: PlayerId,
1080 valid: &[CardId],
1081 select_prompt: &str,
1082 is_optional: bool,
1083 ) -> Option<CardId> {
1084 choices::choose_single_card_for_zone_change(
1085 self,
1086 game,
1087 player,
1088 valid,
1089 select_prompt,
1090 is_optional,
1091 )
1092 }
1093
1094 fn choose_cards_for_zone_change(
1095 &mut self,
1096 game: &GameState,
1097 player: PlayerId,
1098 valid: &[CardId],
1099 min: usize,
1100 max: usize,
1101 select_prompt: &str,
1102 ) -> Vec<CardId> {
1103 choices::choose_cards_for_zone_change(self, game, player, valid, min, max, select_prompt)
1104 }
1105
1106 fn choose_type(
1107 &mut self,
1108 player: PlayerId,
1109 type_category: &str,
1110 valid_types: &[String],
1111 ) -> Option<String> {
1112 choices::choose_type(self, player, type_category, valid_types)
1113 }
1114
1115 fn choose_counter_type(
1116 &mut self,
1117 player: PlayerId,
1118 options: &[CounterType],
1119 prompt: &str,
1120 ) -> Option<CounterType> {
1121 choices::choose_counter_type(self, player, options, prompt)
1122 }
1123
1124 fn choose_card_name(&mut self, player: PlayerId, valid_names: &[String]) -> Option<String> {
1125 choices::choose_card_name(self, player, valid_names)
1126 }
1127
1128 fn choose_number(
1129 &mut self,
1130 player: PlayerId,
1131 source: Option<CardId>,
1132 title: &str,
1133 description: Option<&str>,
1134 min: i32,
1135 max: i32,
1136 ) -> Option<i32> {
1137 choices::choose_number(self, player, source, title, description, min, max)
1138 }
1139
1140 fn choose_number_from_list(
1141 &mut self,
1142 player: PlayerId,
1143 choices: &[i32],
1144 message: &str,
1145 source_card_id: Option<CardId>,
1146 ) -> Option<i32> {
1147 choices::choose_number_from_list(self, player, choices, message, source_card_id)
1148 }
1149
1150 fn choose_roll_to_ignore(
1151 &mut self,
1152 player: PlayerId,
1153 rolls: &[i32],
1154 source: Option<CardId>,
1155 ) -> Option<i32> {
1156 choices::choose_roll_to_ignore(self, player, rolls, source)
1157 }
1158
1159 fn choose_roll_to_swap(
1160 &mut self,
1161 player: PlayerId,
1162 rolls: &[i32],
1163 source: Option<CardId>,
1164 ) -> Option<i32> {
1165 choices::choose_roll_to_swap(self, player, rolls, source)
1166 }
1167
1168 fn choose_dice_to_reroll(
1169 &mut self,
1170 player: PlayerId,
1171 rolls: &[i32],
1172 source: Option<CardId>,
1173 ) -> Vec<i32> {
1174 choices::choose_dice_to_reroll(self, player, rolls, source)
1175 }
1176
1177 fn choose_roll_to_modify(
1178 &mut self,
1179 player: PlayerId,
1180 rolls: &[i32],
1181 source: Option<CardId>,
1182 ) -> Option<i32> {
1183 choices::choose_roll_to_modify(self, player, rolls, source)
1184 }
1185
1186 fn choose_roll_swap_value(
1187 &mut self,
1188 player: PlayerId,
1189 current_result: i32,
1190 power: i32,
1191 toughness: i32,
1192 source: Option<CardId>,
1193 ) -> Option<RollSwapChoice> {
1194 choices::choose_roll_swap_value(self, player, current_result, power, toughness, source)
1195 }
1196
1197 fn flip_coin_call(&mut self, player: PlayerId) -> bool {
1198 choices::flip_coin_call(self, player)
1199 }
1200
1201 fn pay_combat_cost(
1202 &mut self,
1203 player: PlayerId,
1204 attacker: CardId,
1205 cost: i32,
1206 description: &str,
1207 mana_ability_options: &[manabrew_engine::agent::ManaAbilityOption],
1208 tappable_lands: &[CardId],
1209 untappable_lands: &[CardId],
1210 mana_pool_total: i32,
1211 ) -> CombatCostAction {
1212 combat::pay_combat_cost(
1213 self,
1214 player,
1215 attacker,
1216 cost,
1217 description,
1218 mana_ability_options,
1219 tappable_lands,
1220 untappable_lands,
1221 mana_pool_total,
1222 )
1223 }
1224
1225 fn choose_improvise(
1226 &mut self,
1227 player: PlayerId,
1228 untapped_artifacts: &[CardId],
1229 remaining_cost: &forge_foundation::ManaCost,
1230 source: Option<CardId>,
1231 ) -> Vec<CardId> {
1232 costs::choose_improvise(self, player, untapped_artifacts, remaining_cost, source)
1233 }
1234
1235 fn choose_convoke(
1236 &mut self,
1237 player: PlayerId,
1238 untapped_creatures: &[CardId],
1239 remaining_cost: &forge_foundation::ManaCost,
1240 source: Option<CardId>,
1241 ) -> Vec<CardId> {
1242 costs::choose_convoke(self, player, untapped_creatures, remaining_cost, source)
1243 }
1244
1245 fn pay_mana_cost(
1246 &mut self,
1247 player: PlayerId,
1248 card_id: CardId,
1249 card_name: &str,
1250 mana_cost: &str,
1251 mana_cost_display: &str,
1252 _mana_cost_checkpoint: &str,
1253 can_confirm_from_pool: bool,
1254 _allow_reserved_source_reuse: bool,
1255 _reserved_sacrifices: &[CardId],
1256 mana_ability_options: &[manabrew_engine::agent::ManaAbilityOption],
1257 tappable_lands: &[CardId],
1258 untappable_lands: &[CardId],
1259 mana_pool: &ManaPool,
1260 ) -> ManaCostAction {
1261 costs::pay_mana_cost(
1262 self,
1263 player,
1264 card_id,
1265 card_name,
1266 mana_cost,
1267 mana_cost_display,
1268 can_confirm_from_pool,
1269 mana_ability_options,
1270 tappable_lands,
1271 untappable_lands,
1272 mana_pool,
1273 )
1274 }
1275
1276 fn await_display_ack(&mut self) {
1277 if self.conceded {
1278 return;
1279 }
1280 if let ClientToServerMessage::Directive { directive } = self.responder.await_ack() {
1281 self.handle_directive(directive);
1282 }
1283 }
1284
1285 fn specify_mana_combo(
1286 &mut self,
1287 player: PlayerId,
1288 available_colors: &[String],
1289 amount: usize,
1290 source: Option<CardId>,
1291 express_choice: Option<u16>,
1292 ) -> Vec<String> {
1293 costs::specify_mana_combo(
1294 self,
1295 player,
1296 available_colors,
1297 amount,
1298 source,
1299 express_choice,
1300 )
1301 }
1302
1303 fn exert_attackers(&mut self, player: PlayerId, attackers: &[CardId]) -> Vec<CardId> {
1304 combat::exert_attackers(self, player, attackers)
1305 }
1306
1307 fn enlist_attackers(&mut self, player: PlayerId, attackers: &[CardId]) -> Vec<CardId> {
1308 combat::enlist_attackers(self, player, attackers)
1309 }
1310
1311 fn choose_reorder_library(
1312 &mut self,
1313 game: &GameState,
1314 player: PlayerId,
1315 cards: &[CardId],
1316 ) -> Vec<CardId> {
1317 library::choose_reorder_library(self, game, player, cards)
1318 }
1319
1320 fn help_pay_assist(&mut self, player: PlayerId, card_name: &str, max_generic: u32) -> u32 {
1321 choices::help_pay_assist(self, player, card_name, max_generic)
1322 }
1323
1324 fn choose_random_discard(
1325 &mut self,
1326 player: PlayerId,
1327 hand: &[CardId],
1328 num: usize,
1329 ) -> Vec<CardId> {
1330 choices::choose_random_discard(self, player, hand, num)
1331 }
1332
1333 fn choose_land_or_spell(&mut self, player: PlayerId) -> Option<bool> {
1334 choices::choose_land_or_spell(self, player)
1335 }
1336
1337 fn notify(&mut self, event: GameNotification) {
1338 match event {
1339 GameNotification::Event(log_event) => {
1340 self.responder
1341 .send_log(GameLogEntryDto::from_event(log_event));
1342 }
1343 GameNotification::CardPlayed {
1344 player,
1345 card_id,
1346 card_name,
1347 set_code,
1348 } => {
1349 self.emit_display(DisplayEvent::CardPlayed {
1350 card_id: card_id_str(card_id),
1351 card_name,
1352 set_code,
1353 player_id: player_id_str(player),
1354 });
1355 self.emit_state();
1356 }
1357 GameNotification::TurnChanged {
1358 active_player,
1359 turn_number,
1360 } => {
1361 let player_id = player_id_str(active_player);
1362 let active_player_name = self
1363 .latest_view
1364 .as_ref()
1365 .and_then(|v| v.players.iter().find(|p| p.id == player_id))
1366 .map(|p| p.name.clone())
1367 .unwrap_or_else(|| format!("Player {}", active_player.0));
1368 self.responder.send_log(GameLogEntryDto::from_event(
1369 manabrew_engine::agent::GameLogEvent::rule(format!(
1370 "TURN {} — {}",
1371 turn_number, active_player_name
1372 ))
1373 .with_player(active_player),
1374 ));
1375 self.emit_display(DisplayEvent::TurnChanged {
1376 active_player_id: player_id,
1377 active_player_name,
1378 turn_number,
1379 });
1380 self.emit_state();
1381 }
1382 GameNotification::PhaseChanged { .. } | GameNotification::StateChanged => {
1383 self.emit_state();
1384 }
1385 GameNotification::PriorityChanged { .. } => {
1386 self.emit_state();
1387 }
1388 GameNotification::FirstPlayerRoll {
1389 sides,
1390 rolls,
1391 winner,
1392 } => {
1393 let view = self.view();
1394 let winner_id = player_id_str(winner);
1395 let entries = rolls
1396 .into_iter()
1397 .map(|(pid, value)| {
1398 let id = player_id_str(pid);
1399 let name = view
1400 .players
1401 .iter()
1402 .find(|p| p.id == id)
1403 .map(|p| p.name.clone())
1404 .unwrap_or_else(|| id.clone());
1405 manabrew_protocol::prompts::dice_rolled::DiceRollEntry {
1406 label: Some(name),
1407 highlighted: id == winner_id,
1408 player_id: Some(id),
1409 natural_results: vec![value],
1410 final_results: vec![value],
1411 ignored_rolls: vec![],
1412 }
1413 })
1414 .collect();
1415 self.present_prompt(
1416 PromptInput::DiceRolled(
1417 manabrew_protocol::prompts::dice_rolled::DiceRolledInput {
1418 presentation: PromptPresentation {
1419 title: "Roll for first player".to_string(),
1420 description: None,
1421 text: None,
1422 targets: Vec::new(),
1423 },
1424 sides,
1425 rolls: entries,
1426 source_card_name: None,
1427 },
1428 ),
1429 None,
1430 );
1431 }
1434 GameNotification::DiceRolled {
1435 player,
1436 sides,
1437 natural_results,
1438 final_results,
1439 ignored_rolls,
1440 source_card_name,
1441 } => {
1442 self.present_prompt(
1448 PromptInput::DiceRolled(
1449 manabrew_protocol::prompts::dice_rolled::DiceRolledInput {
1450 presentation: PromptPresentation {
1451 title: "Dice roll".to_string(),
1452 description: None,
1453 text: None,
1454 targets: Vec::new(),
1455 },
1456 sides,
1457 rolls: vec![manabrew_protocol::prompts::dice_rolled::DiceRollEntry {
1458 label: None,
1459 player_id: Some(player_id_str(player)),
1460 natural_results,
1461 final_results,
1462 ignored_rolls,
1463 highlighted: false,
1464 }],
1465 source_card_name,
1466 },
1467 ),
1468 None,
1469 );
1470 }
1471 GameNotification::SnapshotCreated {
1472 checkpoint_id,
1473 label,
1474 } => {
1475 if let Some(view) = self.latest_view.clone() {
1476 self.responder.send_snapshot(GameSnapshotEventDto::new(
1477 checkpoint_id,
1478 label,
1479 view,
1480 ));
1481 }
1482 }
1483 GameNotification::GameOver => {
1484 self.emit_state();
1485 self.present_prompt(
1486 PromptInput::GameOver(manabrew_protocol::prompts::game_over::GameOverInput {}),
1487 None,
1488 );
1489 }
1490 GameNotification::ManaPaymentResolved { .. } => {}
1491 GameNotification::ActivatedAbilityPaymentFailed { .. } => {
1492 self.emit_state();
1493 }
1494 }
1495 }
1496
1497 fn take_restore_request(&mut self) -> Option<u64> {
1498 self.pending_restore_checkpoint.take()
1499 }
1500}