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:
parent
d851d90640
commit
c8eb6ab5be
3 changed files with 210 additions and 16 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
```
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue