+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<bool>);
/// first bool is player exists,
/// third bool is if cyan player
pub type CheckPairingType = (bool, bool, bool);
+pub type BoardStateType = (DBGameState, Option<String>);
+
+#[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<i64> 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<GetIDSenderType>),
id: u32,
response_sender: SyncSender<CheckPairingType>,
},
+ GetGameState {
+ id: u32,
+ response_sender: SyncSender<BoardStateType>,
+ },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
// 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::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;
}
- } // DBHandlerRequest::GetID(player_tx)
+ // 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
}
fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> {
- if let Some(conn) = conn {
- let mut to_pair: Option<u32> = 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)?;
- }
+ if conn.is_none() {
+ return self.pair_up_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
+ }
+ let conn = conn.unwrap();
+ let mut to_pair: Option<u32> = 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))
}
+
+ Ok(())
}
fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> {
- 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();
- }
+ 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))?;
+ // 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)
- }
+ Ok(game_id)
}
- fn check_if_player_is_paired(&self, player_id: u32) -> Result<CheckPairingType, String> {
+ fn check_if_player_is_paired(
+ &self,
+ conn: Option<&Connection>,
+ player_id: u32,
+ ) -> Result<CheckPairingType, String> {
{
let player_exists_result = self.check_if_player_exists(None, player_id);
if player_exists_result.is_err() || !player_exists_result.unwrap() {
}
}
- 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::<usize, u32>(0));
if let Ok(cyan_player) = check_player_row {
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");
}
}
conn: Option<&Connection>,
player_id: u32,
) -> Result<bool, String> {
- if let Some(conn) = conn {
- let check_player_row =
- conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| {
- row.get::<usize, u32>(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::<usize, u32>(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<BoardStateType, String> {
+ 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((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 {
- Ok(false)
+ 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"))
}
}
}
"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 {
}
fn handle_check_pairing(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
- if let Some(Value::Number(id)) = root.get("id") {
- let (request_tx, request_rx) = sync_channel::<CheckPairingType>(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::<CheckPairingType>(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())
}
}
Err("{\"type\":\"unimplemented\"}".into())
}
-fn handle_game_state(root: Value) -> Result<String, String> {
- Err("{\"type\":\"unimplemented\"}".into())
+fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
+ 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())
+ }
}