Impl turn timeouts, db entry timeouts

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.
This commit is contained in:
Stephen Seo 2022-04-01 19:49:50 +09:00
parent d851d90640
commit c8eb6ab5be
3 changed files with 210 additions and 16 deletions

View File

@ -1,7 +1,12 @@
use crate::constants::{COLS, ROWS};
use crate::state::{board_from_string, new_string_board, string_from_board, BoardState};
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, SyncSender};
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);
}
}
});
}

View 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);
```

View 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;