1use std::time::Instant;
2
3use crate::protocol::{
4 DraftConfig, EngineKind, GameFormat, PlayerDeckInfo, RoomInfo, RoomPlayerInfo, RoomStatus,
5 SealedConfig,
6};
7use crate::replay::GameReplayCache;
8use manabrew_protocol::deck_dto::Deck;
9
10#[derive(Debug, Clone)]
11pub struct RoomSlot {
12 pub player_id: String,
13 pub username: String,
14 pub ready: bool,
15 pub connected: bool,
16 pub is_bot: bool,
17 pub selected_deck_name: Option<String>,
18 pub selected_deck: Option<Deck>,
19 pub published_deck_id: Option<String>,
20 pub selected_commander_name: Option<String>,
21 pub avatar: Option<String>,
22}
23
24#[derive(Debug, Clone)]
25pub struct RoomObserver {
26 pub player_id: String,
27 pub connected: bool,
28}
29
30#[derive(Debug)]
31pub struct Room {
32 pub room_id: String,
33 pub room_name: String,
34 pub protocol_version: u32,
35 pub host_player_id: String,
36 pub host_username: String,
37 pub hosted: bool,
38 pub official: bool,
39 pub password: Option<String>,
40 pub max_players: u8,
41 pub format: GameFormat,
42 pub engine: EngineKind,
43 pub status: RoomStatus,
44 pub players: Vec<RoomSlot>,
45 pub observers: Vec<RoomObserver>,
46 pub draft_config: Option<DraftConfig>,
47 pub sealed_config: Option<SealedConfig>,
48 pub reconnect_timeout_s: u32,
49 pub replay: Option<GameReplayCache>,
50 pub resume_token: String,
51 pub humanless_since: Option<Instant>,
52}
53
54impl Room {
55 #[allow(clippy::too_many_arguments)]
56 pub fn new(
57 room_id: String,
58 room_name: String,
59 protocol_version: u32,
60 host_player_id: String,
61 host_username: String,
62 max_players: u8,
63 format: GameFormat,
64 engine: EngineKind,
65 host_plays: bool,
66 draft_config: Option<DraftConfig>,
67 sealed_config: Option<SealedConfig>,
68 official: bool,
69 password: Option<String>,
70 reconnect_timeout_s: u32,
71 ) -> Self {
72 let max_players = max_players.clamp(2, 8);
73 let (players, observers) = if host_plays {
74 (
75 vec![RoomSlot {
76 player_id: host_player_id.clone(),
77 username: host_username.clone(),
78 ready: false,
79 connected: true,
80 is_bot: false,
81 selected_deck_name: None,
82 selected_deck: None,
83 published_deck_id: None,
84 selected_commander_name: None,
85 avatar: None,
86 }],
87 vec![],
88 )
89 } else {
90 (
91 vec![],
92 vec![RoomObserver {
93 player_id: host_player_id.clone(),
94 connected: true,
95 }],
96 )
97 };
98 Room {
99 room_id,
100 room_name,
101 protocol_version,
102 host_player_id: host_player_id.clone(),
103 host_username,
104 hosted: !host_plays,
105 official,
106 password,
107 max_players,
108 format,
109 engine,
110 status: RoomStatus::Lobby,
111 players,
112 observers,
113 draft_config,
114 sealed_config,
115 reconnect_timeout_s,
116 replay: None,
117 resume_token: String::new(),
118 humanless_since: None,
119 }
120 }
121
122 pub fn is_full(&self) -> bool {
123 self.players.len() >= self.max_players as usize
124 }
125
126 pub fn is_empty(&self) -> bool {
127 self.players.is_empty() && self.observers.is_empty()
128 }
129
130 pub fn is_limited_session(&self) -> bool {
131 matches!(
132 self.format,
133 GameFormat::Any | GameFormat::Draft | GameFormat::Sealed
134 ) && (self.draft_config.is_some() || self.sealed_config.is_some())
135 }
136
137 pub fn reset_lobby_settings(&mut self) {
138 if self.draft_config.is_none() && self.sealed_config.is_none() {
139 self.format = GameFormat::Any;
140 self.max_players = 4;
141 }
142 }
143
144 pub fn all_ready(&self) -> bool {
145 let min_players = if self.is_limited_session() { 1 } else { 2 };
146 let controller_id = self.controller_id();
147 self.players.len() >= min_players
148 && self
149 .players
150 .iter()
151 .filter(|p| Some(p.player_id.as_str()) != controller_id)
152 .all(|p| p.ready)
153 }
154
155 pub fn add_player(
156 &mut self,
157 player_id: String,
158 username: String,
159 is_bot: bool,
160 ) -> Result<(), String> {
161 if self.is_full() {
162 return Err("Room is full".into());
163 }
164 if self.status != RoomStatus::Lobby {
165 return Err("Game already started".into());
166 }
167 if self.players.iter().any(|p| p.player_id == player_id) {
168 return Err("Already in this room".into());
169 }
170 self.players.push(RoomSlot {
171 player_id,
172 username,
173 ready: false,
174 connected: true,
175 is_bot,
176 selected_deck_name: None,
177 selected_deck: None,
178 published_deck_id: None,
179 selected_commander_name: None,
180 avatar: None,
181 });
182 Ok(())
183 }
184
185 pub fn add_observer(&mut self, player_id: String, _username: String) -> Result<(), String> {
186 if self.players.iter().any(|p| p.player_id == player_id)
187 || self.observers.iter().any(|p| p.player_id == player_id)
188 {
189 return Err("Already in this room".into());
190 }
191 self.observers.push(RoomObserver {
192 player_id,
193 connected: true,
194 });
195 Ok(())
196 }
197
198 pub fn remove_player(&mut self, player_id: &str) -> Option<RoomSlot> {
199 if let Some(idx) = self.players.iter().position(|p| p.player_id == player_id) {
200 let slot = self.players.remove(idx);
201 if self.host_player_id == player_id {
203 if let Some(new_host) = self.players.first() {
204 self.host_player_id = new_host.player_id.clone();
205 self.host_username = new_host.username.clone();
206 }
207 }
208 Some(slot)
209 } else {
210 None
211 }
212 }
213
214 pub fn remove_observer(&mut self, player_id: &str) -> Option<RoomObserver> {
215 self.observers
216 .iter()
217 .position(|p| p.player_id == player_id)
218 .map(|idx| self.observers.remove(idx))
219 }
220
221 pub fn remove_participant(&mut self, player_id: &str) -> bool {
222 self.remove_player(player_id).is_some() || self.remove_observer(player_id).is_some()
223 }
224
225 pub fn set_connected(&mut self, player_id: &str, connected: bool) {
226 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
227 slot.connected = connected;
228 return;
229 }
230 if let Some(observer) = self.observers.iter_mut().find(|p| p.player_id == player_id) {
231 observer.connected = connected;
232 }
233 }
234
235 #[allow(dead_code)]
237 pub fn all_disconnected(&self) -> bool {
238 self.players.iter().all(|p| !p.connected) && self.observers.iter().all(|p| !p.connected)
239 }
240
241 pub fn has_connected_human(&self) -> bool {
242 self.players.iter().any(|p| p.connected && !p.is_bot)
243 || self
244 .observers
245 .iter()
246 .any(|o| o.connected && !(self.hosted && o.player_id == self.host_player_id))
247 }
248
249 pub fn set_ready(&mut self, player_id: &str, ready: bool) -> Result<(), String> {
250 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
251 slot.ready = ready;
252 Ok(())
253 } else {
254 Err("Player not in room".into())
255 }
256 }
257
258 pub fn set_deck_selection(
259 &mut self,
260 player_id: &str,
261 deck_name: String,
262 deck: Deck,
263 published_deck_id: Option<String>,
264 commander_name: Option<String>,
265 avatar: Option<String>,
266 ) -> Result<(), String> {
267 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
268 slot.selected_deck_name = Some(deck_name);
269 slot.selected_deck = Some(deck);
270 slot.published_deck_id = published_deck_id;
271 slot.selected_commander_name = commander_name;
272 slot.avatar = avatar;
273 slot.ready = false;
274 Ok(())
275 } else {
276 Err("Player not in room".into())
277 }
278 }
279
280 pub fn has_selected_deck(&self, player_id: &str) -> bool {
281 self.players
282 .iter()
283 .find(|p| p.player_id == player_id)
284 .map(|p| p.selected_deck.is_some() && p.selected_deck_name.is_some())
285 .unwrap_or(false)
286 }
287
288 pub fn player_decks(&self) -> Vec<PlayerDeckInfo> {
289 self.players
290 .iter()
291 .filter_map(|p| {
292 p.selected_deck.clone().map(|deck| PlayerDeckInfo {
293 username: p.username.clone(),
294 deck_name: p
295 .selected_deck_name
296 .clone()
297 .unwrap_or_else(|| "Unknown Deck".to_string()),
298 deck,
299 published_deck_id: p.published_deck_id.clone(),
300 commander_name: p.selected_commander_name.clone(),
301 avatar: p.avatar.clone(),
302 })
303 })
304 .collect()
305 }
306
307 pub fn is_host(&self, player_id: &str) -> bool {
308 self.host_player_id == player_id
309 }
310
311 pub fn controller_id(&self) -> Option<&str> {
317 self.players
318 .iter()
319 .find(|p| !p.is_bot)
320 .or_else(|| self.players.first())
321 .map(|p| p.player_id.as_str())
322 }
323
324 pub fn is_controller(&self, player_id: &str) -> bool {
325 self.controller_id() == Some(player_id)
326 }
327
328 pub fn host_username(&self) -> String {
329 self.host_username.clone()
330 }
331
332 pub fn host_is_player(&self) -> bool {
333 self.players
334 .iter()
335 .any(|p| p.player_id == self.host_player_id)
336 }
337
338 pub fn host_connected(&self) -> bool {
339 self.players
340 .iter()
341 .any(|p| p.player_id == self.host_player_id && p.connected)
342 || self
343 .observers
344 .iter()
345 .any(|p| p.player_id == self.host_player_id && p.connected)
346 }
347
348 pub fn player_usernames(&self) -> Vec<String> {
349 self.players.iter().map(|p| p.username.clone()).collect()
350 }
351
352 #[allow(dead_code)]
354 pub fn player_ids(&self) -> Vec<String> {
355 self.players.iter().map(|p| p.player_id.clone()).collect()
356 }
357
358 pub fn connected_player_ids(&self) -> Vec<String> {
359 self.players
360 .iter()
361 .filter(|p| p.connected)
362 .map(|p| p.player_id.clone())
363 .chain(
364 self.observers
365 .iter()
366 .filter(|p| p.connected)
367 .map(|p| p.player_id.clone()),
368 )
369 .collect()
370 }
371
372 pub fn to_room_info(&self) -> RoomInfo {
373 RoomInfo {
374 room_id: self.room_id.clone(),
375 room_name: self.room_name.clone(),
376 protocol_version: self.protocol_version,
377 host: self.host_username(),
378 hosted: self.hosted,
379 official: self.official,
380 password_protected: self.password.is_some(),
381 players: self
382 .players
383 .iter()
384 .map(|p| RoomPlayerInfo {
385 username: p.username.clone(),
386 ready: p.ready,
387 connected: p.connected,
388 is_bot: p.is_bot,
389 selected_deck_name: p.selected_deck_name.clone(),
390 })
391 .collect(),
392 max_players: self.max_players,
393 format: self.format.clone(),
394 engine: self.engine,
395 reconnect_timeout_s: self.reconnect_timeout_s,
396 status: self.status.clone(),
397 draft_config: self.draft_config.clone(),
398 sealed_config: self.sealed_config.clone(),
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn room(host_plays: bool) -> Room {
408 Room::new(
409 "r".into(),
410 "room".into(),
411 manabrew_protocol::protocol::PROTOCOL_VERSION,
412 "host".into(),
413 "host".into(),
414 4,
415 GameFormat::Commander,
416 EngineKind::Forge,
417 host_plays,
418 None,
419 None,
420 false,
421 None,
422 60,
423 )
424 }
425
426 #[test]
427 fn first_user_is_controller_in_hosted_room() {
428 let mut r = room(false);
429 assert!(r.is_host("host"));
430 assert!(!r.is_controller("host"));
431 r.add_player("human".into(), "human".into(), false).unwrap();
432 assert!(r.is_controller("human"));
433 r.add_player("bot".into(), "bot".into(), true).unwrap();
434 assert!(!r.is_controller("bot"));
435 }
436}