Skip to main content

manabrew_server/
replay.rs

1use 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}