]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
Impl turn timeouts, db entry timeouts
authorStephen Seo <seo.disparate@gmail.com>
Fri, 1 Apr 2022 10:49:50 +0000 (19:49 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Fri, 1 Apr 2022 10:52:46 +0000 (19:52 +0900)
AI takes a players turn if they take too long on their turn (currently
set to 25 seconds).
Backend times out player/game entries in database.

back_end/src/db_handler.rs
backend_database_specification.md
front_end/src/constants.rs

index 8f9007b4191a656d034153875b3df70a718adc39..28baabff5eefc44245cbb5f7df8478b9b4495ab2 100644 (file)
@@ -1,7 +1,12 @@
-use crate::constants::{COLS, ROWS};
-use crate::state::{board_from_string, new_string_board, string_from_board, BoardState};
-
-use std::sync::mpsc::{Receiver, SyncSender};
+use crate::ai::{get_ai_choice, AIDifficulty};
+use crate::constants::{
+    COLS, GAME_CLEANUP_TIMEOUT, PLAYER_CLEANUP_TIMEOUT, PLAYER_COUNT_LIMIT, ROWS, TURN_SECONDS,
+};
+use crate::game_logic::check_win_draw;
+use crate::state::{board_from_string, new_string_board, string_from_board, BoardState, Turn};
+
+use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
+use std::time::Duration;
 use std::{fmt, thread};
 
 use rand::{thread_rng, Rng};
@@ -138,8 +143,10 @@ struct DBHandler {
 impl DBHandler {
     /// Returns true if should break out of outer loop
     fn handle_request(&mut self) -> bool {
-        let rx_recv_result = self.rx.recv();
-        if let Err(e) = rx_recv_result {
+        let rx_recv_result = self.rx.recv_timeout(Duration::from_secs(1));
+        if let Err(RecvTimeoutError::Timeout) = rx_recv_result {
+            return false;
+        } else if let Err(e) = rx_recv_result {
             println!("Failed to get DBHandlerRequest: {:?}", e);
             self.shutdown_tx.send(()).ok();
             return false;
@@ -158,7 +165,8 @@ impl DBHandler {
                 let create_player_result = self.create_new_player(Some(&conn));
                 if let Err(e) = create_player_result {
                     println!("{}", e);
-                    return true;
+                    // don't stop server because player limit may have been reached
+                    return false;
                 }
                 let player_id = create_player_result.unwrap();
 
@@ -254,7 +262,7 @@ impl DBHandler {
             let result = conn.execute(
                 "
                 CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
-                    date_added TEXT NOT NULL,
+                    date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     game_id INTEGER,
                     FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
             ",
@@ -273,9 +281,10 @@ impl DBHandler {
                 CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
                     cyan_player INTEGER UNIQUE,
                     magenta_player INTEGER UNIQUE,
-                    date_added TEXT NOT NULL,
+                    date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     board TEXT NOT NULL,
                     status INTEGER NOT NULL,
+                    turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
                     FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
             ",
@@ -300,6 +309,18 @@ impl DBHandler {
         }
         let conn = conn.unwrap();
 
+        let row_result: Result<usize, _> =
+            conn.query_row("SELECT count(id) FROM players;", [], |row| row.get(0));
+        if let Ok(count) = row_result {
+            if count > PLAYER_COUNT_LIMIT {
+                return Err(String::from(
+                    "Player limit reached, cannot create new players",
+                ));
+            }
+        } else {
+            return Err(String::from("Failed to get player count in db"));
+        }
+
         let mut player_id: u32 = thread_rng().gen();
         loop {
             let exists_result = self.check_if_player_exists(Some(conn), player_id);
@@ -318,10 +339,7 @@ impl DBHandler {
             }
         }
 
-        let insert_result = conn.execute(
-            "INSERT INTO players (id, date_added) VALUES (?, datetime());",
-            [player_id],
-        );
+        let insert_result = conn.execute("INSERT INTO players (id) VALUES (?);", [player_id]);
         if let Err(e) = insert_result {
             return Err(format!("Failed to insert player into db: {:?}", e));
         }
@@ -376,7 +394,7 @@ impl DBHandler {
 
         // 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);",
+            "INSERT INTO games (id, cyan_player, magenta_player, board, status) VALUES (?, ?, ?, ?, 0);",
             params![game_id, players[0], players[1], new_string_board()]
         )
         .map_err(|e| format!("{:?}", e))?;
@@ -759,7 +777,7 @@ impl DBHandler {
         // update DB
         let update_result = if ended_state_opt.is_none() {
             conn.execute(
-                "UPDATE games SET status = ?, board = ? FROM players WHERE players.game_id = games.id AND players.id = ?;",
+                "UPDATE games SET status = ?, board = ?, turn_time_start = datetime() FROM players WHERE players.game_id = games.id AND players.id = ?;",
                 params![if status == 0 { 1u8 }
                             else { 0u8 },
                         board_string,
@@ -799,6 +817,165 @@ impl DBHandler {
             Ok((DBPlaceStatus::Accepted, Some(board_string)))
         }
     }
+
+    fn check_turn_times(&self) -> Result<(), String> {
+        let conn = self.get_conn(DBFirstRun::NotFirstRun)?;
+
+        let mut prepared_stmt = conn
+            .prepare(
+                "SELECT id, status, board FROM games WHERE unixepoch() - unixepoch(turn_time_start) > ? AND cyan_player NOTNULL and magenta_player NOTNULL AND status < 2;",
+            )
+            .map_err(|_| String::from("Failed to prepare db query based on turn time"))?;
+
+        let rows = prepared_stmt
+            .query_map([TURN_SECONDS], |row| {
+                let id_result = row.get(0);
+                let status_result = row.get(1);
+                let board_result = row.get(2);
+                if id_result.is_ok() && status_result.is_ok() && board_result.is_ok() {
+                    if let (Ok(id), Ok(status), Ok(board)) =
+                        (id_result, status_result, board_result)
+                    {
+                        Ok((id, status, board))
+                    } else {
+                        unreachable!();
+                    }
+                } else if id_result.is_err() {
+                    id_result.map(|_| (0, 0, String::new()))
+                } else if status_result.is_err() {
+                    status_result.map(|_| (0, 0, String::new()))
+                } else {
+                    board_result.map(|_| (0, 0, String::new()))
+                }
+            })
+            .map_err(|_| String::from("Failed to query db based on turn time"))?;
+
+        for row_result in rows {
+            if let Ok((id, status, board)) = row_result {
+                self.have_ai_take_players_turn(Some(&conn), id, status, board)?;
+            } else {
+                unreachable!("This part should never execute");
+            }
+        }
+
+        Ok(())
+    }
+
+    fn have_ai_take_players_turn(
+        &self,
+        conn: Option<&Connection>,
+        game_id: u32,
+        status: u32,
+        board_string: String,
+    ) -> Result<(), String> {
+        if status > 1 {
+            return Err(String::from(
+                "have_ai_take_players_turn: got invalid status",
+            ));
+        }
+
+        if conn.is_none() {
+            return self.have_ai_take_players_turn(
+                Some(&self.get_conn(DBFirstRun::NotFirstRun)?),
+                game_id,
+                status,
+                board_string,
+            );
+        }
+        let conn = conn.unwrap();
+
+        let is_cyan = status == 0;
+        let board = board_from_string(board_string);
+        let mut ai_choice_pos: usize = get_ai_choice(
+            AIDifficulty::Hard,
+            if is_cyan {
+                Turn::CyanPlayer
+            } else {
+                Turn::MagentaPlayer
+            },
+            &board,
+        )
+        .map_err(|_| String::from("Failed to get ai choice on turn timeout"))?
+        .into();
+
+        if board[ai_choice_pos].get() != BoardState::Empty {
+            return Err(String::from("ai returned illegal move on turn timeout"));
+        }
+
+        // get final position of token
+        loop {
+            if board.len() <= ai_choice_pos + COLS as usize {
+                break;
+            } else if board[ai_choice_pos + COLS as usize].get() == BoardState::Empty {
+                ai_choice_pos += COLS as usize;
+            } else {
+                break;
+            }
+        }
+
+        // place token
+        board[ai_choice_pos].replace(if is_cyan {
+            BoardState::Cyan
+        } else {
+            BoardState::Magenta
+        });
+
+        // get board string from board while checking if game has ended
+        let (board_string, end_state_opt) = string_from_board(board, ai_choice_pos);
+
+        let state;
+        if let Some(board_state) = end_state_opt {
+            if board_state == BoardState::Empty {
+                state = 4;
+            } else if board_state.from_win() == BoardState::Cyan {
+                state = 2;
+            } else if board_state.from_win() == BoardState::Magenta {
+                state = 3;
+            } else {
+                unreachable!();
+            }
+        } else {
+            state = if is_cyan { 1 } else { 0 };
+        }
+
+        conn.execute(
+            "UPDATE games SET board = ?, status = ?, turn_time_start = datetime() WHERE id = ?;",
+            params![board_string, state, game_id],
+        )
+        .map_err(|_| String::from("Failed to update game with ai choice on turn timeout"))?;
+
+        Ok(())
+    }
+
+    fn cleanup_stale_games(&self, conn: Option<&Connection>) -> Result<(), String> {
+        if conn.is_none() {
+            return self.cleanup_stale_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
+        }
+        let conn = conn.unwrap();
+
+        conn.execute(
+            "DELETE FROM games WHERE unixepoch() - unixepoch(date_added) > ?;",
+            [GAME_CLEANUP_TIMEOUT],
+        )
+        .ok();
+
+        Ok(())
+    }
+
+    fn cleanup_stale_players(&self, conn: Option<&Connection>) -> Result<(), String> {
+        if conn.is_none() {
+            return self.cleanup_stale_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
+        }
+        let conn = conn.unwrap();
+
+        conn.execute(
+            "DELETE FROM players WHERE unixepoch() - unixepoch(date_added) > ? AND game_id ISNULL;",
+            [PLAYER_CLEANUP_TIMEOUT],
+        )
+        .ok();
+
+        Ok(())
+    }
 }
 
 pub fn start_db_handler_thread(
@@ -825,6 +1002,17 @@ pub fn start_db_handler_thread(
                 handler.shutdown_tx.send(()).ok();
                 break 'outer;
             }
+
+            if let Err(e) = handler.check_turn_times() {
+                println!("{}", e);
+            }
+
+            if let Err(e) = handler.cleanup_stale_games(None) {
+                println!("{}", e);
+            }
+            if let Err(e) = handler.cleanup_stale_players(None) {
+                println!("{}", e);
+            }
         }
     });
 }
index d43555605cdeaa4e96545499d2465262c61f4c12..41e340f782430d6dbda28fc01e16c99c97064c09 100644 (file)
@@ -11,7 +11,7 @@ PRAGMA foreign_keys = ON;
 
 // fields should be self explanatory for the players table
 CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
-                      date_added TEXT NOT NULL,
+                      date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                       game_id INTEGER,
                       FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
 
@@ -26,9 +26,10 @@ CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
 CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
                     cyan_player INTEGER UNIQUE,
                     magenta_player INTEGER UNIQUE,
-                    date_added TEXT NOT NULL,
+                    date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     board TEXT NOT NULL,
                     status INTEGER NOT NULL,
+                    turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
                     FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
 ```
index 3b111941df507b3b2f6f410a11e5574baf1e3131..ffcce756a93103b622f176fc0938218b72b51112 100644 (file)
@@ -5,3 +5,8 @@ pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
 
 pub const AI_EASY_MAX_CHOICES: usize = 5;
 pub const AI_NORMAL_MAX_CHOICES: usize = 3;
+
+pub const PLAYER_COUNT_LIMIT: usize = 1000;
+pub const TURN_SECONDS: u64 = 25;
+pub const GAME_CLEANUP_TIMEOUT: u64 = (TURN_SECONDS + 1) * ((ROWS * COLS) as u64 + 5u64);
+pub const PLAYER_CLEANUP_TIMEOUT: u64 = 300;