manabrew_server/
replay.rs1use 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}