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