Skip to main content

minesweeper_client/
game.rs

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/// Events emitted by the minesweeper game
15#[derive(Debug, Clone)]
16pub enum GameEvent {
17    /// The game board was updated with new cell states
18    BoardUpdated {
19        /// List of cell positions that changed
20        changed_positions: Vec<Pos>,
21    },
22    /// Game status changed (won/lost)
23    GameStatusChanged { won: bool, lost: bool },
24    /// Game was initialized or restarted
25    GameInitialized {
26        width: usize,
27        height: usize,
28        bombs: usize,
29    },
30    /// Connection was lost
31    ConnectionLost,
32}
33
34/// Represents the current state of a minesweeper game
35#[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    /// Create a new game state
47    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    /// Get the cell at the specified position
59    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    /// Update a cell at the specified position
68    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    /// Count the number of cells in each state
79    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    /// Check if the game is in a completed state (won or lost)
97    pub fn is_game_over(&self) -> bool {
98        self.game_over
99    }
100
101    /// Check if the player has won
102    pub fn is_won(&self) -> bool {
103        self.won
104    }
105}
106
107/// Connection state - all fields are required when connected
108struct ConnectionState {
109    websocket_sender: mpsc::UnboundedSender<ClientMessage>,
110    game_id: String,
111    background_task: JoinHandle<()>,
112}
113
114impl ConnectionState {
115    /// Send a message through the WebSocket connection
116    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    /// Get the game ID
124    fn get_game_id(&self) -> &String {
125        &self.game_id
126    }
127
128    /// Abort the background task and wait for it to finish
129    async fn abort_and_wait_background_task(self) {
130        self.background_task.abort();
131        let _ = self.background_task.await;
132    }
133}
134
135/// High-level minesweeper game client that manages game state locally
136pub 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    /// Create a new game instance
145    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    /// Subscribe to game events. Returns a receiver for game events.
156    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    /// Start a new game with the specified parameters
164    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        // Create the game via HTTP API
171        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        // Stop any existing background task
183        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        // Connect to the game via WebSocket
189        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        // Start background message listener
196        let background_task = self.start_background_listener(websocket);
197
198        // Create new connection state
199        *conn_state = Some(ConnectionState {
200            websocket_sender,
201            game_id,
202            background_task,
203        });
204
205        Ok(())
206    }
207
208    /// Send a message to the connected game
209    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    /// Reveal a cell at the specified position
222    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    /// Flag/unflag a cell at the specified position
230    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    /// Restart the game with new parameters
238    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    /// Get the current game state
249    pub async fn get_state(&self) -> Option<GameState> {
250        self.state.read().await.clone()
251    }
252
253    /// Get the game ID
254    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    /// Check if we're connected to a game
260    pub async fn is_connected(&self) -> bool {
261        let conn_state = self.connection_state.read().await;
262        conn_state.is_some()
263    }
264
265    /// Close the connection and clean up
266    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        // Clear event sender
274        *self.event_sender.write().await = None;
275
276        // Clear game state
277        *self.state.write().await = None;
278
279        info!("Disconnected from game");
280        Ok(())
281    }
282
283    /// Start background WebSocket message listener
284    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    /// Background task that handles incoming WebSocket messages
294    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                    // Connection closed
304                    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                            // Apply updates to local board
359                            for update in updates {
360                                game_state.set_cell(update.pos, update.value);
361                            }
362
363                            // Update game status
364                            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}