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;
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("prompt") => {
72                if let Some(slot) = envelope.get("forPlayer").and_then(Value::as_str) {
73                    self.pending_prompts
74                        .insert(slot.to_string(), envelope.clone());
75                }
76            }
77            Some("response") => {
78                if let Some(slot) = envelope.get("fromPlayer").and_then(Value::as_str) {
79                    self.pending_prompts.remove(slot);
80                }
81            }
82            Some("fatal") => {
83                if let Some(message) = envelope.get("message").and_then(Value::as_str) {
84                    self.outcome.fatal_message =
85                        Some(message.chars().take(MAX_FATAL_MESSAGE_CHARS).collect());
86                }
87            }
88            _ => {}
89        }
90    }
91
92    fn observe_outcome(&mut self, envelope: &Value) {
93        let Some(game_view) = envelope
94            .get("state")
95            .and_then(|state| state.get("gameView"))
96        else {
97            return;
98        };
99        if game_view.get("gameOver").and_then(Value::as_bool) != Some(true) {
100            return;
101        }
102        self.outcome.game_over = true;
103        self.outcome.winner_slot = game_view
104            .get("winnerId")
105            .and_then(Value::as_str)
106            .map(str::to_string);
107        if let Some(players) = game_view.get("players").and_then(Value::as_array) {
108            self.outcome.conceded_slots = players
109                .iter()
110                .filter(|player| player.get("status").and_then(Value::as_str) == Some("conceded"))
111                .filter_map(|player| player.get("id").and_then(Value::as_str))
112                .map(str::to_string)
113                .collect();
114        }
115    }
116
117    pub fn slot_for(&self, username: &str) -> Option<String> {
118        self.player_order
119            .iter()
120            .position(|name| name == username)
121            .map(|index| format!("{PLAYER_SLOT_PREFIX}{index}"))
122    }
123
124    pub fn username_for_slot(&self, slot: &str) -> Option<String> {
125        slot.strip_prefix(PLAYER_SLOT_PREFIX)
126            .and_then(|index| index.parse::<usize>().ok())
127            .and_then(|index| self.player_order.get(index).cloned())
128    }
129
130    pub fn queue_input(&mut self, host_username: &str, from_player: &str, state: Value) {
131        self.queued_inputs
132            .entry(host_username.to_string())
133            .or_default()
134            .push(QueuedEngineInput {
135                from_player: from_player.to_string(),
136                state,
137            });
138    }
139
140    pub fn queued_inputs_for(&self, username: &str) -> Vec<QueuedEngineInput> {
141        self.queued_inputs
142            .get(username)
143            .cloned()
144            .unwrap_or_default()
145    }
146
147    pub fn acknowledge_inputs(&mut self, host_username: &str) {
148        self.queued_inputs.remove(host_username);
149    }
150}