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