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.remove(slot),
78 None => self.last_state.take(),
79 };
80 let Some(mut rebuilt) = previous else {
81 return;
82 };
83 let Some(patch) = envelope.get("patch") else {
84 self.restore_state(slot, rebuilt);
85 return;
86 };
87 if let Some(object) = rebuilt.as_object_mut() {
88 let base = object.remove("state").unwrap_or(Value::Null);
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 self.restore_state(slot, rebuilt);
99 }
100 Some("prompt") => {
101 if let Some(slot) = envelope.get("forPlayer").and_then(Value::as_str) {
102 self.pending_prompts
103 .insert(slot.to_string(), envelope.clone());
104 }
105 }
106 Some("response") => {
107 if let Some(slot) = envelope.get("fromPlayer").and_then(Value::as_str) {
108 self.pending_prompts.remove(slot);
109 }
110 }
111 Some("fatal") => {
112 if let Some(message) = envelope.get("message").and_then(Value::as_str) {
113 self.outcome.fatal_message =
114 Some(message.chars().take(MAX_FATAL_MESSAGE_CHARS).collect());
115 }
116 }
117 _ => {}
118 }
119 }
120
121 fn restore_state(&mut self, slot: Option<&str>, state: Value) {
122 match slot {
123 Some(slot) => {
124 self.last_state_by_slot.insert(slot.to_string(), state);
125 }
126 None => self.last_state = Some(state),
127 }
128 }
129
130 fn observe_outcome(&mut self, envelope: &Value) {
131 let Some(game_view) = envelope
132 .get("state")
133 .and_then(|state| state.get("gameView"))
134 else {
135 return;
136 };
137 if game_view.get("gameOver").and_then(Value::as_bool) != Some(true) {
138 return;
139 }
140 self.outcome.game_over = true;
141 self.outcome.winner_slot = game_view
142 .get("winnerId")
143 .and_then(Value::as_str)
144 .map(str::to_string);
145 if let Some(players) = game_view.get("players").and_then(Value::as_array) {
146 self.outcome.conceded_slots = players
147 .iter()
148 .filter(|player| player.get("status").and_then(Value::as_str) == Some("conceded"))
149 .filter_map(|player| player.get("id").and_then(Value::as_str))
150 .map(str::to_string)
151 .collect();
152 }
153 }
154
155 pub fn state_after(&self, envelope: &Value) -> Option<&Value> {
159 match envelope.get("forPlayer").and_then(Value::as_str) {
160 Some(slot) => self.last_state_by_slot.get(slot),
161 None => self.last_state.as_ref(),
162 }
163 }
164
165 pub fn slot_for(&self, username: &str) -> Option<String> {
166 self.player_order
167 .iter()
168 .position(|name| name == username)
169 .map(|index| format!("{PLAYER_SLOT_PREFIX}{index}"))
170 }
171
172 pub fn username_for_slot(&self, slot: &str) -> Option<String> {
173 slot.strip_prefix(PLAYER_SLOT_PREFIX)
174 .and_then(|index| index.parse::<usize>().ok())
175 .and_then(|index| self.player_order.get(index).cloned())
176 }
177
178 pub fn queue_input(&mut self, host_username: &str, from_player: &str, state: Value) {
179 self.queued_inputs
180 .entry(host_username.to_string())
181 .or_default()
182 .push(QueuedEngineInput {
183 from_player: from_player.to_string(),
184 state,
185 });
186 }
187
188 pub fn queued_inputs_for(&self, username: &str) -> Vec<QueuedEngineInput> {
189 self.queued_inputs
190 .get(username)
191 .cloned()
192 .unwrap_or_default()
193 }
194
195 pub fn acknowledge_inputs(&mut self, host_username: &str) {
196 self.queued_inputs.remove(host_username);
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use manabrew_relay_protocol::state_delta;
204 use serde_json::json;
205
206 fn cache() -> GameReplayCache {
207 GameReplayCache::new("game-1".into(), vec!["alice".into()], Vec::new(), 40)
208 }
209
210 fn full(slot: &str, state: Value) -> Value {
211 json!({
212 "kind": "state",
213 "forPlayer": slot,
214 "fingerprint": state_delta::fingerprint(&state),
215 "state": state,
216 })
217 }
218
219 fn patch(slot: &str, previous: &Value, next: &Value) -> Value {
222 json!({
223 "kind": "stateDelta",
224 "forPlayer": slot,
225 "fingerprint": state_delta::fingerprint(next),
226 "patch": state_delta::diff(previous, next).expect("the boards differ"),
227 })
228 }
229
230 #[test]
231 fn folds_a_patch_into_the_board_it_hands_back() {
232 let before = json!({ "gameView": { "turn": 1, "phase": "MAIN1" } });
233 let after = json!({ "gameView": { "turn": 2, "phase": "MAIN1" } });
234 let mut replay = cache();
235 replay.observe(&full("player0", before.clone()));
236 let patch = patch("player0", &before, &after);
237 replay.observe(&patch);
238
239 let folded = replay.state_after(&patch).expect("a board for that seat");
240 assert_eq!(folded["state"], after);
241 assert_eq!(folded["fingerprint"], patch["fingerprint"]);
242 }
243
244 #[test]
245 fn a_patch_for_a_seat_with_no_board_leaves_nothing_to_hand_back() {
246 let mut replay = cache();
247 let patch = patch(
248 "player0",
249 &json!({ "gameView": { "turn": 1 } }),
250 &json!({ "gameView": { "turn": 2 } }),
251 );
252 replay.observe(&patch);
253 assert!(replay.state_after(&patch).is_none());
254 }
255
256 #[test]
257 fn seats_do_not_share_a_board() {
258 let mut replay = cache();
259 let mine = json!({ "gameView": { "hand": ["Swamp"] } });
260 let theirs = json!({ "gameView": { "hand": [] } });
261 replay.observe(&full("player0", mine.clone()));
262 replay.observe(&full("player1", theirs.clone()));
263
264 let patch = patch("player0", &mine, &json!({ "gameView": { "hand": [] } }));
265 replay.observe(&patch);
266
267 let other = replay
268 .state_after(&json!({ "kind": "stateDelta", "forPlayer": "player1" }))
269 .expect("player1 still has a board");
270 assert_eq!(other["state"], theirs);
271 }
272}