2022-04-06 11:49:54 +00:00
//Four Line Dropper Backend - A server enabling networked multiplayer for Four Line Dropper
//Copyright (C) 2022 Stephen Seo
//
//This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
2022-04-01 10:49:50 +00:00
use crate ::ai ::{ get_ai_choice , AIDifficulty } ;
use crate ::constants ::{
2022-04-20 03:21:02 +00:00
BACKEND_CLEANUP_INTERVAL_SECONDS , COLS , GAME_CLEANUP_TIMEOUT , PLAYER_CLEANUP_TIMEOUT ,
PLAYER_COUNT_LIMIT , ROWS , TURN_SECONDS ,
2022-04-01 10:49:50 +00:00
} ;
2022-04-29 09:30:41 +00:00
use crate ::state ::{
board_from_string , new_string_board , string_from_board , BoardState , EmoteEnum , Turn ,
} ;
2022-04-01 10:49:50 +00:00
2022-04-27 02:42:28 +00:00
use std ::collections ::HashMap ;
2022-04-01 10:49:50 +00:00
use std ::sync ::mpsc ::{ Receiver , RecvTimeoutError , SyncSender } ;
2022-04-20 03:21:02 +00:00
use std ::time ::{ Duration , Instant } ;
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-04-08 02:01:33 +00:00
/// first value is ID, None if too many players
/// second value is true if player is cyan_player, None if not paired yet
pub type GetIDSenderType = ( Option < 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-04-30 07:27:43 +00:00
/// second String is board string, third String is date updated, fourth value
/// is EmoteEnum
pub type BoardStateType = (
DBGameState ,
Option < String > ,
Option < String > ,
Option < EmoteEnum > ,
) ;
2022-03-30 11:43:49 +00:00
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-04-27 02:42:28 +00:00
GetID {
response_sender : SyncSender < GetIDSenderType > ,
phrase : Option < String > ,
} ,
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-04-29 08:16:32 +00:00
SendEmote {
id : u32 ,
emote_type : EmoteEnum ,
response_sender : SyncSender < Result < ( ) , ( ) > > ,
} ,
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 {
2022-04-27 02:42:28 +00:00
DBHandlerRequest ::GetID {
response_sender ,
phrase ,
} = > {
2022-03-29 06:02:20 +00:00
// 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
2022-04-27 02:42:28 +00:00
let create_player_result = self . create_new_player ( Some ( & conn ) , phrase ) ;
2022-04-01 08:37:37 +00:00
if let Err ( e ) = create_player_result {
println! ( " {} " , e ) ;
2022-04-27 02:42:28 +00:00
response_sender . send ( ( None , None ) ) . ok ( ) ;
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
2022-04-27 02:42:28 +00:00
response_sender . send ( ( Some ( player_id ) , Some ( is_cyan ) ) ) . ok ( ) ;
2022-04-01 08:37:37 +00:00
} else {
// don't stop server on send fail, may have timed
// out and dropped the receiver
2022-04-27 02:42:28 +00:00
response_sender . send ( ( Some ( player_id ) , None ) ) . ok ( ) ;
2022-04-01 08:37:37 +00:00
}
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
2022-04-29 08:16:32 +00:00
response_sender
2022-04-30 07:27:43 +00:00
. send ( ( DBGameState ::UnknownID , None , None , None ) )
2022-04-29 08:16:32 +00:00
. ok ( ) ;
2022-03-30 11:43:49 +00:00
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-04-29 08:16:32 +00:00
DBHandlerRequest ::SendEmote {
id ,
emote_type ,
response_sender ,
} = > {
let result = self . create_new_sent_emote ( None , id , emote_type ) ;
if let Err ( error_string ) = result {
println! ( " {} " , error_string ) ;
// don't stop server on send fail, may have timed
// out and dropped the receiver
response_sender . send ( Err ( ( ) ) ) . ok ( ) ;
} else {
// don't stop server on send fail, may have timed
// out and dropped the receiver
response_sender . send ( Ok ( ( ) ) ) . 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 ,
2022-04-27 02:15:53 +00:00
phrase TEXT ,
2022-03-29 06:05:23 +00:00
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-04-27 02:15:53 +00:00
if let Err ( e ) = self . db_check_migration ( & conn ) {
println! ( " {} " , e ) ;
}
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-04-29 08:16:32 +00:00
let result = conn . execute (
"
CREATE TABLE emotes ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
type TEXT NOT NULL ,
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ,
receiver_id INTEGER NOT NULL ,
FOREIGN KEY ( receiver_id ) REFERENCES players ( id ) ON DELETE CASCADE ) ;
" ,
[ ] ,
) ;
if result . is_ok ( ) {
if first_run = = DBFirstRun ::FirstRun {
println! ( " Created \" emotes \" table " ) ;
}
} else if first_run = = DBFirstRun ::FirstRun {
println! ( " \" emotes \" table exists " ) ;
}
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-27 02:15:53 +00:00
fn db_check_migration ( & self , conn : & Connection ) -> Result < ( ) , String > {
let mut table_entries_stmt = conn
. prepare ( " PRAGMA table_info(players); " )
. map_err ( | e | format! ( " {:?} " , e ) ) ? ;
let mut table_entries_rows = table_entries_stmt
. query ( [ ] )
. map_err ( | e | format! ( " {:?} " , e ) ) ? ;
// check if "phrase" column exists
let mut phrase_exists = false ;
while let Some ( row ) = table_entries_rows . next ( ) . map_err ( | e | format! ( " {:?} " , e ) ) ? {
let column_name : String = row . get ( 1 ) . map_err ( | e | format! ( " {:?} " , e ) ) ? ;
if column_name . contains ( " phrase " ) {
phrase_exists = true ;
}
}
if ! phrase_exists {
conn . execute ( " ALTER TABLE players ADD COLUMN phrase TEXT; " , [ ] )
. map_err ( | e | format! ( " {:?} " , e ) ) ? ;
println! ( " Added \" phrase \" column to \" players \" in db. " ) ;
}
Ok ( ( ) )
}
2022-04-27 02:42:28 +00:00
fn create_new_player (
& self ,
conn : Option < & Connection > ,
phrase : Option < String > ,
) -> Result < u32 , String > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-04-01 08:37:37 +00:00
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-27 02:42:28 +00:00
let insert_result = conn . execute (
" INSERT INTO players (id, phrase) VALUES (?, ?); " ,
params! [ player_id , phrase ] ,
) ;
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-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-30 11:43:49 +00:00
let mut to_pair : Option < u32 > = None ;
let mut unpaired_players_stmt = conn
2022-04-27 02:42:28 +00:00
. prepare ( " SELECT id, phrase FROM players WHERE game_id ISNULL ORDER BY date_added; " )
2022-03-30 11:43:49 +00:00
. map_err ( | e | format! ( " {:?} " , e ) ) ? ;
let mut unpaired_players_rows = unpaired_players_stmt
. query ( [ ] )
. map_err ( | e | format! ( " {:?} " , e ) ) ? ;
2022-04-27 02:42:28 +00:00
let mut phrase_map : HashMap < String , u32 > = HashMap ::new ( ) ;
2022-03-30 11:43:49 +00:00
while let Some ( row ) = unpaired_players_rows
. next ( )
. map_err ( | e | format! ( " {:?} " , e ) ) ?
{
2022-04-27 02:42:28 +00:00
if let Ok ( phrase_text ) = row . get ::< usize , String > ( 1 ) {
// pair players with matching phrases
if let Some ( matching_player_id ) = phrase_map . get ( & phrase_text ) {
let players : [ u32 ; 2 ] = [
* matching_player_id ,
row . get ( 0 ) . map_err ( | e | format! ( " {:?} " , e ) ) ? ,
] ;
self . create_game ( Some ( conn ) , & players ) ? ;
phrase_map . remove ( & phrase_text ) ;
} else {
phrase_map . insert ( phrase_text , row . get ( 0 ) . map_err ( | e | format! ( " {:?} " , e ) ) ? ) ;
}
2022-03-30 11:43:49 +00:00
} else {
2022-04-27 02:42:28 +00:00
// pair players that did not use a phrase
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-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-30 11:43:49 +00:00
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-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . 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-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . 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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-31 11:38:22 +00:00
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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-30 11:43:49 +00:00
2022-04-29 08:16:32 +00:00
let mut received_emote : Option < EmoteEnum > = None ;
{
let row_result : Result < ( u64 , String ) , RusqliteError > = conn . query_row (
" SELECT id, type FROM emotes WHERE receiver_id = ? ORDER BY date_added ASC; " ,
[ player_id ] ,
| row | {
Ok ( (
row . get ( 0 ) . expect ( " emotes.id should exist " ) ,
row . get ( 1 ) . expect ( " emotes.type should exist " ) ,
) )
} ,
) ;
if let Err ( RusqliteError ::QueryReturnedNoRows ) = row_result {
// no-op
} else if let Err ( e ) = row_result {
println! ( " Error while fetching received emotes: {:?} " , e ) ;
} else {
let ( emote_id , emote_type ) = row_result . unwrap ( ) ;
received_emote = emote_type . as_str ( ) . try_into ( ) . ok ( ) ;
if received_emote . is_none ( ) {
println! ( " WARNING: Invalid emote type \" {} \" in db " , emote_type ) ;
}
conn . execute ( " DELETE FROM emotes WHERE id = ?; " , [ emote_id ] )
. ok ( ) ;
}
}
2022-03-30 11:43:49 +00:00
// TODO maybe handle "opponent_disconnected" case
2022-05-03 04:17:54 +00:00
type ResultTuple = ( String , i64 , Option < u32 > , Option < u32 > , String ) ;
let row_result : Result < ResultTuple , RusqliteError > = conn . query_row (
2022-04-30 07:27:43 +00:00
" SELECT games.board, games.status, games.cyan_player, games.magenta_player, games.turn_time_start FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id; " ,
2022-03-31 11:38:22 +00:00
[ 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 ) ;
2022-04-30 07:27:43 +00:00
let updated_time = row . get ( 4 ) ;
if board_result . is_ok ( ) & & status_result . is_ok ( ) & & cyan_player . is_ok ( ) & & magenta_player . is_ok ( ) & & updated_time . is_ok ( ) {
if let ( Ok ( board ) , Ok ( status ) , Ok ( cyan_id ) , Ok ( magenta_id ) , Ok ( updated_time ) ) = ( board_result , status_result , cyan_player , magenta_player , updated_time ) {
Ok ( ( board , status , cyan_id , magenta_id , updated_time ) )
2022-03-31 08:38:03 +00:00
} else {
2022-04-30 07:27:43 +00:00
unreachable! ( " All 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
2022-04-30 07:27:43 +00:00
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None , String ::new ( ) ) )
2022-03-31 11:38:22 +00:00
} else if status_result . is_err ( ) {
status_result
2022-04-30 07:27:43 +00:00
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None , String ::new ( ) ) )
2022-03-31 11:38:22 +00:00
} else if cyan_player . is_err ( ) {
cyan_player
2022-04-30 07:27:43 +00:00
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None , String ::new ( ) ) )
} else if magenta_player . is_err ( ) {
2022-03-31 11:38:22 +00:00
magenta_player
2022-04-30 07:27:43 +00:00
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None , String ::new ( ) ) )
} else {
updated_time
. map ( | _ | ( String ::from ( " this value should never be returned " ) , 0 , None , None , String ::new ( ) ) )
2022-03-30 11:43:49 +00:00
}
2022-03-31 11:38:22 +00:00
}
) ;
2022-04-30 07:27:43 +00:00
if let Ok ( ( board , status , cyan_opt , magenta_opt , updated_time ) ) = 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-04-30 07:27:43 +00:00
Ok ( (
DBGameState ::InternalError ,
None ,
Some ( updated_time ) ,
received_emote ,
) )
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-04-30 07:27:43 +00:00
Ok ( (
DBGameState ::InternalError ,
None ,
Some ( updated_time ) ,
received_emote ,
) )
2022-04-01 08:17:42 +00:00
} else if status = = 2 | | status = = 3 {
2022-04-30 07:27:43 +00:00
Ok ( (
DBGameState ::from ( status ) ,
Some ( board ) ,
Some ( updated_time ) ,
received_emote ,
) )
2022-03-31 08:38:03 +00:00
} else {
2022-04-29 08:16:32 +00:00
Ok ( (
DBGameState ::OpponentDisconnected ,
Some ( board ) ,
2022-04-30 07:27:43 +00:00
Some ( updated_time ) ,
2022-04-29 08:16:32 +00:00
received_emote ,
) )
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-04-30 07:27:43 +00:00
Ok ( (
DBGameState ::from ( status ) ,
Some ( board ) ,
Some ( updated_time ) ,
received_emote ,
) )
2022-03-30 11:43:49 +00:00
}
} 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 {
2022-04-30 07:27:43 +00:00
Ok ( ( DBGameState ::UnknownID , None , None , received_emote ) )
2022-03-30 11:43:49 +00:00
} else if ! is_paired {
2022-04-30 07:27:43 +00:00
Ok ( ( DBGameState ::NotPaired , None , None , received_emote ) )
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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-31 08:38:03 +00:00
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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-31 08:38:03 +00:00
// 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 {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-03-31 11:38:22 +00:00
// 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
return Ok ( ( DBPlaceStatus ::GameEndedCyanWon , Some ( board_string ) ) ) ;
}
3 = > {
// game over, magenta won
return Ok ( ( DBPlaceStatus ::GameEndedMagentaWon , Some ( board_string ) ) ) ;
}
4 = > {
// game over, draw
return Ok ( ( DBPlaceStatus ::GameEndedDraw , Some ( board_string ) ) ) ;
2022-03-31 11:38:22 +00:00
}
_ = > ( ) ,
}
// get board state
2022-05-04 05:55:01 +00:00
let board = board_from_string ( & board_string ) ;
2022-03-31 11:38:22 +00:00
// 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-29 06:21:22 +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 {
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 {
2022-05-05 03:35:30 +00:00
self . have_ai_take_players_turn ( Some ( & conn ) , id , status , & board ) ? ;
2022-04-01 10:49:50 +00:00
} else {
unreachable! ( " This part should never execute " ) ;
}
}
Ok ( ( ) )
}
fn have_ai_take_players_turn (
& self ,
conn : Option < & Connection > ,
game_id : u32 ,
status : u32 ,
2022-05-05 03:35:30 +00:00
board_string : & str ,
2022-04-01 10:49:50 +00:00
) -> Result < ( ) , String > {
if status > 1 {
return Err ( String ::from (
" have_ai_take_players_turn: got invalid status " ,
) ) ;
}
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-04-01 10:49:50 +00:00
let is_cyan = status = = 0 ;
2022-05-05 03:35:30 +00:00
let board = board_from_string ( board_string ) ;
2022-04-01 10:49:50 +00:00
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
2022-04-29 06:21:22 +00:00
let ( board_string , end_state_opt ) = string_from_board ( & board , ai_choice_pos ) ;
2022-04-01 10:49:50 +00:00
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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-04-01 10:49:50 +00:00
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 > {
2022-04-20 03:21:02 +00:00
let mut _conn_result = Err ( String ::new ( ) ) ;
2022-04-27 07:51:57 +00:00
let conn = if let Some ( c ) = conn {
c
} else {
2022-04-20 03:21:02 +00:00
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-04-01 10:49:50 +00:00
conn . execute (
" DELETE FROM players WHERE unixepoch() - unixepoch(date_added) > ? AND game_id ISNULL; " ,
[ PLAYER_CLEANUP_TIMEOUT ] ,
)
. ok ( ) ;
Ok ( ( ) )
}
2022-04-29 08:16:32 +00:00
fn cleanup_stale_emotes ( & self , conn : Option < & Connection > ) -> Result < ( ) , String > {
let mut _conn_result = Err ( String ::new ( ) ) ;
let conn = if let Some ( c ) = conn {
c
} else {
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
conn . execute (
" DELETE FROM emotes WHERE unixepoch() - unixepoch(date_added) > ?; " ,
[ GAME_CLEANUP_TIMEOUT ] ,
)
. ok ( ) ;
Ok ( ( ) )
}
fn create_new_sent_emote (
& self ,
conn : Option < & Connection > ,
sender_id : u32 ,
emote : EmoteEnum ,
) -> Result < ( ) , String > {
let mut _conn_result = Err ( String ::new ( ) ) ;
let conn = if let Some ( c ) = conn {
c
} else {
_conn_result = self . get_conn ( DBFirstRun ::NotFirstRun ) ;
_conn_result . as_ref ( ) . unwrap ( )
} ;
2022-04-29 09:30:41 +00:00
let mut prepared_stmt = conn . prepare ( " SELECT games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id; " )
2022-04-29 08:16:32 +00:00
. map_err ( | _ | String ::from ( " Failed to prepare db query for getting opponent id for sending emote " ) ) ? ;
let row_result : Result < ( Option < u32 > , Option < u32 > ) , RusqliteError > =
prepared_stmt . query_row ( [ sender_id ] , | row | Ok ( ( row . get ( 0 ) . ok ( ) , row . get ( 1 ) . ok ( ) ) ) ) ;
if let Err ( RusqliteError ::QueryReturnedNoRows ) = row_result {
return Err ( String ::from ( " Failed to send emote, game doesn't exist " ) ) ;
} else if let Err ( e ) = row_result {
return Err ( format! ( " Failed to send emote: {:?} " , e ) ) ;
}
let ( cyan_player_opt , magenta_player_opt ) = row_result . unwrap ( ) ;
if cyan_player_opt . is_none ( ) {
return Err ( String ::from (
" Failed to send emote, cyan player disconnected " ,
) ) ;
} else if magenta_player_opt . is_none ( ) {
return Err ( String ::from (
" Failed to send emote, magenta player disconnected " ,
) ) ;
}
let cyan_player_id = cyan_player_opt . unwrap ( ) ;
let magenta_player_id = magenta_player_opt . unwrap ( ) ;
let receiver_id = if cyan_player_id = = sender_id {
magenta_player_id
} else {
cyan_player_id
} ;
conn . execute (
" INSERT INTO emotes (type, receiver_id) VALUES (?, ?); " ,
params! [ String ::from ( emote ) , receiver_id ] ,
)
. map_err ( | _ | {
format! (
" Failed to store emote from player {} to player {} " ,
sender_id , receiver_id
)
} ) ? ;
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-04-20 03:21:02 +00:00
let mut cleanup_instant = Instant ::now ( ) ;
let cleanup_duration = Duration ::from_secs ( BACKEND_CLEANUP_INTERVAL_SECONDS ) ;
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 ) ;
}
2022-04-20 03:21:02 +00:00
if cleanup_instant . elapsed ( ) > cleanup_duration {
2022-05-05 03:35:30 +00:00
let conn = handler . get_conn ( DBFirstRun ::NotFirstRun ) . ok ( ) ;
2022-04-20 03:21:02 +00:00
cleanup_instant = Instant ::now ( ) ;
2022-05-05 03:35:30 +00:00
if let Err ( e ) = handler . cleanup_stale_games ( conn . as_ref ( ) ) {
2022-04-20 03:21:02 +00:00
println! ( " {} " , e ) ;
}
2022-05-05 03:35:30 +00:00
if let Err ( e ) = handler . cleanup_stale_players ( conn . as_ref ( ) ) {
2022-04-20 03:21:02 +00:00
println! ( " {} " , e ) ;
}
2022-05-05 03:35:30 +00:00
if let Err ( e ) = handler . cleanup_stale_emotes ( conn . as_ref ( ) ) {
2022-04-29 08:16:32 +00:00
println! ( " {} " , e ) ;
}
2022-05-05 03:35:30 +00:00
if let Err ( e ) = handler . clear_empty_games ( conn . as_ref ( ) ) {
2022-04-29 08:16:32 +00:00
println! ( " {} " , e ) ;
}
2022-04-01 10:49:50 +00:00
}
2022-03-29 06:02:20 +00:00
}
2022-03-18 10:29:38 +00:00
} ) ;
}