1use std::collections::HashMap;
2use std::time::Instant;
3
4use serde_json::Value;
5
6use crate::protocol::PlayerDeckInfo;
7const PLAYER_SLOT_PREFIX: &str = "player-";
8const MAX_FATAL_MESSAGE_CHARS: usize = 500;
9
10#[derive(Debug, Clone)]
11pub struct QueuedEngineInput {
12 pub from_player: String,
13 pub state: Value,
14}
15
16#[derive(Debug, Default)]
17pub struct ObservedOutcome {
18 pub game_over: bool,
19 pub winner_slot: Option<String>,
20 pub conceded_slots: Vec<String>,
21 pub fatal_message: Option<String>,
22}
23
24#[derive(Debug)]
25pub struct GameReplayCache {
26 pub game_id: String,
27 pub started_at: Instant,
28 pub player_order: Vec<String>,
29 pub player_decks: Vec<PlayerDeckInfo>,
30 pub starting_life: i32,
31 pub last_state: Option<Value>,
32 pub last_state_by_slot: HashMap<String, Value>,
33 pub pending_prompts: HashMap<String, Value>,
34 pub queued_inputs: HashMap<String, Vec<QueuedEngineInput>>,
35 pub outcome: ObservedOutcome,
36}
37
38impl GameReplayCache {
39 pub fn new(
40 game_id: String,
41 player_order: Vec<String>,
42 player_decks: Vec<PlayerDeckInfo>,
43 starting_life: i32,
44 ) -> Self {
45 GameReplayCache {
46 game_id,
47 started_at: Instant::now(),
48 player_order,
49 player_decks,
50 starting_life,
51 last_state: None,
52 last_state_by_slot: HashMap::new(),
53 pending_prompts: HashMap::new(),
54 queued_inputs: HashMap::new(),
55 outcome: ObservedOutcome::default(),
56 }
57 }
58
59 pub fn observe(&mut self, envelope: &Value) {
60 match envelope.get("kind").and_then(Value::as_str) {
61 Some("state") => {
62 self.observe_outcome(envelope);
63 match envelope.get("forPlayer").and_then(Value::as_str) {
64 Some(slot) => {
65 self.last_state_by_slot
66 .insert(slot.to_string(), envelope.clone());
67 }
68 None => self.last_state = Some(envelope.clone()),
69 }
70 }
71 Some("stateDelta") => {
75 let slot = envelope.get("forPlayer").and_then(Value::as_str);
76 let previous = match slot {
77 Some(slot) => self.last_state_by_slot.get(slot),
78 None => self.last_state.as_ref(),
79 };
80 let Some(previous) = previous else {
81 return;
82 };
83 let Some(patch) = envelope.get("patch") else {
84 return;
85 };
86 let base = previous.get("state").cloned().unwrap_or(Value::Null);
87 let mut rebuilt = previous.clone();
88 if let Some(object) = rebuilt.as_object_mut() {
89 object.insert(
90 "state".to_string(),
91 manabrew_relay_protocol::state_delta::apply(&base, patch),
92 );
93 if let Some(fingerprint) = envelope.get("fingerprint") {
94 object.insert("fingerprint".to_string(), fingerprint.clone());
95 }
96 }
97 self.observe_outcome(&rebuilt);
98 match slot {
99 Some(slot) => {
100 self.last_state_by_slot.insert(slot.to_string(), rebuilt);
101 }
102 None => self.last_state = Some(rebuilt),
103 }
104 }
105 Some("prompt") => {
106 if let Some(slot) = envelope.get("forPlayer").and_then(Value::as_str) {
107 self.pending_prompts
108 .insert(slot.to_string(), envelope.clone());
109 }
110 }
111 Some("response") => {
112 if let Some(slot) = envelope.get("fromPlayer").and_then(Value::as_str) {
113 self.pending_prompts.remove(slot);
114 }
115 }
116 Some("fatal") => {
117 if let Some(message) = envelope.get("message").and_then(Value::as_str) {
118 self.outcome.fatal_message =
119 Some(message.chars().take(MAX_FATAL_MESSAGE_CHARS).collect());
120 }
121 }
122 _ => {}
123 }
124 }
125
126 fn observe_outcome(&mut self, envelope: &Value) {
127 let Some(game_view) = envelope
128 .get("state")
129 .and_then(|state| state.get("gameView"))
130 else {
131 return;
132 };
133 if game_view.get("gameOver").and_then(Value::as_bool) != Some(true) {
134 return;
135 }
136 self.outcome.game_over = true;
137 self.outcome.winner_slot = game_view
138 .get("winnerId")
139 .and_then(Value::as_str)
140 .map(str::to_string);
141 if let Some(players) = game_view.get("players").and_then(Value::as_array) {
142 self.outcome.conceded_slots = players
143 .iter()
144 .filter(|player| player.get("status").and_then(Value::as_str) == Some("conceded"))
145 .filter_map(|player| player.get("id").and_then(Value::as_str))
146 .map(str::to_string)
147 .collect();
148 }
149 }
150
151 pub fn state_after(&self, envelope: &Value) -> Option<&Value> {
155 match envelope.get("forPlayer").and_then(Value::as_str) {
156 Some(slot) => self.last_state_by_slot.get(slot),
157 None => self.last_state.as_ref(),
158 }
159 }
160
161 pub fn slot_for(&self, username: &str) -> Option<String> {
162 self.player_order
163 .iter()
164 .position(|name| name == username)
165 .map(|index| format!("{PLAYER_SLOT_PREFIX}{index}"))
166 }
167
168 pub fn username_for_slot(&self, slot: &str) -> Option<String> {
169 slot.strip_prefix(PLAYER_SLOT_PREFIX)
170 .and_then(|index| index.parse::<usize>().ok())
171 .and_then(|index| self.player_order.get(index).cloned())
172 }
173
174 pub fn queue_input(&mut self, host_username: &str, from_player: &str, state: Value) {
175 self.queued_inputs
176 .entry(host_username.to_string())
177 .or_default()
178 .push(QueuedEngineInput {
179 from_player: from_player.to_string(),
180 state,
181 });
182 }
183
184 pub fn queued_inputs_for(&self, username: &str) -> Vec<QueuedEngineInput> {
185 self.queued_inputs
186 .get(username)
187 .cloned()
188 .unwrap_or_default()
189 }
190
191 pub fn acknowledge_inputs(&mut self, host_username: &str) {
192 self.queued_inputs.remove(host_username);
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use manabrew_relay_protocol::state_delta;
200 use serde_json::json;
201
202 fn cache() -> GameReplayCache {
203 GameReplayCache::new("game-1".into(), vec!["alice".into()], Vec::new(), 40)
204 }
205
206 fn full(slot: &str, state: Value) -> Value {
207 json!({
208 "kind": "state",
209 "forPlayer": slot,
210 "fingerprint": state_delta::fingerprint(&state),
211 "state": state,
212 })
213 }
214
215 fn patch(slot: &str, previous: &Value, next: &Value) -> Value {
218 json!({
219 "kind": "stateDelta",
220 "forPlayer": slot,
221 "fingerprint": state_delta::fingerprint(next),
222 "patch": state_delta::diff(previous, next).expect("the boards differ"),
223 })
224 }
225
226 #[test]
227 fn folds_a_patch_into_the_board_it_hands_back() {
228 let before = json!({ "gameView": { "turn": 1, "phase": "MAIN1" } });
229 let after = json!({ "gameView": { "turn": 2, "phase": "MAIN1" } });
230 let mut replay = cache();
231 replay.observe(&full("player0", before.clone()));
232 let patch = patch("player0", &before, &after);
233 replay.observe(&patch);
234
235 let folded = replay.state_after(&patch).expect("a board for that seat");
236 assert_eq!(folded["state"], after);
237 assert_eq!(folded["fingerprint"], patch["fingerprint"]);
238 }
239
240 #[test]
241 fn a_patch_for_a_seat_with_no_board_leaves_nothing_to_hand_back() {
242 let mut replay = cache();
243 let patch = patch(
244 "player0",
245 &json!({ "gameView": { "turn": 1 } }),
246 &json!({ "gameView": { "turn": 2 } }),
247 );
248 replay.observe(&patch);
249 assert!(replay.state_after(&patch).is_none());
250 }
251
252 #[test]
253 fn seats_do_not_share_a_board() {
254 let mut replay = cache();
255 let mine = json!({ "gameView": { "hand": ["Swamp"] } });
256 let theirs = json!({ "gameView": { "hand": [] } });
257 replay.observe(&full("player0", mine.clone()));
258 replay.observe(&full("player1", theirs.clone()));
259
260 let patch = patch("player0", &mine, &json!({ "gameView": { "hand": [] } }));
261 replay.observe(&patch);
262
263 let other = replay
264 .state_after(&json!({ "kind": "stateDelta", "forPlayer": "player1" }))
265 .expect("player1 still has a board");
266 assert_eq!(other["state"], theirs);
267 }
268}