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