Stephen Seo
0d9e35dd28
Protocol was also updated to match the output from the backend. Fixed "id" not returning as an integer from the backend.
811 lines
30 KiB
Rust
811 lines
30 KiB
Rust
use crate::constants::{COLS, ROWS};
|
|
use crate::state::{board_from_string, new_string_board, string_from_board, BoardState};
|
|
|
|
use std::sync::mpsc::{Receiver, SyncSender};
|
|
use std::{fmt, thread};
|
|
|
|
use rand::{thread_rng, Rng};
|
|
use rusqlite::{params, Connection, Error as RusqliteError};
|
|
|
|
pub type GetIDSenderType = (u32, Option<bool>);
|
|
/// first bool is player exists,
|
|
/// second bool is if paired,
|
|
/// third bool is if cyan player
|
|
pub type CheckPairingType = (bool, bool, bool);
|
|
|
|
pub type BoardStateType = (DBGameState, Option<String>);
|
|
|
|
pub type PlaceResultType = Result<(DBPlaceStatus, Option<String>), DBPlaceError>;
|
|
|
|
// TODO use Error types instead of Strings for Result Errs
|
|
|
|
#[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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub enum DBPlaceStatus {
|
|
Accepted,
|
|
GameEndedDraw,
|
|
GameEndedCyanWon,
|
|
GameEndedMagentaWon,
|
|
}
|
|
|
|
impl fmt::Display for DBPlaceStatus {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match *self {
|
|
DBPlaceStatus::Accepted => write!(f, "accepted"),
|
|
DBPlaceStatus::GameEndedDraw => write!(f, "game_ended_draw"),
|
|
DBPlaceStatus::GameEndedCyanWon => write!(f, "game_ended_cyan_won"),
|
|
DBPlaceStatus::GameEndedMagentaWon => write!(f, "game_ended_magenta_won"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum DBHandlerRequest {
|
|
GetID(SyncSender<GetIDSenderType>),
|
|
CheckPairing {
|
|
id: u32,
|
|
response_sender: SyncSender<CheckPairingType>,
|
|
},
|
|
GetGameState {
|
|
id: u32,
|
|
response_sender: SyncSender<BoardStateType>,
|
|
},
|
|
DisconnectID {
|
|
id: u32,
|
|
response_sender: SyncSender<bool>,
|
|
},
|
|
PlaceToken {
|
|
id: u32,
|
|
pos: usize,
|
|
response_sender: SyncSender<PlaceResultType>,
|
|
},
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
enum DBFirstRun {
|
|
FirstRun,
|
|
NotFirstRun,
|
|
}
|
|
|
|
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 {
|
|
let rx_recv_result = self.rx.recv();
|
|
if let Err(e) = rx_recv_result {
|
|
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
|
|
let mut player_id: u32 = thread_rng().gen();
|
|
let conn_result = self.get_conn(DBFirstRun::NotFirstRun);
|
|
if let Err(e) = conn_result {
|
|
println!("Failed to get sqlite db connection: {:?}", e);
|
|
self.shutdown_tx.send(()).ok();
|
|
return false;
|
|
}
|
|
let conn = conn_result.unwrap();
|
|
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();
|
|
println!("Failed to check if player exists in db: {:?}", error);
|
|
self.shutdown_tx.send(()).ok();
|
|
return true;
|
|
}
|
|
}
|
|
let insert_result = conn.execute(
|
|
"INSERT INTO players (id, date_added) VALUES (?, datetime());",
|
|
[player_id],
|
|
);
|
|
if let Err(e) = insert_result {
|
|
println!("Failed to insert into sqlite db: {:?}", e);
|
|
self.shutdown_tx.send(()).ok();
|
|
return true;
|
|
}
|
|
|
|
let pair_up_result = self.pair_up_players(Some(&conn));
|
|
if let Err(e) = pair_up_result {
|
|
println!("Failed to pair up players: {}", e);
|
|
return true;
|
|
}
|
|
|
|
// Check if current player has been paired
|
|
let mut is_cyan_player_opt: Option<bool> = None;
|
|
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 paired, is cyan_player
|
|
is_cyan_player_opt = Some(true);
|
|
} else {
|
|
// is paired, not cyan_player
|
|
is_cyan_player_opt = Some(false);
|
|
}
|
|
} else if check_player_row.is_err() {
|
|
// not paired, can do nothing here
|
|
}
|
|
|
|
// don't stop server on send fail, may have timed out and
|
|
// dropped the receiver
|
|
player_tx.send((player_id, is_cyan_player_opt)).ok();
|
|
}
|
|
DBHandlerRequest::CheckPairing {
|
|
id,
|
|
response_sender,
|
|
} => {
|
|
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();
|
|
} else {
|
|
// 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);
|
|
if get_board_result.is_err() {
|
|
// don't stop server on send fail, may have timed out and
|
|
// dropped the receiver
|
|
response_sender.send((DBGameState::UnknownID, None)).ok();
|
|
return false;
|
|
}
|
|
// don't stop server on send fail, may have timed out and
|
|
// dropped the receiver
|
|
response_sender.send(get_board_result.unwrap()).ok();
|
|
}
|
|
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();
|
|
}
|
|
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();
|
|
}
|
|
} // match db_request
|
|
|
|
false
|
|
}
|
|
|
|
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,
|
|
date_added TEXT NOT NULL,
|
|
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");
|
|
}
|
|
|
|
let result = conn.execute(
|
|
"
|
|
CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
|
|
cyan_player INTEGER UNIQUE,
|
|
magenta_player INTEGER UNIQUE,
|
|
date_added TEXT NOT NULL,
|
|
board TEXT NOT NULL,
|
|
status INTEGER NOT NULL,
|
|
FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
|
|
FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
|
|
",
|
|
[],
|
|
);
|
|
if result.is_ok() {
|
|
if first_run == DBFirstRun::FirstRun {
|
|
println!("Created \"games\" table");
|
|
}
|
|
} else if first_run == DBFirstRun::FirstRun {
|
|
println!("\"games\" table exists");
|
|
}
|
|
Ok(conn)
|
|
} else {
|
|
Err(String::from("Failed to open connection"))
|
|
}
|
|
}
|
|
|
|
fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> {
|
|
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)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> {
|
|
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();
|
|
}
|
|
}
|
|
|
|
// TODO randomize players (or first-come-first-serve ok to do?)
|
|
conn.execute(
|
|
"INSERT INTO games (id, cyan_player, magenta_player, date_added, board, status) VALUES (?, ?, ?, datetime(), ?, 0);",
|
|
params![game_id, players[0], players[1], new_string_board()]
|
|
)
|
|
.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))?;
|
|
|
|
Ok(game_id)
|
|
}
|
|
|
|
fn check_if_player_is_paired(
|
|
&self,
|
|
conn: Option<&Connection>,
|
|
player_id: u32,
|
|
) -> Result<CheckPairingType, String> {
|
|
{
|
|
let player_exists_result = self.check_if_player_exists(None, player_id);
|
|
if player_exists_result.is_err() || !player_exists_result.unwrap() {
|
|
// player doesn't exist
|
|
return Ok((false, false, true));
|
|
}
|
|
}
|
|
|
|
if conn.is_none() {
|
|
return self.check_if_player_is_paired(
|
|
Some(&self.get_conn(DBFirstRun::NotFirstRun)?),
|
|
player_id,
|
|
);
|
|
}
|
|
let conn = conn.unwrap();
|
|
|
|
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 {
|
|
// 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))
|
|
}
|
|
} else if let Err(e) = check_player_row {
|
|
Err(format!("check_if_player_is_paired: {:?}", e))
|
|
} else {
|
|
unreachable!("All possible Ok and Err results are already checked");
|
|
}
|
|
}
|
|
|
|
fn check_if_player_exists(
|
|
&self,
|
|
conn: Option<&Connection>,
|
|
player_id: u32,
|
|
) -> Result<bool, String> {
|
|
if conn.is_none() {
|
|
return self
|
|
.check_if_player_exists(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id);
|
|
}
|
|
let conn = conn.unwrap();
|
|
let check_player_row: Result<u32, _> =
|
|
conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| {
|
|
row.get(0)
|
|
});
|
|
if let Ok(_id) = check_player_row {
|
|
Ok(true)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
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))
|
|
} else {
|
|
unreachable!("Both row items should be Ok");
|
|
}
|
|
} 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))
|
|
}
|
|
}
|
|
);
|
|
if let Ok((board, status, cyan_opt, magenta_opt)) = row_result {
|
|
if board.len() != (ROWS * COLS) as usize {
|
|
// board is invalid size
|
|
Ok((DBGameState::InternalError, None))
|
|
} else if cyan_opt.is_none() || magenta_opt.is_none() {
|
|
// One player disconnected
|
|
self.disconnect_player(Some(conn), player_id).ok();
|
|
// Remove the game(s) with disconnected players
|
|
if self.clear_empty_games(Some(conn)).is_err() {
|
|
Ok((DBGameState::InternalError, None))
|
|
} else if status == 2 || status == 3 {
|
|
Ok((DBGameState::from(status), Some(board)))
|
|
} else {
|
|
Ok((DBGameState::OpponentDisconnected, Some(board)))
|
|
}
|
|
} else {
|
|
// Game in progress, or other state depending on "status"
|
|
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))
|
|
} else {
|
|
unreachable!("either exists or is_paired must be false");
|
|
}
|
|
} else {
|
|
// TODO use internal error enum instead of string
|
|
Err(String::from("internal_error"))
|
|
}
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
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) {
|
|
if let (Some(cyan_id), Some(_magenta_id)) = (cyan_id_opt, magenta_id_opt) {
|
|
Ok(Ok((cyan_id == player_id, status, board)))
|
|
} else if (2..=4).contains(&status) {
|
|
// game has ended, don't return error
|
|
// first result will be safely ignored
|
|
Ok(Ok((false, status, board)))
|
|
} 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 {
|
|
self.disconnect_player(Some(conn), player_id).ok();
|
|
if self.clear_empty_games(Some(conn)).is_err() {
|
|
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);
|
|
}
|
|
}
|
|
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)));
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
// 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
|
|
let (board_string, ended_state_opt) = string_from_board(board, final_pos);
|
|
|
|
// update DB
|
|
let update_result = if ended_state_opt.is_none() {
|
|
conn.execute(
|
|
"UPDATE games SET status = ?, board = ? FROM players WHERE players.game_id = games.id AND players.id = ?;",
|
|
params![if status == 0 { 1u8 }
|
|
else { 0u8 },
|
|
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 { 4u8 }
|
|
else if ended_state_opt.unwrap() == BoardState::CyanWin { 2u8 }
|
|
else { 3u8 },
|
|
board_string,
|
|
player_id]
|
|
)
|
|
};
|
|
|
|
if let Err(_e) = update_result {
|
|
return Err(DBPlaceError::InternalError);
|
|
} else if let Ok(count) = update_result {
|
|
if count != 1 {
|
|
return Err(DBPlaceError::InternalError);
|
|
}
|
|
}
|
|
|
|
if let Some(ended_state) = ended_state_opt {
|
|
self.disconnect_player(Some(conn), player_id).ok();
|
|
Ok((
|
|
match ended_state {
|
|
BoardState::Empty => DBPlaceStatus::GameEndedDraw,
|
|
BoardState::Cyan | BoardState::Magenta => unreachable!(),
|
|
BoardState::CyanWin => DBPlaceStatus::GameEndedCyanWon,
|
|
BoardState::MagentaWin => DBPlaceStatus::GameEndedMagentaWon,
|
|
},
|
|
Some(board_string),
|
|
))
|
|
} else {
|
|
Ok((DBPlaceStatus::Accepted, Some(board_string)))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn start_db_handler_thread(
|
|
rx: Receiver<DBHandlerRequest>,
|
|
sqlite_path: String,
|
|
shutdown_tx: SyncSender<()>,
|
|
) {
|
|
let mut handler = DBHandler {
|
|
rx,
|
|
sqlite_path,
|
|
shutdown_tx,
|
|
};
|
|
thread::spawn(move || {
|
|
// temporarily get conn which should initialize on first setup of db
|
|
if let Ok(_conn) = handler.get_conn(DBFirstRun::FirstRun) {
|
|
} else {
|
|
println!("ERROR: Failed init sqlite db connection");
|
|
handler.shutdown_tx.send(()).ok();
|
|
return;
|
|
}
|
|
|
|
'outer: loop {
|
|
if handler.handle_request() {
|
|
handler.shutdown_tx.send(()).ok();
|
|
break 'outer;
|
|
}
|
|
}
|
|
});
|
|
}
|