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