]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
backend: refactorings, impl "board_state" query
authorStephen Seo <seo.disparate@gmail.com>
Wed, 30 Mar 2022 11:43:49 +0000 (20:43 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Wed, 30 Mar 2022 11:44:18 +0000 (20:44 +0900)
Implemented the fifth backend protocol request/response ("board_state").
Some refactorings involve improving readability from handling unwrapping Option
of &Connection objects.

back_end/src/db_handler.rs
back_end/src/json_handlers.rs
backend_protocol_specification.md

index f95dcddaa0e67f92a15e3e69c65aac523160dbb1..9d2c9aba0a4ab727feb2ee883624b2cd1ae51295 100644 (file)
@@ -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<bool>);
 /// first bool is player exists,
@@ -10,6 +12,50 @@ pub type GetIDSenderType = (u32, Option<bool>);
 /// 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>),
@@ -17,6 +63,10 @@ pub enum DBHandlerRequest {
         id: u32,
         response_sender: SyncSender<CheckPairingType>,
     },
+    GetGameState {
+        id: u32,
+        response_sender: SyncSender<BoardStateType>,
+    },
 }
 
 #[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::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
@@ -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<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() {
@@ -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));
         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<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"))
         }
     }
 }
index 9872e62cc382560622c1ccb5dbb808b82affe642..b69168b20873c5c8a34ae5ed9382f90c2624181c 100644 (file)
@@ -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<DBHandlerRequest>) -> Result<String, St
 }
 
 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())
     }
 }
 
@@ -96,6 +97,44 @@ fn handle_disconnect(root: Value) -> Result<String, String> {
     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())
+    }
 }
index a008bc9f109a0e35623dae957b49d92572c46aa4..86ea079e735bb3cc5118a9d00e9eee7f9388559c 100644 (file)
@@ -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: