2022-04-01 10:49:50 +00:00
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 ::state ::{ board_from_string , new_string_board , string_from_board , BoardState , Turn } ;
use std ::sync ::mpsc ::{ Receiver , RecvTimeoutError , SyncSender } ;
use std ::time ::Duration ;
2022-03-30 11:43:49 +00:00
use std ::{ fmt , thread } ;
2022-03-18 10:29:38 +00:00
use rand ::{ thread_rng , Rng } ;
2022-03-30 11:43:49 +00:00
use rusqlite ::{ params , Connection , Error as RusqliteError } ;
2022-03-18 10:29:38 +00:00
2022-03-29 06:02:20 +00:00
pub type GetIDSenderType = ( u32 , Option < bool > ) ;
2022-03-29 08:02:57 +00:00
/// first bool is player exists,
/// second bool is if paired,
/// third bool is if cyan player
pub type CheckPairingType = ( bool , bool , bool ) ;
2022-03-29 06:02:20 +00:00
2022-03-30 11:43:49 +00:00
pub type BoardStateType = ( DBGameState , Option < String > ) ;
2022-03-31 11:38:22 +00:00
pub type PlaceResultType = Result < ( DBPlaceStatus , Option < String > ) , DBPlaceError > ;
2022-03-30 11:43:49 +00:00
#[ 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 ,
}
}
}
2022-03-31 11:38:22 +00:00
#[ derive(Copy, Clone, Debug, PartialEq, Eq) ]
pub enum DBPlaceStatus {
Accepted ,
2022-04-01 08:17:42 +00:00
GameEndedDraw ,
GameEndedCyanWon ,
GameEndedMagentaWon ,
2022-03-31 11:38:22 +00:00
}
impl fmt ::Display for DBPlaceStatus {
fn fmt ( & self , f : & mut fmt ::Formatter < '_ > ) -> fmt ::Result {
match * self {
DBPlaceStatus ::Accepted = > write! ( f , " accepted " ) ,
2022-04-01 08:17:42 +00:00
DBPlaceStatus ::GameEndedDraw = > write! ( f , " game_ended_draw " ) ,
DBPlaceStatus ::GameEndedCyanWon = > write! ( f , " game_ended_cyan_won " ) ,
DBPlaceStatus ::GameEndedMagentaWon = > write! ( f , " game_ended_magenta_won " ) ,
2022-03-31 11:38:22 +00:00
}
}
}
#[ derive(Copy, Clone, Debug, PartialEq, Eq) ]
pub enum DBPlaceError {
NotPairedYet ,
NotYourTurn ,
Illegal ,
OpponentDisconnected ,
UnknownID ,
InternalError ,
}
impl fmt ::Display for DBPlaceError {
fn fmt ( & self , f : & mut fmt ::Formatter < '_ > ) -> fmt ::Result {
match * self {
DBPlaceError ::NotPairedYet = > write! ( f , " not_paired_yet " ) ,
DBPlaceError ::NotYourTurn = > write! ( f , " not_your_turn " ) ,
DBPlaceError ::Illegal = > write! ( f , " illegal " ) ,
DBPlaceError ::OpponentDisconnected = > write! ( f , " opponent_disconnected " ) ,
DBPlaceError ::UnknownID = > write! ( f , " unknown_id " ) ,
DBPlaceError ::InternalError = > write! ( f , " internal_error " ) ,
}
}
}
2022-03-28 07:31:53 +00:00
#[ derive(Clone, Debug) ]
pub enum DBHandlerRequest {
2022-03-29 06:02:20 +00:00
GetID ( SyncSender < GetIDSenderType > ) ,
2022-03-29 08:02:57 +00:00
CheckPairing {
id : u32 ,
response_sender : SyncSender < CheckPairingType > ,
} ,
2022-03-30 11:43:49 +00:00
GetGameState {
id : u32 ,
response_sender : SyncSender < BoardStateType > ,
} ,
2022-03-31 08:38:03 +00:00
DisconnectID {
id : u32 ,
response_sender : SyncSender < bool > ,
} ,
2022-03-31 11:38:22 +00:00
PlaceToken {
id : u32 ,
pos : usize ,
response_sender : SyncSender < PlaceResultType > ,
} ,
2022-03-28 07:31:53 +00:00
}
2022-03-18 10:29:38 +00:00
#[ derive(Copy, Clone, Debug, PartialEq, Eq) ]
enum DBFirstRun {
FirstRun ,
NotFirstRun ,
}
2022-03-29 06:02:20 +00:00
struct DBHandler {
rx : Receiver < DBHandlerRequest > ,
sqlite_path : String ,
shutdown_tx : SyncSender < ( ) > ,
}
impl DBHandler {
/// Returns true if should break out of outer loop
fn handle_request ( & mut self ) -> bool {
2022-04-01 10:49:50 +00:00
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 {
2022-03-29 06:02:20 +00:00
println! ( " Failed to get DBHandlerRequest: {:?} " , e ) ;
self . shutdown_tx . send ( ( ) ) . ok ( ) ;
return false ;
}
let db_request = rx_recv_result . unwrap ( ) ;
match db_request {
DBHandlerRequest ::GetID ( player_tx ) = > {
// got request to create new player, create new player
2022-03-29 06:05:23 +00:00
let conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
2022-03-29 06:02:20 +00:00
if let Err ( e ) = conn_result {
2022-04-01 08:37:37 +00:00
println! ( " {} " , e ) ;
return true ;
2022-03-29 06:02:20 +00:00
}
let conn = conn_result . unwrap ( ) ;
2022-04-01 08:37:37 +00:00
let create_player_result = self . create_new_player ( Some ( & conn ) ) ;
if let Err ( e ) = create_player_result {
println! ( " {} " , e ) ;
2022-04-01 10:49:50 +00:00
// don't stop server because player limit may have been reached
return false ;
2022-03-29 06:02:20 +00:00
}
2022-04-01 08:37:37 +00:00
let player_id = create_player_result . unwrap ( ) ;
2022-03-29 08:02:57 +00:00
let pair_up_result = self . pair_up_players ( Some ( & conn ) ) ;
if let Err ( e ) = pair_up_result {
2022-04-01 08:37:37 +00:00
println! ( " {} " , e ) ;
2022-03-29 08:02:57 +00:00
return true ;
}
// Check if current player has been paired
2022-04-01 08:37:37 +00:00
let paired_check_result = self . check_if_player_is_paired ( Some ( & conn ) , player_id ) ;
if let Err ( e ) = paired_check_result {
println! ( " {} " , e ) ;
return true ;
} else if let Ok ( ( exists , paired , is_cyan ) ) = paired_check_result {
if exists {
if paired {
// don't stop server on send fail, may have timed
// out and dropped the receiver
player_tx . send ( ( player_id , Some ( is_cyan ) ) ) . ok ( ) ;
} else {
// don't stop server on send fail, may have timed
// out and dropped the receiver
player_tx . send ( ( player_id , None ) ) . ok ( ) ;
}
2022-03-29 08:02:57 +00:00
} else {
2022-04-01 08:37:37 +00:00
println! ( " Internal error, created player doesn't exist " ) ;
return true ;
2022-03-29 08:02:57 +00:00
}
2022-04-01 08:37:37 +00:00
} else {
unreachable! ( ) ;
2022-03-29 08:02:57 +00:00
}
}
DBHandlerRequest ::CheckPairing {
id ,
response_sender ,
} = > {
2022-03-30 11:43:49 +00:00
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 ( ) ;
2022-03-29 08:02:57 +00:00
} else {
2022-03-30 11:43:49 +00:00
// 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 ) ;
2022-04-01 08:37:37 +00:00
if let Err ( e ) = get_board_result {
println! ( " {} " , e ) ;
2022-03-30 11:43:49 +00:00
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender . send ( ( DBGameState ::UnknownID , None ) ) . ok ( ) ;
return false ;
2022-03-29 08:02:57 +00:00
}
2022-03-30 11:43:49 +00:00
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender . send ( get_board_result . unwrap ( ) ) . ok ( ) ;
}
2022-03-31 08:38:03 +00:00
DBHandlerRequest ::DisconnectID {
id ,
response_sender ,
} = > {
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender
. send ( self . disconnect_player ( None , id ) . is_ok ( ) )
. ok ( ) ;
}
2022-03-31 11:38:22 +00:00
DBHandlerRequest ::PlaceToken {
id ,
pos ,
response_sender ,
} = > {
let place_result = self . place_token ( None , id , pos ) ;
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender . send ( place_result ) . ok ( ) ;
}
2022-03-29 06:02:20 +00:00
} // match db_request
false
}
2022-03-29 06:05:23 +00:00
fn get_conn ( & self , first_run : DBFirstRun ) -> Result < Connection , String > {
if let Ok ( conn ) = Connection ::open ( & self . sqlite_path ) {
conn . execute ( " PRAGMA foreign_keys = ON; " , [ ] )
. map_err ( | e | format! ( " Should be able to handle \" foreign_keys \" : {:?} " , e ) ) ? ;
let result = conn . execute (
"
CREATE TABLE players ( id INTEGER PRIMARY KEY NOT NULL ,
2022-04-01 10:49:50 +00:00
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ,
2022-03-29 06:05:23 +00:00
game_id INTEGER ,
FOREIGN KEY ( game_id ) REFERENCES games ( id ) ON DELETE CASCADE ) ;
" ,
[ ] ,
) ;
if result . is_ok ( ) {
if first_run = = DBFirstRun ::FirstRun {
println! ( " Created \" players \" table " ) ;
}
} else if first_run = = DBFirstRun ::FirstRun {
println! ( " \" players \" table exists " ) ;
2022-03-18 10:29:38 +00:00
}
2022-03-29 06:05:23 +00:00
let result = conn . execute (
"
CREATE TABLE games ( id INTEGER PRIMARY KEY NOT NULL ,
cyan_player INTEGER UNIQUE ,
magenta_player INTEGER UNIQUE ,
2022-04-01 10:49:50 +00:00
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ,
2022-03-29 06:05:23 +00:00
board TEXT NOT NULL ,
status INTEGER NOT NULL ,
2022-04-01 10:49:50 +00:00
turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ,
2022-03-31 08:38:03 +00:00
FOREIGN KEY ( cyan_player ) REFERENCES players ( id ) ON DELETE SET NULL ,
FOREIGN KEY ( magenta_player ) REFERENCES players ( id ) ON DELETE SET NULL ) ;
2022-03-29 06:05:23 +00:00
" ,
[ ] ,
) ;
if result . is_ok ( ) {
if first_run = = DBFirstRun ::FirstRun {
println! ( " Created \" games \" table " ) ;
}
} else if first_run = = DBFirstRun ::FirstRun {
println! ( " \" games \" table exists " ) ;
2022-03-18 10:29:38 +00:00
}
2022-03-29 06:05:23 +00:00
Ok ( conn )
} else {
Err ( String ::from ( " Failed to open connection " ) )
2022-03-18 10:29:38 +00:00
}
}
2022-03-29 08:02:57 +00:00
2022-04-01 08:37:37 +00:00
fn create_new_player ( & self , conn : Option < & Connection > ) -> Result < u32 , String > {
if conn . is_none ( ) {
return self . create_new_player ( Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) ) ;
}
let conn = conn . unwrap ( ) ;
2022-04-01 10:49:50 +00:00
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 " ) ) ;
}
2022-04-01 08:37:37 +00:00
let mut player_id : u32 = thread_rng ( ) . gen ( ) ;
loop {
let exists_result = self . check_if_player_exists ( Some ( conn ) , player_id ) ;
if let Ok ( exists ) = exists_result {
if exists {
player_id = thread_rng ( ) . gen ( ) ;
} else {
break ;
}
} else {
let error = exists_result . unwrap_err ( ) ;
return Err ( format! (
" Failed to check if player exists in db: {:?} " ,
error
) ) ;
}
}
2022-04-01 10:49:50 +00:00
let insert_result = conn . execute ( " INSERT INTO players (id) VALUES (?); " , [ player_id ] ) ;
2022-04-01 08:37:37 +00:00
if let Err ( e ) = insert_result {
return Err ( format! ( " Failed to insert player into db: {:?} " , e ) ) ;
}
Ok ( player_id )
}
2022-03-29 08:02:57 +00:00
fn pair_up_players ( & self , conn : Option < & Connection > ) -> Result < ( ) , String > {
2022-03-30 11:43:49 +00:00
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 ) ? ;
2022-03-29 08:02:57 +00:00
}
}
2022-03-30 11:43:49 +00:00
Ok ( ( ) )
2022-03-29 08:02:57 +00:00
}
fn create_game ( & self , conn : Option < & Connection > , players : & [ u32 ; 2 ] ) -> Result < u32 , String > {
2022-03-30 11:43:49 +00:00
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 ( ) ;
2022-03-29 08:02:57 +00:00
}
2022-03-30 11:43:49 +00:00
}
2022-03-29 08:02:57 +00:00
2022-03-30 11:43:49 +00:00
// TODO randomize players (or first-come-first-serve ok to do?)
conn . execute (
2022-04-01 10:49:50 +00:00
" INSERT INTO games (id, cyan_player, magenta_player, board, status) VALUES (?, ?, ?, ?, 0); " ,
2022-03-31 12:02:52 +00:00
params! [ game_id , players [ 0 ] , players [ 1 ] , new_string_board ( ) ]
2022-03-30 11:43:49 +00:00
)
. 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 ) ) ? ;
2022-03-29 08:02:57 +00:00
2022-03-30 11:43:49 +00:00
Ok ( game_id )
2022-03-29 08:02:57 +00:00
}
2022-03-30 11:43:49 +00:00
fn check_if_player_is_paired (
& self ,
conn : Option < & Connection > ,
player_id : u32 ,
) -> Result < CheckPairingType , String > {
2022-03-29 08:02:57 +00:00
{
2022-03-29 08:10:12 +00:00
let player_exists_result = self . check_if_player_exists ( None , player_id ) ;
2022-03-29 08:02:57 +00:00
if player_exists_result . is_err ( ) | | ! player_exists_result . unwrap ( ) {
// player doesn't exist
return Ok ( ( false , false , true ) ) ;
}
}
2022-03-30 11:43:49 +00:00
if conn . is_none ( ) {
return self . check_if_player_is_paired (
Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) ,
player_id ,
) ;
}
let conn = conn . unwrap ( ) ;
2022-03-29 08:02:57 +00:00
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 {
if cyan_player = = player_id {
// is cyan player
Ok ( ( true , true , true ) )
} else {
// is magenta player
Ok ( ( true , true , false ) )
}
} else if let Err ( rusqlite ::Error ::QueryReturnedNoRows ) = check_player_row {
2022-03-30 11:43:49 +00:00
// 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 ) )
}
2022-03-29 08:02:57 +00:00
} else if let Err ( e ) = check_player_row {
Err ( format! ( " check_if_player_is_paired: {:?} " , e ) )
} else {
2022-03-30 11:43:49 +00:00
unreachable! ( " All possible Ok and Err results are already checked " ) ;
2022-03-29 08:02:57 +00:00
}
}
2022-03-29 08:10:12 +00:00
fn check_if_player_exists (
& self ,
conn : Option < & Connection > ,
player_id : u32 ,
) -> Result < bool , String > {
2022-03-30 11:43:49 +00:00
if conn . is_none ( ) {
return self
. check_if_player_exists ( Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) , player_id ) ;
}
let conn = conn . unwrap ( ) ;
2022-03-31 11:38:22 +00:00
let check_player_row : Result < u32 , _ > =
2022-03-30 11:43:49 +00:00
conn . query_row ( " SELECT id FROM players WHERE id = ?; " , [ player_id ] , | row | {
2022-03-31 11:38:22 +00:00
row . get ( 0 )
2022-03-30 11:43:49 +00:00
} ) ;
if let Ok ( _id ) = check_player_row {
Ok ( true )
} else {
Ok ( false )
}
}
2022-03-31 11:38:22 +00:00
fn check_if_player_in_game (
& self ,
conn : Option < & Connection > ,
player_id : u32 ,
) -> Result < bool , String > {
if conn . is_none ( ) {
return self . check_if_player_in_game (
Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) ,
player_id ,
) ;
}
let conn = conn . unwrap ( ) ;
let check_player_game_row : Result < u32 , _ > = conn . query_row (
" SELECT games.id FROM games JOIN players WHERE players.id = ? AND players.game_id NOTNULL AND players.game_id = games.id; " ,
[ player_id ] ,
| row | row . get ( 0 ) ) ;
if check_player_game_row . is_ok ( ) {
Ok ( true )
} else {
Ok ( false )
}
}
2022-03-30 11:43:49 +00:00
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
2022-03-31 11:38:22 +00:00
let row_result : Result < ( String , i64 , Option < u32 > , Option < u32 > ) , RusqliteError > = conn . query_row (
" SELECT games.board, games.status, games.cyan_player, games.magenta_player 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 ) ;
let cyan_player = row . get ( 2 ) ;
let magenta_player = row . get ( 3 ) ;
if board_result . is_ok ( ) & & status_result . is_ok ( ) & & cyan_player . is_ok ( ) & & magenta_player . is_ok ( ) {
if let ( Ok ( board ) , Ok ( status ) , Ok ( cyan_id ) , Ok ( magenta_id ) ) = ( board_result , status_result , cyan_player , magenta_player ) {
Ok ( ( board , status , cyan_id , magenta_id ) )
2022-03-31 08:38:03 +00:00
} else {
2022-03-31 11:38:22 +00:00
unreachable! ( " Both row items should be Ok " ) ;
2022-03-30 11:43:49 +00:00
}
2022-03-31 11:38:22 +00:00
} else if board_result . is_err ( ) {
board_result
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None ) )
} else if status_result . is_err ( ) {
status_result
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None ) )
} else if cyan_player . is_err ( ) {
cyan_player
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None ) )
} else {
magenta_player
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None ) )
2022-03-30 11:43:49 +00:00
}
2022-03-31 11:38:22 +00:00
}
) ;
2022-03-31 08:38:03 +00:00
if let Ok ( ( board , status , cyan_opt , magenta_opt ) ) = row_result {
2022-03-30 11:43:49 +00:00
if board . len ( ) ! = ( ROWS * COLS ) as usize {
2022-03-31 08:38:03 +00:00
// board is invalid size
2022-03-30 11:43:49 +00:00
Ok ( ( DBGameState ::InternalError , None ) )
2022-03-31 08:38:03 +00:00
} else if cyan_opt . is_none ( ) | | magenta_opt . is_none ( ) {
// One player disconnected
2022-04-01 08:17:42 +00:00
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
// Remove the game(s) with disconnected players
if self . clear_empty_games ( Some ( conn ) ) . is_err ( ) {
2022-03-31 08:38:03 +00:00
Ok ( ( DBGameState ::InternalError , None ) )
2022-04-01 08:17:42 +00:00
} else if status = = 2 | | status = = 3 {
Ok ( ( DBGameState ::from ( status ) , Some ( board ) ) )
2022-03-31 08:38:03 +00:00
} else {
2022-04-01 08:17:42 +00:00
Ok ( ( DBGameState ::OpponentDisconnected , Some ( board ) ) )
2022-03-31 08:38:03 +00:00
}
2022-03-30 11:43:49 +00:00
} else {
2022-03-31 08:38:03 +00:00
// Game in progress, or other state depending on "status"
2022-03-30 11:43:49 +00:00
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 ) )
2022-03-29 08:10:12 +00:00
} else {
2022-03-30 11:43:49 +00:00
unreachable! ( " either exists or is_paired must be false " ) ;
2022-03-29 08:10:12 +00:00
}
2022-03-29 08:02:57 +00:00
} else {
2022-03-30 11:43:49 +00:00
// TODO use internal error enum instead of string
Err ( String ::from ( " internal_error " ) )
2022-03-29 08:02:57 +00:00
}
}
2022-03-31 08:38:03 +00:00
fn disconnect_player ( & self , conn : Option < & Connection > , player_id : u32 ) -> Result < ( ) , String > {
if conn . is_none ( ) {
return self
. disconnect_player ( Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) , player_id ) ;
}
let conn = conn . unwrap ( ) ;
let stmt_result = conn . execute ( " DELETE FROM players WHERE id = ?; " , [ player_id ] ) ;
if let Ok ( 1 ) = stmt_result {
Ok ( ( ) )
} else {
Err ( String ::from ( " id not found " ) )
}
}
fn clear_empty_games ( & self , conn : Option < & Connection > ) -> Result < ( ) , String > {
if conn . is_none ( ) {
return self . clear_empty_games ( Some ( & self . get_conn ( DBFirstRun ::NotFirstRun ) ? ) ) ;
}
let conn = conn . unwrap ( ) ;
// Only fails if no rows were removed, and that is not an issue
conn . execute (
" DELETE FROM games WHERE cyan_player ISNULL AND magenta_player ISNULL; " ,
[ ] ,
)
. ok ( ) ;
Ok ( ( ) )
}
2022-03-31 11:38:22 +00:00
fn place_token (
& self ,
conn : Option < & Connection > ,
player_id : u32 ,
pos : usize ,
) -> PlaceResultType {
if conn . is_none ( ) {
return self . place_token (
Some (
& self
. get_conn ( DBFirstRun ::NotFirstRun )
. map_err ( | _ | DBPlaceError ::InternalError ) ? ,
) ,
player_id ,
pos ,
) ;
}
let conn = conn . unwrap ( ) ;
// check if player exists
let player_exist_check_result = self . check_if_player_exists ( Some ( conn ) , player_id ) ;
if let Ok ( exists ) = player_exist_check_result {
if ! exists {
return Err ( DBPlaceError ::UnknownID ) ;
}
} else {
return Err ( DBPlaceError ::InternalError ) ;
}
// check if player belongs to a game
let player_game_result = self . check_if_player_in_game ( Some ( conn ) , player_id ) ;
if let Ok ( is_in_game ) = player_game_result {
if ! is_in_game {
return Err ( DBPlaceError ::NotPairedYet ) ;
}
} else {
return Err ( DBPlaceError ::InternalError ) ;
}
// check if player is cyan or magenta
let query_result_result : Result < Result < ( bool , u32 , String ) , DBPlaceError > , _ > =
conn . query_row (
" SELECT cyan_player, magenta_player, status, board FROM games JOIN players WHERE players.id = ? AND players.game_id = games.id; " ,
[ player_id ] ,
| row | {
let cyan_id_result : Result < Option < u32 > , _ > = row . get ( 0 ) ;
let magenta_id_result : Result < Option < u32 > , _ > = row . get ( 1 ) ;
let status_result : Result < u32 , _ > = row . get ( 2 ) ;
let board_result : Result < String , _ > = row . get ( 3 ) ;
if status_result . is_err ( ) {
return status_result . map ( | _ | Ok ( ( false , 0 , " " . into ( ) ) ) ) ;
}
let status : u32 = status_result . unwrap ( ) ;
if board_result . is_err ( ) {
return board_result . map ( | _ | Ok ( ( false , 0 , " " . into ( ) ) ) ) ;
}
let board = board_result . unwrap ( ) ;
if cyan_id_result . is_ok ( ) & & magenta_id_result . is_ok ( ) {
if let ( Ok ( cyan_id_opt ) , Ok ( magenta_id_opt ) ) = ( cyan_id_result , magenta_id_result ) {
2022-03-31 12:02:52 +00:00
if let ( Some ( cyan_id ) , Some ( _magenta_id ) ) = ( cyan_id_opt , magenta_id_opt ) {
2022-03-31 11:38:22 +00:00
Ok ( Ok ( ( cyan_id = = player_id , status , board ) ) )
2022-04-01 08:17:42 +00:00
} else if ( 2 ..= 4 ) . contains ( & status ) {
// game has ended, don't return error
// first result will be safely ignored
Ok ( Ok ( ( false , status , board ) ) )
2022-03-31 11:38:22 +00:00
} else {
Ok ( Err ( DBPlaceError ::OpponentDisconnected ) )
}
} else {
unreachable! ( " both row items should be Ok " )
}
} else if cyan_id_result . is_err ( ) {
cyan_id_result . map ( | _ | Err ( DBPlaceError ::InternalError ) )
} else {
magenta_id_result . map ( | _ | Err ( DBPlaceError ::InternalError ) )
}
} ) ;
let query_result = query_result_result . map_err ( | _ | DBPlaceError ::InternalError ) ? ;
// if opponent has disconnected, disconnect the remaining player as well
if let Err ( DBPlaceError ::OpponentDisconnected ) = query_result {
2022-04-01 08:17:42 +00:00
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
if self . clear_empty_games ( Some ( conn ) ) . is_err ( ) {
2022-03-31 11:38:22 +00:00
return Err ( DBPlaceError ::InternalError ) ;
}
}
let ( is_cyan , status , board_string ) = query_result ? ;
match status {
0 = > {
// cyan's turn
if ! is_cyan {
return Err ( DBPlaceError ::NotYourTurn ) ;
}
}
1 = > {
// magenta's turn
if is_cyan {
return Err ( DBPlaceError ::NotYourTurn ) ;
}
}
2022-04-01 08:17:42 +00:00
2 = > {
// game over, cyan won
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
if self . clear_empty_games ( Some ( conn ) ) . is_err ( ) {
return Err ( DBPlaceError ::InternalError ) ;
}
return Ok ( ( DBPlaceStatus ::GameEndedCyanWon , Some ( board_string ) ) ) ;
}
3 = > {
// game over, magenta won
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
if self . clear_empty_games ( Some ( conn ) ) . is_err ( ) {
return Err ( DBPlaceError ::InternalError ) ;
}
return Ok ( ( DBPlaceStatus ::GameEndedMagentaWon , Some ( board_string ) ) ) ;
}
4 = > {
// game over, draw
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
if self . clear_empty_games ( Some ( conn ) ) . is_err ( ) {
return Err ( DBPlaceError ::InternalError ) ;
}
return Ok ( ( DBPlaceStatus ::GameEndedDraw , Some ( board_string ) ) ) ;
2022-03-31 11:38:22 +00:00
}
_ = > ( ) ,
}
// get board state
let board = board_from_string ( board_string ) ;
// find placement position or return "illegal move" if unable to
let mut final_pos = pos ;
loop {
if board [ final_pos ] . get ( ) = = BoardState ::Empty {
if final_pos + COLS as usize > = board . len ( )
| | board [ final_pos + COLS as usize ] . get ( ) ! = BoardState ::Empty
{
break ;
} else if board [ final_pos + COLS as usize ] . get ( ) = = BoardState ::Empty {
final_pos + = COLS as usize ;
}
} else {
return Err ( DBPlaceError ::Illegal ) ;
}
}
// place into board
if is_cyan {
board [ final_pos ] . replace ( BoardState ::Cyan ) ;
} else {
board [ final_pos ] . replace ( BoardState ::Magenta ) ;
}
// board back to string
2022-04-01 08:17:42 +00:00
let ( board_string , ended_state_opt ) = string_from_board ( board , final_pos ) ;
2022-03-31 11:38:22 +00:00
// update DB
2022-04-01 08:17:42 +00:00
let update_result = if ended_state_opt . is_none ( ) {
conn . execute (
2022-04-01 10:49:50 +00:00
" UPDATE games SET status = ?, board = ?, turn_time_start = datetime() FROM players WHERE players.game_id = games.id AND players.id = ?; " ,
2022-04-01 08:17:42 +00:00
params! [ if status = = 0 { 1 u8 }
else { 0 u8 } ,
board_string ,
player_id ]
)
} else {
conn . execute (
" UPDATE games SET status = ?, board = ? FROM players WHERE players.game_id = games.id AND players.id = ?; " ,
params! [ if ended_state_opt . unwrap ( ) = = BoardState ::Empty { 4 u8 }
else if ended_state_opt . unwrap ( ) = = BoardState ::CyanWin { 2 u8 }
else { 3 u8 } ,
board_string ,
player_id ]
)
} ;
2022-03-31 11:38:22 +00:00
if let Err ( _e ) = update_result {
return Err ( DBPlaceError ::InternalError ) ;
} else if let Ok ( count ) = update_result {
if count ! = 1 {
return Err ( DBPlaceError ::InternalError ) ;
}
}
2022-04-01 08:17:42 +00:00
if let Some ( ended_state ) = ended_state_opt {
2022-03-31 11:38:22 +00:00
self . disconnect_player ( Some ( conn ) , player_id ) . ok ( ) ;
2022-04-01 08:17:42 +00:00
Ok ( (
match ended_state {
BoardState ::Empty = > DBPlaceStatus ::GameEndedDraw ,
BoardState ::Cyan | BoardState ::Magenta = > unreachable! ( ) ,
BoardState ::CyanWin = > DBPlaceStatus ::GameEndedCyanWon ,
BoardState ::MagentaWin = > DBPlaceStatus ::GameEndedMagentaWon ,
} ,
Some ( board_string ) ,
) )
2022-03-31 11:38:22 +00:00
} else {
Ok ( ( DBPlaceStatus ::Accepted , Some ( board_string ) ) )
}
}
2022-04-01 10:49:50 +00:00
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 ( ( ) )
}
2022-03-18 10:29:38 +00:00
}
2022-03-18 14:43:15 +00:00
pub fn start_db_handler_thread (
2022-03-28 07:31:53 +00:00
rx : Receiver < DBHandlerRequest > ,
2022-03-18 14:43:15 +00:00
sqlite_path : String ,
shutdown_tx : SyncSender < ( ) > ,
) {
2022-03-29 06:02:20 +00:00
let mut handler = DBHandler {
rx ,
sqlite_path ,
shutdown_tx ,
} ;
2022-03-18 10:29:38 +00:00
thread ::spawn ( move | | {
// temporarily get conn which should initialize on first setup of db
2022-03-29 06:05:23 +00:00
if let Ok ( _conn ) = handler . get_conn ( DBFirstRun ::FirstRun ) {
2022-03-18 10:29:38 +00:00
} else {
println! ( " ERROR: Failed init sqlite db connection " ) ;
2022-03-29 06:02:20 +00:00
handler . shutdown_tx . send ( ( ) ) . ok ( ) ;
2022-03-18 10:29:38 +00:00
return ;
}
2022-03-18 14:43:15 +00:00
' outer : loop {
2022-03-29 06:02:20 +00:00
if handler . handle_request ( ) {
handler . shutdown_tx . send ( ( ) ) . ok ( ) ;
break 'outer ;
2022-03-18 10:29:38 +00:00
}
2022-04-01 10:49:50 +00:00
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 ) ;
}
2022-03-29 06:02:20 +00:00
}
2022-03-18 10:29:38 +00:00
} ) ;
}