diff --git a/back_end/src/db_handler.rs b/back_end/src/db_handler.rs index f95dcdd..9d2c9ab 100644 --- a/back_end/src/db_handler.rs +++ b/back_end/src/db_handler.rs @@ -1,8 +1,10 @@ +use crate::constants::{COLS, ROWS}; + use std::sync::mpsc::{Receiver, SyncSender}; -use std::thread; +use std::{fmt, thread}; use rand::{thread_rng, Rng}; -use rusqlite::{params, Connection}; +use rusqlite::{params, Connection, Error as RusqliteError}; pub type GetIDSenderType = (u32, Option); /// first bool is player exists, @@ -10,6 +12,50 @@ pub type GetIDSenderType = (u32, Option); /// third bool is if cyan player pub type CheckPairingType = (bool, bool, bool); +pub type BoardStateType = (DBGameState, Option); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DBGameState { + CyanTurn, + MagentaTurn, + CyanWon, + MagentaWon, + Draw, + NotPaired, + OpponentDisconnected, + UnknownID, + InternalError, +} + +impl fmt::Display for DBGameState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + DBGameState::CyanTurn => write!(f, "cyan_turn"), + DBGameState::MagentaTurn => write!(f, "magenta_turn"), + DBGameState::CyanWon => write!(f, "cyan_won"), + DBGameState::MagentaWon => write!(f, "magenta_won"), + DBGameState::Draw => write!(f, "draw"), + DBGameState::NotPaired => write!(f, "not_paired"), + DBGameState::OpponentDisconnected => write!(f, "opponent_disconnected"), + DBGameState::UnknownID => write!(f, "unknown_id"), + DBGameState::InternalError => write!(f, "internal_error"), + } + } +} + +impl From for DBGameState { + fn from(value: i64) -> Self { + match value { + 0 => DBGameState::CyanTurn, + 1 => DBGameState::MagentaTurn, + 2 => DBGameState::CyanWon, + 3 => DBGameState::MagentaWon, + 4 => DBGameState::Draw, + _ => DBGameState::InternalError, + } + } +} + #[derive(Clone, Debug)] pub enum DBHandlerRequest { GetID(SyncSender), @@ -17,6 +63,10 @@ pub enum DBHandlerRequest { id: u32, response_sender: SyncSender, }, + GetGameState { + id: u32, + response_sender: SyncSender, + }, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -98,30 +148,39 @@ impl DBHandler { // not paired, can do nothing here } - let send_result = player_tx.send((player_id, is_cyan_player_opt)); - if let Err(e) = send_result { - println!("Failed to send back player id: {:?}", e); - self.shutdown_tx.send(()).ok(); - return true; - } - send_result.unwrap(); + // don't stop server on send fail, may have timed out and + // dropped the receiver + player_tx.send((player_id, is_cyan_player_opt)).ok(); } DBHandlerRequest::CheckPairing { id, response_sender, } => { - let check_result = self.check_if_player_is_paired(id); - if let Ok((exists, is_paired, is_cyan)) = check_result { - let send_result = response_sender.send((exists, is_paired, is_cyan)); - if let Err(e) = send_result { - println!("Failed to send back check pairing status: {:?}", e); - self.shutdown_tx.send(()).ok(); - return true; - } - send_result.unwrap(); + if let Ok((exists, is_paired, is_cyan)) = self.check_if_player_is_paired(None, id) { + // don't stop server on send fail, may have timed out and + // dropped the receiver + response_sender.send((exists, is_paired, is_cyan)).ok(); } else { + // On error, just respond that the given player_id doesn't + // exist + response_sender.send((false, false, true)).ok(); } - } // DBHandlerRequest::GetID(player_tx) + } + DBHandlerRequest::GetGameState { + id, + response_sender, + } => { + let get_board_result = self.get_board_state(None, id); + if get_board_result.is_err() { + // don't stop server on send fail, may have timed out and + // dropped the receiver + response_sender.send((DBGameState::UnknownID, None)).ok(); + return false; + } + // don't stop server on send fail, may have timed out and + // dropped the receiver + response_sender.send(get_board_result.unwrap()).ok(); + } } // match db_request false @@ -175,73 +234,75 @@ impl DBHandler { } fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> { - if let Some(conn) = conn { - let mut to_pair: Option = None; - let mut unpaired_players_stmt = conn - .prepare("SELECT id FROM players WHERE game_id ISNULL ORDER BY date_added;") - .map_err(|e| format!("{:?}", e))?; - let mut unpaired_players_rows = unpaired_players_stmt - .query([]) - .map_err(|e| format!("{:?}", e))?; - while let Some(row) = unpaired_players_rows - .next() - .map_err(|e| format!("{:?}", e))? - { - if to_pair.is_none() { - to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?); - } else { - let players: [u32; 2] = [ - to_pair.take().unwrap(), - row.get(0).map_err(|e| format!("{:?}", e))?, - ]; - self.create_game(Some(conn), &players)?; - } - } - - Ok(()) - } else { - let conn = self.get_conn(DBFirstRun::NotFirstRun)?; - self.pair_up_players(Some(&conn)) + if conn.is_none() { + return self.pair_up_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); } + let conn = conn.unwrap(); + let mut to_pair: Option = None; + let mut unpaired_players_stmt = conn + .prepare("SELECT id FROM players WHERE game_id ISNULL ORDER BY date_added;") + .map_err(|e| format!("{:?}", e))?; + let mut unpaired_players_rows = unpaired_players_stmt + .query([]) + .map_err(|e| format!("{:?}", e))?; + while let Some(row) = unpaired_players_rows + .next() + .map_err(|e| format!("{:?}", e))? + { + if to_pair.is_none() { + to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?); + } else { + let players: [u32; 2] = [ + to_pair.take().unwrap(), + row.get(0).map_err(|e| format!("{:?}", e))?, + ]; + self.create_game(Some(conn), &players)?; + } + } + + Ok(()) } fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result { - if let Some(conn) = conn { - let mut game_id: u32 = thread_rng().gen(); - { - let mut get_game_stmt = conn - .prepare("SELECT id FROM games WHERE id = ?;") - .map_err(|e| format!("{:?}", e))?; - while get_game_stmt.query_row([game_id], |_row| Ok(())).is_ok() { - game_id = thread_rng().gen(); - } - } - - // TODO randomize players (or first-come-first-serve ok to do?) - conn.execute( - "INSERT INTO games (id, cyan_player, magenta_player, date_added, board, status) VALUES (?, ?, ?, datetime(), ?, 0);", - params![game_id, players[0], players[1], new_board()] - ) - .map_err(|e| format!("{:?}", e))?; - conn.execute( - "UPDATE players SET game_id = ? WHERE id = ?", - [game_id, players[0]], - ) - .map_err(|e| format!("{:?}", e))?; - conn.execute( - "UPDATE players SET game_id = ? WHERE id = ?", - [game_id, players[1]], - ) - .map_err(|e| format!("{:?}", e))?; - - Ok(game_id) - } else { - let conn = self.get_conn(DBFirstRun::NotFirstRun)?; - self.create_game(Some(&conn), players) + if conn.is_none() { + return self.create_game(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), players); } + let conn = conn.unwrap(); + let mut game_id: u32 = thread_rng().gen(); + { + let mut get_game_stmt = conn + .prepare("SELECT id FROM games WHERE id = ?;") + .map_err(|e| format!("{:?}", e))?; + while get_game_stmt.query_row([game_id], |_row| Ok(())).is_ok() { + game_id = thread_rng().gen(); + } + } + + // TODO randomize players (or first-come-first-serve ok to do?) + conn.execute( + "INSERT INTO games (id, cyan_player, magenta_player, date_added, board, status) VALUES (?, ?, ?, datetime(), ?, 0);", + params![game_id, players[0], players[1], new_board()] + ) + .map_err(|e| format!("{:?}", e))?; + conn.execute( + "UPDATE players SET game_id = ? WHERE id = ?", + [game_id, players[0]], + ) + .map_err(|e| format!("{:?}", e))?; + conn.execute( + "UPDATE players SET game_id = ? WHERE id = ?", + [game_id, players[1]], + ) + .map_err(|e| format!("{:?}", e))?; + + Ok(game_id) } - fn check_if_player_is_paired(&self, player_id: u32) -> Result { + fn check_if_player_is_paired( + &self, + conn: Option<&Connection>, + player_id: u32, + ) -> Result { { let player_exists_result = self.check_if_player_exists(None, player_id); if player_exists_result.is_err() || !player_exists_result.unwrap() { @@ -250,7 +311,13 @@ impl DBHandler { } } - let conn = self.get_conn(DBFirstRun::NotFirstRun)?; + if conn.is_none() { + return self.check_if_player_is_paired( + Some(&self.get_conn(DBFirstRun::NotFirstRun)?), + player_id, + ); + } + let conn = conn.unwrap(); let check_player_row = conn.query_row("SELECT games.cyan_player FROM players JOIN games where games.id = players.game_id AND players.id = ?;", [player_id], |row| row.get::(0)); if let Ok(cyan_player) = check_player_row { @@ -262,12 +329,23 @@ impl DBHandler { Ok((true, true, false)) } } else if let Err(rusqlite::Error::QueryReturnedNoRows) = check_player_row { - // is not paired - Ok((true, false, true)) + // either does not exist or is not paired + let exists_check_result = self.check_if_player_exists(Some(conn), player_id); + if let Ok(exists) = exists_check_result { + if exists { + Ok((true, false, true)) + } else { + Ok((false, false, true)) + } + } else { + // pass the error contained in result, making sure the Ok type + // is the expected type + exists_check_result.map(|_| (false, false, false)) + } } else if let Err(e) = check_player_row { Err(format!("check_if_player_is_paired: {:?}", e)) } else { - unreachable!(); + unreachable!("All possible Ok and Err results are already checked"); } } @@ -276,19 +354,74 @@ impl DBHandler { conn: Option<&Connection>, player_id: u32, ) -> Result { - if let Some(conn) = conn { - let check_player_row = - conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| { - row.get::(0) - }); - if let Ok(_id) = check_player_row { - Ok(true) + if conn.is_none() { + return self + .check_if_player_exists(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id); + } + let conn = conn.unwrap(); + let check_player_row = + conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| { + row.get::(0) + }); + if let Ok(_id) = check_player_row { + Ok(true) + } else { + Ok(false) + } + } + + // clippy lint allow required due to conn.query_row() needing to handle + // returning a tuple in a Result + #[allow(clippy::unnecessary_unwrap)] + fn get_board_state( + &self, + conn: Option<&Connection>, + player_id: u32, + ) -> Result { + if conn.is_none() { + return self.get_board_state(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id); + } + let conn = conn.unwrap(); + + // TODO maybe handle "opponent_disconnected" case + let row_result: Result<(String, i64), RusqliteError> = + conn.query_row( + "SELECT games.board, games.status FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;", + [player_id], + |row| { + let board_result = row.get(0); + let status_result = row.get(1); + if board_result.is_ok() && status_result.is_ok() { + Ok((board_result.unwrap(), status_result.unwrap())) + } else if board_result.is_err() { + board_result + .map(|_| (String::from("this value should never be returned"), 0)) + } else { + status_result + .map(|_| (String::from("this value should never be returned"), 0)) + } + } + ); + if let Ok((board, status)) = row_result { + if board.len() != (ROWS * COLS) as usize { + Ok((DBGameState::InternalError, None)) } else { - Ok(false) + Ok((DBGameState::from(status), Some(board))) + } + } else if let Err(RusqliteError::QueryReturnedNoRows) = row_result { + // No rows is either player doesn't exist or not paired + let (exists, is_paired, _is_cyan) = + self.check_if_player_is_paired(Some(conn), player_id)?; + if !exists { + Ok((DBGameState::UnknownID, None)) + } else if !is_paired { + Ok((DBGameState::NotPaired, None)) + } else { + unreachable!("either exists or is_paired must be false"); } } else { - let conn = self.get_conn(DBFirstRun::NotFirstRun)?; - self.check_if_player_exists(Some(&conn), player_id) + // TODO use internal error enum instead of string + Err(String::from("internal_error")) } } } diff --git a/back_end/src/json_handlers.rs b/back_end/src/json_handlers.rs index 9872e62..b69168b 100644 --- a/back_end/src/json_handlers.rs +++ b/back_end/src/json_handlers.rs @@ -20,7 +20,7 @@ pub fn handle_json( "check_pairing" => handle_check_pairing(root, tx), "place_token" => handle_place_token(root), "disconnect" => handle_disconnect(root), - "game_state" => handle_game_state(root), + "game_state" => handle_game_state(root, tx), _ => Err("{\"type\":\"invalid_type\"}".into()), } } else { @@ -52,39 +52,40 @@ fn handle_pairing_request(tx: SyncSender) -> Result) -> Result { - if let Some(Value::Number(id)) = root.get("id") { - let (request_tx, request_rx) = sync_channel::(1); - let player_id = id - .as_u64() - .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; - let player_id: u32 = player_id - .try_into() - .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; - if tx - .send(DBHandlerRequest::CheckPairing { - id: player_id, - response_sender: request_tx, - }) - .is_err() - { - return Err("{\"type\":\"pairing_response\", \"status\":\"internal_error\"}".into()); - } - if let Ok((exists, is_paired, is_cyan)) = request_rx.recv_timeout(DB_REQUEST_TIMEOUT) { - if !exists { - Err("{\"type\":\"pairing_response\", \"status\":\"unknown_id\"}".into()) - } else if is_paired { - Ok(format!( - "{{\"type\":\"pairing_response\", \"status\":\"paired\", \"color\":\"{}\"}}", - if is_cyan { "cyan" } else { "magenta" } - )) - } else { - Ok("{\"type\"\"pairing_response\", \"status\":\"waiting\"}".into()) - } + let id_option = root.get("id"); + if id_option.is_none() { + return Err("{\"type\":\"invalid_syntax\"}".into()); + } + let player_id = id_option + .unwrap() + .as_u64() + .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; + let player_id: u32 = player_id + .try_into() + .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; + let (request_tx, request_rx) = sync_channel::(1); + if tx + .send(DBHandlerRequest::CheckPairing { + id: player_id, + response_sender: request_tx, + }) + .is_err() + { + return Err("{\"type\":\"pairing_response\", \"status\":\"internal_error\"}".into()); + } + if let Ok((exists, is_paired, is_cyan)) = request_rx.recv_timeout(DB_REQUEST_TIMEOUT) { + if !exists { + Err("{\"type\":\"pairing_response\", \"status\":\"unknown_id\"}".into()) + } else if is_paired { + Ok(format!( + "{{\"type\":\"pairing_response\", \"status\":\"paired\", \"color\":\"{}\"}}", + if is_cyan { "cyan" } else { "magenta" } + )) } else { - Err("{\"type\":\"pairing_response\", \"status\":\"internal_error_timeout\"}".into()) + Ok("{\"type\"\"pairing_response\", \"status\":\"waiting\"}".into()) } } else { - Err("{\"type\":\"invalid_syntax\"}".into()) + Err("{\"type\":\"pairing_response\", \"status\":\"internal_error_timeout\"}".into()) } } @@ -96,6 +97,44 @@ fn handle_disconnect(root: Value) -> Result { Err("{\"type\":\"unimplemented\"}".into()) } -fn handle_game_state(root: Value) -> Result { - Err("{\"type\":\"unimplemented\"}".into()) +fn handle_game_state(root: Value, tx: SyncSender) -> Result { + let id_option = root.get("id"); + if id_option.is_none() { + return Err("{\"type\":\"invalid_syntax\"}".into()); + } + let player_id = id_option + .unwrap() + .as_u64() + .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; + let player_id: u32 = player_id + .try_into() + .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; + + let (resp_tx, resp_rx) = sync_channel(1); + + if tx + .send(DBHandlerRequest::GetGameState { + id: player_id, + response_sender: resp_tx, + }) + .is_err() + { + return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into()); + } + + if let Ok((db_game_state, board_string_opt)) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) { + if let Some(board_string) = board_string_opt { + Ok(format!( + "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\"}}", + db_game_state, board_string + )) + } else { + Ok(format!( + "{{\"type\":\"game_state\", \"status\":\"{}\"}}", + db_game_state + )) + } + } else { + Err("{\"type\":\"game_state\", \"status\":\"internal_error_timeout\"}".into()) + } } diff --git a/backend_protocol_specification.md b/backend_protocol_specification.md index a008bc9..86ea079 100644 --- a/backend_protocol_specification.md +++ b/backend_protocol_specification.md @@ -125,7 +125,7 @@ then the back-end will respond with "too\_many\_players". "type": "game_state", "status": "not_paired", // or "unknown_id", "cyan_turn", "magenta_turn", // "cyan_won", "magenta_won", "draw", - // "opponent_disconnected" + // "opponent_disconnected", "internal_error" // "board" may not be in the response if "unknown_id" is the status "board": "abcdefg..." // 56-char long string with mapping: