manabrew_server/
replay.rs1use std::collections::HashMap;
2use std::time::Instant;
3
4use serde_json::Value;
5
6use crate::protocol::PlayerDeckInfo;
7use crate::room::RoomSlot;
8
9const PLAYER_SLOT_PREFIX: &str = "player-";
10const MAX_FATAL_MESSAGE_CHARS: usize = 500;
11
12#[derive(Debug, Default)]
13pub struct ObservedOutcome {
14 pub game_over: bool,
15 pub winner_slot: Option<String>,
16 pub conceded_slots: Vec<String>,
17 pub fatal_message: Option<String>,
18}
19
20#[derive(Debug)]
21pub struct GameReplayCache {
22 pub game_id: String,
23 pub started_at: Instant,
24 pub player_order: Vec<String>,
25 pub player_decks: Vec<PlayerDeckInfo>,
26 pub starting_life: i32,
27 pub last_state: Option<Value>,
28 pub last_state_by_slot: HashMap<String, Value>,
29 pub pending_prompts: HashMap<String, Value>,
30 pub queued_responses: HashMap<String, Vec<Value>>,
31 pub outcome: ObservedOutcome,
32}
33
34impl GameReplayCache {
35 pub fn new(
36 game_id: String,
37 player_order: Vec<String>,
38 player_decks: Vec<PlayerDeckInfo>,
39 starting_life: i32,
40 ) -> Self {
41 GameReplayCache {
42 game_id,
43 started_at: Instant::now(),
44 player_order,
45 player_decks,
46 starting_life,
47 last_state: None,
48 last_state_by_slot: HashMap::new(),
49 pending_prompts: HashMap::new(),
50 queued_responses: HashMap::new(),
51 outcome: ObservedOutcome::default(),
52 }
53 }
54
55 pub fn observe(&mut self, envelope: &Value, players: &[RoomSlot]) {
56 match envelope.get("kind").and_then(Value::as_str) {
57 Some("state") => {
58 self.observe_outcome(envelope);
59 match envelope.get("forPlayer").and_then(Value::as_str) {
60 Some(slot) => {
61 self.last_state_by_slot
62 .insert(slot.to_string(), envelope.clone());
63 }
64 None => self.last_state = Some(envelope.clone()),
65 }
66 }
67 Some("prompt") => {
68 if let Some(slot) = envelope.get("forPlayer").and_then(Value::as_str) {
69 self.pending_prompts
70 .insert(slot.to_string(), envelope.clone());
71 }
72 }
73 Some("response") => {
74 if let Some(slot) = envelope.get("fromPlayer").and_then(Value::as_str) {
75 self.pending_prompts.remove(slot);
76 }
77 for player in players.iter().filter(|p| !p.connected) {
78 self.queued_responses
79 .entry(player.username.clone())
80 .or_default()
81 .push(envelope.clone());
82 }
83 }
84 Some("fatal") => {
85 if let Some(message) = envelope.get("message").and_then(Value::as_str) {
86 self.outcome.fatal_message =
87 Some(message.chars().take(MAX_FATAL_MESSAGE_CHARS).collect());
88 }
89 }
90 _ => {}
91 }
92 }
93
94 fn observe_outcome(&mut self, envelope: &Value) {
95 let Some(game_view) = envelope
96 .get("state")
97 .and_then(|state| state.get("gameView"))
98 else {
99 return;
100 };
101 if game_view.get("gameOver").and_then(Value::as_bool) != Some(true) {
102 return;
103 }
104 self.outcome.game_over = true;
105 self.outcome.winner_slot = game_view
106 .get("winnerId")
107 .and_then(Value::as_str)
108 .map(str::to_string);
109 if let Some(players) = game_view.get("players").and_then(Value::as_array) {
110 self.outcome.conceded_slots = players
111 .iter()
112 .filter(|player| player.get("status").and_then(Value::as_str) == Some("conceded"))
113 .filter_map(|player| player.get("id").and_then(Value::as_str))
114 .map(str::to_string)
115 .collect();
116 }
117 }
118
119 pub fn slot_for(&self, username: &str) -> Option<String> {
120 self.player_order
121 .iter()
122 .position(|name| name == username)
123 .map(|index| format!("{PLAYER_SLOT_PREFIX}{index}"))
124 }
125
126 pub fn username_for_slot(&self, slot: &str) -> Option<String> {
127 slot.strip_prefix(PLAYER_SLOT_PREFIX)
128 .and_then(|index| index.parse::<usize>().ok())
129 .and_then(|index| self.player_order.get(index).cloned())
130 }
131
132 pub fn take_queued_responses(&mut self, username: &str) -> Vec<Value> {
133 self.queued_responses.remove(username).unwrap_or_default()
134 }
135}