1use std::collections::HashMap;
2use std::sync::Arc;
3
4use minesweeper_common::{
5 models::{Cell, GameParams, Pos},
6 protocol::{ClientMessage, ServerMessage},
7};
8use tokio::sync::{RwLock, mpsc};
9use tokio::task::JoinHandle;
10use tracing::{debug, info, warn};
11
12use crate::{MinesweeperClient, MinesweeperWebSocket, Result};
13
14#[derive(Debug, Clone)]
16pub enum GameEvent {
17 BoardUpdated {
19 changed_positions: Vec<Pos>,
21 },
22 GameStatusChanged { won: bool, lost: bool },
24 GameInitialized {
26 width: usize,
27 height: usize,
28 bombs: usize,
29 },
30 ConnectionLost,
32}
33
34#[derive(Debug, Clone)]
36pub struct GameState {
37 pub width: usize,
38 pub height: usize,
39 pub bombs: usize,
40 pub board: Vec<Vec<Cell>>,
41 pub game_over: bool,
42 pub won: bool,
43}
44
45impl GameState {
46 pub fn new(width: usize, height: usize, bombs: usize, board: Vec<Vec<Cell>>) -> Self {
48 Self {
49 width,
50 height,
51 bombs,
52 board,
53 game_over: false,
54 won: false,
55 }
56 }
57
58 pub fn get_cell(&self, pos: Pos) -> Option<&Cell> {
60 if pos.x < self.width && pos.y < self.height {
61 self.board.get(pos.y)?.get(pos.x)
62 } else {
63 None
64 }
65 }
66
67 pub fn set_cell(&mut self, pos: Pos, cell: Cell) {
69 if pos.x < self.width
70 && pos.y < self.height
71 && let Some(row) = self.board.get_mut(pos.y)
72 && let Some(cell_ref) = row.get_mut(pos.x)
73 {
74 *cell_ref = cell;
75 }
76 }
77
78 pub fn count_cells(&self) -> HashMap<String, usize> {
80 let mut counts = HashMap::new();
81 for row in &self.board {
82 for cell in row {
83 let state = match cell {
84 Cell::Hidden => "hidden",
85 Cell::Marked => "marked",
86 Cell::Flagged => "flagged",
87 Cell::Revealed { .. } => "revealed",
88 Cell::Bomb => "bomb",
89 };
90 *counts.entry(state.to_string()).or_insert(0) += 1;
91 }
92 }
93 counts
94 }
95
96 pub fn is_game_over(&self) -> bool {
98 self.game_over
99 }
100
101 pub fn is_won(&self) -> bool {
103 self.won
104 }
105}
106
107struct ConnectionState {
109 websocket_sender: mpsc::UnboundedSender<ClientMessage>,
110 game_id: String,
111 background_task: JoinHandle<()>,
112}
113
114impl ConnectionState {
115 fn send_message(&self, message: ClientMessage) -> Result<()> {
117 self.websocket_sender
118 .send(message)
119 .map_err(|_| "WebSocket sender closed")?;
120 Ok(())
121 }
122
123 fn get_game_id(&self) -> &String {
125 &self.game_id
126 }
127
128 async fn abort_and_wait_background_task(self) {
130 self.background_task.abort();
131 let _ = self.background_task.await;
132 }
133}
134
135pub struct MinesweeperGame {
137 client: MinesweeperClient,
138 connection_state: Arc<RwLock<Option<ConnectionState>>>,
139 event_sender: Arc<RwLock<Option<mpsc::UnboundedSender<GameEvent>>>>,
140 state: Arc<RwLock<Option<GameState>>>,
141}
142
143impl MinesweeperGame {
144 pub fn new(server_url: &str) -> Result<Self> {
146 let client = MinesweeperClient::new(server_url)?;
147 Ok(Self {
148 client,
149 connection_state: Arc::new(RwLock::new(None)),
150 event_sender: Arc::new(RwLock::new(None)),
151 state: Arc::new(RwLock::new(None)),
152 })
153 }
154
155 pub async fn subscribe_to_events(&self) -> mpsc::UnboundedReceiver<GameEvent> {
157 let (sender, receiver) = mpsc::unbounded_channel();
158 let mut event_sender = self.event_sender.write().await;
159 *event_sender = Some(sender);
160 receiver
161 }
162
163 pub async fn start_game(&self, params: GameParams) -> Result<()> {
165 info!(
166 "Starting new game: {}x{} with {} bombs",
167 params.width, params.height, params.bombs
168 );
169
170 let game_id = self.client.create_game(params).await?;
172 info!("Created game with ID: {}", game_id);
173
174 self.join_game(game_id).await
175 }
176
177 pub async fn join_game(&self, game_id: String) -> Result<()> {
178 info!("Joining game with ID: {}", game_id);
179
180 let mut conn_state = self.connection_state.write().await;
181
182 if let Some(existing_conn) = conn_state.take() {
184 existing_conn.abort_and_wait_background_task().await;
185 }
186 self.state.write().await.take();
187
188 let ws_url = self.client.websocket_url(&game_id)?;
190 let websocket = MinesweeperWebSocket::connect(&ws_url).await?;
191 let websocket_sender = websocket.get_sender();
192
193 info!("Connected to game with ID: {}", game_id);
194
195 let background_task = self.start_background_listener(websocket);
197
198 *conn_state = Some(ConnectionState {
200 websocket_sender,
201 game_id,
202 background_task,
203 });
204
205 Ok(())
206 }
207
208 async fn send_client_message(&self, message: ClientMessage) -> Result<()> {
210 let conn_state = self.connection_state.read().await;
211
212 if let Some(ref conn) = *conn_state {
213 conn.send_message(message)?;
214 } else {
215 return Err("Not connected to a game. Call start_game() first.".into());
216 }
217
218 Ok(())
219 }
220
221 pub async fn reveal(&self, pos: Pos) -> Result<()> {
223 debug!("Revealing cell at ({}, {})", pos.x, pos.y);
224
225 let message = ClientMessage::Reveal { pos };
226 self.send_client_message(message).await
227 }
228
229 pub async fn flag(&self, pos: Pos) -> Result<()> {
231 debug!("Flagging cell at ({}, {})", pos.x, pos.y);
232
233 let message = ClientMessage::Flag { pos };
234 self.send_client_message(message).await
235 }
236
237 pub async fn restart(&self, params: GameParams) -> Result<()> {
239 info!(
240 "Restarting game with new parameters: {}x{} with {} bombs",
241 params.width, params.height, params.bombs
242 );
243
244 let message = ClientMessage::Restart { params };
245 self.send_client_message(message).await
246 }
247
248 pub async fn get_state(&self) -> Option<GameState> {
250 self.state.read().await.clone()
251 }
252
253 pub async fn get_game_id(&self) -> Option<String> {
255 let conn_state = self.connection_state.read().await;
256 conn_state.as_ref().map(|conn| conn.get_game_id().clone())
257 }
258
259 pub async fn is_connected(&self) -> bool {
261 let conn_state = self.connection_state.read().await;
262 conn_state.is_some()
263 }
264
265 pub async fn disconnect(&self) -> Result<()> {
267 let mut conn_state = self.connection_state.write().await;
268
269 if let Some(conn) = conn_state.take() {
270 conn.abort_and_wait_background_task().await;
271 }
272
273 *self.event_sender.write().await = None;
275
276 *self.state.write().await = None;
278
279 info!("Disconnected from game");
280 Ok(())
281 }
282
283 fn start_background_listener(&self, mut websocket: MinesweeperWebSocket) -> JoinHandle<()> {
285 let state = self.state.clone();
286 let event_sender = self.event_sender.clone();
287
288 tokio::spawn(async move {
289 Self::background_message_handler(&mut websocket, state, event_sender).await;
290 })
291 }
292
293 async fn background_message_handler(
295 websocket: &mut MinesweeperWebSocket,
296 state: Arc<RwLock<Option<GameState>>>,
297 event_sender: Arc<RwLock<Option<mpsc::UnboundedSender<GameEvent>>>>,
298 ) {
299 loop {
300 let message = match websocket.receive_message().await {
301 Ok(Some(msg)) => msg,
302 Ok(None) => {
303 if let Some(ref sender) = *event_sender.read().await {
305 let _ = sender.send(GameEvent::ConnectionLost);
306 }
307 break;
308 }
309 Err(e) => {
310 warn!("Error receiving WebSocket message: {}", e);
311 if let Some(ref sender) = *event_sender.read().await {
312 let _ = sender.send(GameEvent::ConnectionLost);
313 }
314 break;
315 }
316 };
317
318 match message {
319 ServerMessage::Init {
320 width,
321 height,
322 bombs,
323 field,
324 } => {
325 info!(
326 "Received game initialization: {}x{} with {} bombs",
327 width, height, bombs
328 );
329
330 let new_state = GameState::new(width, height, bombs, field);
331 *state.write().await = Some(new_state);
332
333 if let Some(ref sender) = *event_sender.read().await {
334 let _ = sender.send(GameEvent::GameInitialized {
335 width,
336 height,
337 bombs,
338 });
339 }
340 }
341 ServerMessage::Update { updates, won, lost } => {
342 debug!(
343 "Received update: {} cells updated, won: {}, lost: {}",
344 updates.len(),
345 won,
346 lost
347 );
348
349 let changed_positions: Vec<Pos> = updates.iter().map(|u| u.pos).collect();
350 let status_changed;
351
352 {
353 let mut state_guard = state.write().await;
354 if let Some(ref mut game_state) = *state_guard {
355 let old_won = game_state.won;
356 let old_game_over = game_state.game_over;
357
358 for update in updates {
360 game_state.set_cell(update.pos, update.value);
361 }
362
363 game_state.won = won;
365 game_state.game_over = won || lost;
366
367 status_changed =
368 game_state.won != old_won || game_state.game_over != old_game_over;
369 } else {
370 status_changed = false;
371 }
372 }
373
374 if let Some(ref sender) = *event_sender.read().await {
375 if !changed_positions.is_empty() {
376 let _ = sender.send(GameEvent::BoardUpdated { changed_positions });
377 }
378
379 if status_changed {
380 let _ = sender.send(GameEvent::GameStatusChanged { won, lost });
381 }
382 }
383 }
384 }
385 }
386 }
387}