backend: refactorings, impl "board_state" query

Implemented the fifth backend protocol request/response ("board_state").
Some refactorings involve improving readability from handling unwrapping Option
of &Connection objects.
This commit is contained in:
Stephen Seo 2022-03-30 20:43:49 +09:00
parent 8706f8a90d
commit fbf47027ef
3 changed files with 299 additions and 127 deletions

View file

@ -1,8 +1,10 @@
use crate::constants::{COLS, ROWS};
use std::sync::mpsc::{Receiver, SyncSender}; use std::sync::mpsc::{Receiver, SyncSender};
use std::thread; use std::{fmt, thread};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use rusqlite::{params, Connection}; use rusqlite::{params, Connection, Error as RusqliteError};
pub type GetIDSenderType = (u32, Option<bool>); pub type GetIDSenderType = (u32, Option<bool>);
/// first bool is player exists, /// first bool is player exists,
@ -10,6 +12,50 @@ pub type GetIDSenderType = (u32, Option<bool>);
/// third bool is if cyan player /// third bool is if cyan player
pub type CheckPairingType = (bool, bool, bool); 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)] #[derive(Clone, Debug)]
pub enum DBHandlerRequest { pub enum DBHandlerRequest {
GetID(SyncSender<GetIDSenderType>), GetID(SyncSender<GetIDSenderType>),
@ -17,6 +63,10 @@ pub enum DBHandlerRequest {
id: u32, id: u32,
response_sender: SyncSender<CheckPairingType>, response_sender: SyncSender<CheckPairingType>,
}, },
GetGameState {
id: u32,
response_sender: SyncSender<BoardStateType>,
},
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -98,30 +148,39 @@ impl DBHandler {
// not paired, can do nothing here // not paired, can do nothing here
} }
let send_result = player_tx.send((player_id, is_cyan_player_opt)); // don't stop server on send fail, may have timed out and
if let Err(e) = send_result { // dropped the receiver
println!("Failed to send back player id: {:?}", e); player_tx.send((player_id, is_cyan_player_opt)).ok();
self.shutdown_tx.send(()).ok();
return true;
}
send_result.unwrap();
} }
DBHandlerRequest::CheckPairing { DBHandlerRequest::CheckPairing {
id, id,
response_sender, response_sender,
} => { } => {
let check_result = self.check_if_player_is_paired(id); if let Ok((exists, is_paired, is_cyan)) = self.check_if_player_is_paired(None, id) {
if let Ok((exists, is_paired, is_cyan)) = check_result { // don't stop server on send fail, may have timed out and
let send_result = response_sender.send((exists, is_paired, is_cyan)); // dropped the receiver
if let Err(e) = send_result { response_sender.send((exists, is_paired, is_cyan)).ok();
println!("Failed to send back check pairing status: {:?}", e);
self.shutdown_tx.send(()).ok();
return true;
}
send_result.unwrap();
} else { } 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;
}
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender.send(get_board_result.unwrap()).ok();
} }
} // DBHandlerRequest::GetID(player_tx)
} // match db_request } // match db_request
false false
@ -175,7 +234,10 @@ impl DBHandler {
} }
fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> { fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> {
if let Some(conn) = 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<u32> = None; let mut to_pair: Option<u32> = None;
let mut unpaired_players_stmt = conn let mut unpaired_players_stmt = conn
.prepare("SELECT id FROM players WHERE game_id ISNULL ORDER BY date_added;") .prepare("SELECT id FROM players WHERE game_id ISNULL ORDER BY date_added;")
@ -199,14 +261,13 @@ impl DBHandler {
} }
Ok(()) Ok(())
} else {
let conn = self.get_conn(DBFirstRun::NotFirstRun)?;
self.pair_up_players(Some(&conn))
}
} }
fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> { fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> {
if let Some(conn) = conn { 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 game_id: u32 = thread_rng().gen();
{ {
let mut get_game_stmt = conn let mut get_game_stmt = conn
@ -235,13 +296,13 @@ impl DBHandler {
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
Ok(game_id) Ok(game_id)
} else {
let conn = self.get_conn(DBFirstRun::NotFirstRun)?;
self.create_game(Some(&conn), players)
}
} }
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); let player_exists_result = self.check_if_player_exists(None, player_id);
if player_exists_result.is_err() || !player_exists_result.unwrap() { 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::<usize, u32>(0)); 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 { if let Ok(cyan_player) = check_player_row {
@ -262,12 +329,23 @@ impl DBHandler {
Ok((true, true, false)) Ok((true, true, false))
} }
} else if let Err(rusqlite::Error::QueryReturnedNoRows) = check_player_row { } else if let Err(rusqlite::Error::QueryReturnedNoRows) = check_player_row {
// is not paired // 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)) 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 { } else if let Err(e) = check_player_row {
Err(format!("check_if_player_is_paired: {:?}", e)) Err(format!("check_if_player_is_paired: {:?}", e))
} else { } else {
unreachable!(); unreachable!("All possible Ok and Err results are already checked");
} }
} }
@ -276,7 +354,11 @@ impl DBHandler {
conn: Option<&Connection>, conn: Option<&Connection>,
player_id: u32, player_id: u32,
) -> Result<bool, String> { ) -> Result<bool, String> {
if let Some(conn) = conn { 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 = let check_player_row =
conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| { conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| {
row.get::<usize, u32>(0) row.get::<usize, u32>(0)
@ -286,9 +368,60 @@ impl DBHandler {
} else { } else {
Ok(false) 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 { } else {
let conn = self.get_conn(DBFirstRun::NotFirstRun)?; status_result
self.check_if_player_exists(Some(&conn), player_id) .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 {
unreachable!("either exists or is_paired must be false");
}
} else {
// TODO use internal error enum instead of string
Err(String::from("internal_error"))
} }
} }
} }

View file

@ -20,7 +20,7 @@ pub fn handle_json(
"check_pairing" => handle_check_pairing(root, tx), "check_pairing" => handle_check_pairing(root, tx),
"place_token" => handle_place_token(root), "place_token" => handle_place_token(root),
"disconnect" => handle_disconnect(root), "disconnect" => handle_disconnect(root),
"game_state" => handle_game_state(root), "game_state" => handle_game_state(root, tx),
_ => Err("{\"type\":\"invalid_type\"}".into()), _ => Err("{\"type\":\"invalid_type\"}".into()),
} }
} else { } else {
@ -52,14 +52,18 @@ fn handle_pairing_request(tx: SyncSender<DBHandlerRequest>) -> Result<String, St
} }
fn handle_check_pairing(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> { fn handle_check_pairing(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
if let Some(Value::Number(id)) = root.get("id") { let id_option = root.get("id");
let (request_tx, request_rx) = sync_channel::<CheckPairingType>(1); if id_option.is_none() {
let player_id = id return Err("{\"type\":\"invalid_syntax\"}".into());
}
let player_id = id_option
.unwrap()
.as_u64() .as_u64()
.ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?;
let player_id: u32 = player_id let player_id: u32 = player_id
.try_into() .try_into()
.map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?;
let (request_tx, request_rx) = sync_channel::<CheckPairingType>(1);
if tx if tx
.send(DBHandlerRequest::CheckPairing { .send(DBHandlerRequest::CheckPairing {
id: player_id, id: player_id,
@ -83,9 +87,6 @@ fn handle_check_pairing(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result
} else { } else {
Err("{\"type\":\"pairing_response\", \"status\":\"internal_error_timeout\"}".into()) Err("{\"type\":\"pairing_response\", \"status\":\"internal_error_timeout\"}".into())
} }
} else {
Err("{\"type\":\"invalid_syntax\"}".into())
}
} }
fn handle_place_token(root: Value) -> Result<String, String> { fn handle_place_token(root: Value) -> Result<String, String> {
@ -96,6 +97,44 @@ fn handle_disconnect(root: Value) -> Result<String, String> {
Err("{\"type\":\"unimplemented\"}".into()) Err("{\"type\":\"unimplemented\"}".into())
} }
fn handle_game_state(root: Value) -> Result<String, String> { fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
Err("{\"type\":\"unimplemented\"}".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 (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())
}
} }

View file

@ -125,7 +125,7 @@ then the back-end will respond with "too\_many\_players".
"type": "game_state", "type": "game_state",
"status": "not_paired", // or "unknown_id", "cyan_turn", "magenta_turn", "status": "not_paired", // or "unknown_id", "cyan_turn", "magenta_turn",
// "cyan_won", "magenta_won", "draw", // "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" may not be in the response if "unknown_id" is the status
"board": "abcdefg..." // 56-char long string with mapping: "board": "abcdefg..." // 56-char long string with mapping: