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::ai::{get_ai_choice, AIDifficulty};
|
||||||
use crate::state::{board_from_string, new_string_board, string_from_board, BoardState};
|
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 std::{fmt, thread};
|
||||||
|
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
@ -138,8 +143,10 @@ struct DBHandler {
|
||||||
impl DBHandler {
|
impl DBHandler {
|
||||||
/// Returns true if should break out of outer loop
|
/// Returns true if should break out of outer loop
|
||||||
fn handle_request(&mut self) -> bool {
|
fn handle_request(&mut self) -> bool {
|
||||||
let rx_recv_result = self.rx.recv();
|
let rx_recv_result = self.rx.recv_timeout(Duration::from_secs(1));
|
||||||
if let Err(e) = rx_recv_result {
|
if let Err(RecvTimeoutError::Timeout) = rx_recv_result {
|
||||||
|
return false;
|
||||||
|
} else if let Err(e) = rx_recv_result {
|
||||||
println!("Failed to get DBHandlerRequest: {:?}", e);
|
println!("Failed to get DBHandlerRequest: {:?}", e);
|
||||||
self.shutdown_tx.send(()).ok();
|
self.shutdown_tx.send(()).ok();
|
||||||
return false;
|
return false;
|
||||||
|
@ -158,7 +165,8 @@ impl DBHandler {
|
||||||
let create_player_result = self.create_new_player(Some(&conn));
|
let create_player_result = self.create_new_player(Some(&conn));
|
||||||
if let Err(e) = create_player_result {
|
if let Err(e) = create_player_result {
|
||||||
println!("{}", e);
|
println!("{}", e);
|
||||||
return true;
|
// don't stop server because player limit may have been reached
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
let player_id = create_player_result.unwrap();
|
let player_id = create_player_result.unwrap();
|
||||||
|
|
||||||
|
@ -254,7 +262,7 @@ impl DBHandler {
|
||||||
let result = conn.execute(
|
let result = conn.execute(
|
||||||
"
|
"
|
||||||
CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
|
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,
|
game_id INTEGER,
|
||||||
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
|
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,
|
CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
|
||||||
cyan_player INTEGER UNIQUE,
|
cyan_player INTEGER UNIQUE,
|
||||||
magenta_player INTEGER UNIQUE,
|
magenta_player INTEGER UNIQUE,
|
||||||
date_added TEXT NOT NULL,
|
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
board TEXT NOT NULL,
|
board TEXT NOT NULL,
|
||||||
status INTEGER 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(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY(magenta_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 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();
|
let mut player_id: u32 = thread_rng().gen();
|
||||||
loop {
|
loop {
|
||||||
let exists_result = self.check_if_player_exists(Some(conn), player_id);
|
let exists_result = self.check_if_player_exists(Some(conn), player_id);
|
||||||
|
@ -318,10 +339,7 @@ impl DBHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let insert_result = conn.execute(
|
let insert_result = conn.execute("INSERT INTO players (id) VALUES (?);", [player_id]);
|
||||||
"INSERT INTO players (id, date_added) VALUES (?, datetime());",
|
|
||||||
[player_id],
|
|
||||||
);
|
|
||||||
if let Err(e) = insert_result {
|
if let Err(e) = insert_result {
|
||||||
return Err(format!("Failed to insert player into db: {:?}", e));
|
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?)
|
// TODO randomize players (or first-come-first-serve ok to do?)
|
||||||
conn.execute(
|
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()]
|
params![game_id, players[0], players[1], new_string_board()]
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
@ -759,7 +777,7 @@ impl DBHandler {
|
||||||
// update DB
|
// update DB
|
||||||
let update_result = if ended_state_opt.is_none() {
|
let update_result = if ended_state_opt.is_none() {
|
||||||
conn.execute(
|
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 }
|
params![if status == 0 { 1u8 }
|
||||||
else { 0u8 },
|
else { 0u8 },
|
||||||
board_string,
|
board_string,
|
||||||
|
@ -799,6 +817,165 @@ impl DBHandler {
|
||||||
Ok((DBPlaceStatus::Accepted, Some(board_string)))
|
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(
|
pub fn start_db_handler_thread(
|
||||||
|
@ -825,6 +1002,17 @@ pub fn start_db_handler_thread(
|
||||||
handler.shutdown_tx.send(()).ok();
|
handler.shutdown_tx.send(()).ok();
|
||||||
break 'outer;
|
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
|
// fields should be self explanatory for the players table
|
||||||
CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
|
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,
|
game_id INTEGER,
|
||||||
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
|
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,
|
CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
|
||||||
cyan_player INTEGER UNIQUE,
|
cyan_player INTEGER UNIQUE,
|
||||||
magenta_player INTEGER UNIQUE,
|
magenta_player INTEGER UNIQUE,
|
||||||
date_added TEXT NOT NULL,
|
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
board TEXT NOT NULL,
|
board TEXT NOT NULL,
|
||||||
status INTEGER 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(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY(magenta_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_EASY_MAX_CHOICES: usize = 5;
|
||||||
pub const AI_NORMAL_MAX_CHOICES: usize = 3;
|
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