Compare commits

...

41 commits

Author SHA1 Message Date
Stephen Seo 2d7c8c37e5 Maintenance update
`cargo update` to update Cargo.lock for backend and frontend Rust
projects.
2024-04-03 11:04:52 +09:00
Stephen Seo 090b8bbd30 Fix crash bug 2022-05-06 11:32:20 +09:00
Stephen Seo 59b2bc34fb back-end: Do refactorings 2022-05-05 12:35:30 +09:00
Stephen Seo 1872c4877e Fix bug: board not updated on win/lose/draw 2022-05-04 14:55:01 +09:00
Stephen Seo b5529cb542 Convert User Stories to MSExcel compatible format 2022-05-04 12:41:04 +09:00
Stephen Seo f4f3ad7a5b Refactorings, fix bug where board doesn't update
Also silence warnings related to unused code since the front-end and
back-end share some code.
2022-05-03 13:18:07 +09:00
Stephen Seo 4331a20daa Update README.md 2022-05-02 21:12:18 +09:00
Stephen Seo 047549ecb5 Convert spreadsheets to MS Excel compatible format 2022-05-02 13:56:48 +09:00
Stephen Seo bc6c234314 Add Sprint 6 Retrospective 2022-05-02 13:53:50 +09:00
Stephen Seo 1f27defe11 Update Sprint 6 Backlog, Product Backlog 2022-05-02 13:45:32 +09:00
Stephen Seo 3eb663c305 Update Sprint 6 backlog 2022-04-30 17:48:58 +09:00
Stephen Seo b2ea79a7f7 Impl conditionally update front-end board
When the front-end polls the back-end for the game-state, the back-end includes
a "date_updated" String in the JSON. If the String is the same as in the
front-end, then no updates are needed, but if they are not the same, then the
front-end will update the board. Because the front-end polls the back-end's
board state approximately every second, this should make the front-end more
efficient.
2022-04-30 16:44:48 +09:00
Stephen Seo b4eaba09c5 Refactorings/Fixes related to emoting 2022-04-29 19:21:59 +09:00
Stephen Seo d88e8ef9f3 Update Product Backlog, Sprint 6 Backlog 2022-04-29 18:34:05 +09:00
Stephen Seo 105cd880f2 Impl sending/receiving emotes 2022-04-29 18:30:41 +09:00
Stephen Seo 36dd43bb70 Fixes related to new send emote functionality 2022-04-29 17:24:42 +09:00
Stephen Seo 5381578b08 Update specs, impl back-end support for send emote 2022-04-29 17:16:32 +09:00
Stephen Seo f498f2c475 Update backend_protocol for emote send/recv 2022-04-29 15:53:36 +09:00
Stephen Seo 8eb30fc5d5 Update Product Backlog 2022-04-29 15:35:26 +09:00
Stephen Seo 6ef8667382 Update Sprint 6 backlog 2022-04-29 15:28:08 +09:00
Stephen Seo a4bf4cbd25 Change fn string_from_board to accept board ref 2022-04-29 15:21:22 +09:00
Stephen Seo f799bae530 front-end: Minor refactorings fixes 2022-04-29 12:12:01 +09:00
Stephen Seo b158e7347e front-end: Minor refactorings 2022-04-29 11:37:15 +09:00
Stephen Seo e6152331b0 front-end: minor refactoring 2022-04-29 11:23:03 +09:00
Stephen Seo e77d25996d front-end: fix repeated disconnects on close
When the front-end connects to the back-end, it creates a callback that
sends a disconnect message with the received ID on "pagehide" and
"beforeunload" events. The previous implementation did not "undo" these
callbacks when the game was reset and a new ID was received. This fix
prevents the front-end from resending disconnect messages with
previously received IDs on browser window/tab close.
2022-04-29 11:08:54 +09:00
Stephen Seo e0ed5fc5d8 back_end: Fix bug where CyanWin is MagentaWin 2022-04-28 22:12:36 +09:00
Stephen Seo 6b430660b7 Update README.md 2022-04-28 12:23:04 +09:00
Stephen Seo 174875b88b back-end/front-end: Rust clippy fixes/refactorings 2022-04-27 16:51:57 +09:00
Stephen Seo 694da61bd6 Update Sprint 6 backlog 2022-04-27 15:17:25 +09:00
Stephen Seo 3172af19f8 front-end/back-end: Tweaks to game AI 2022-04-27 15:02:53 +09:00
Stephen Seo dcc9400483 back-end: Minor fix related to phrase handling
Fixes passing an empty string to the db in (probably) rare cases.
2022-04-27 14:16:34 +09:00
Stephen Seo 665dff94fe back-end: Enforce max-length of user-input phrase 2022-04-27 14:11:02 +09:00
Stephen Seo edd3b0c65c Update Sprint 6 backlog 2022-04-27 13:06:35 +09:00
Stephen Seo 059d0608b6 Impl match players via phrase
Front-end now has option to input phrase on game start.
Fixed back-end accepting empty strings (will treat empty strings as if
no phrase was given).
2022-04-27 12:47:45 +09:00
Stephen Seo f9338d4093 back-end: Impl "phrase", update protocol 2022-04-27 11:42:28 +09:00
Stephen Seo 87d93e5b4f back_end: Update back-end for new "phrase" column 2022-04-27 11:15:53 +09:00
Stephen Seo e060d94186 front-end: Minor fix related to reset button 2022-04-25 15:19:25 +09:00
Stephen Seo 96e28b9d68 Update Sprint 6 backlog 2022-04-25 15:03:37 +09:00
Stephen Seo b26c9ff6d1 front-end: Impl Reset button 2022-04-25 15:02:18 +09:00
Stephen Seo d55e43cc6c Merge branch 'dev' (refactorings) 2022-04-25 13:56:10 +09:00
Stephen Seo 501ce91ac3 back_end refactorings
Have back_end cleanup stale players/games on an interval, not every
iteration of its loop.

Replace usage of recursion in db_handler.rs .
2022-04-20 14:14:03 +09:00
21 changed files with 3807 additions and 1002 deletions

View file

@ -26,6 +26,11 @@ The directory `plans` contains the release plans.
The directory `pictures` holds pertinent images to the project. It includes the The directory `pictures` holds pertinent images to the project. It includes the
"Simple Model" of the project. "Simple Model" of the project.
## Tags
The git repository is tagged per Sprint and per Day. [One can visualize the
progress here.](https://git.seodisparate.com/stephenseo/EN605.607.81.SP22_ASDM_Project/graph)
## What is Four-Line Dropper? ## What is Four-Line Dropper?
Four-Line Dropper is a game where two players take turns dropping tokens into Four-Line Dropper is a game where two players take turns dropping tokens into
@ -34,3 +39,7 @@ diagonally is the win condition of the game. If the board fills up with no
four-line matches, then the game ends in a draw. The game is called "Four-Line four-line matches, then the game ends in a draw. The game is called "Four-Line
Dropper" to avoid clashing with the game's original name that is trademarked Dropper" to avoid clashing with the game's original name that is trademarked
(this game is a clone of an existing game). (this game is a clone of an existing game).
# Link to a hosted instance
[I have hosted an instance of the front-end/back-end here.](https://asdm.seodisparate.com)

830
back_end/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "four_line_dropper_backend" name = "four_line_dropper_backend"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -8,12 +8,16 @@
//You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. //You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::ai::{get_ai_choice, AIDifficulty}; use crate::ai::{get_ai_choice, AIDifficulty};
use crate::constants::{ use crate::constants::{
COLS, GAME_CLEANUP_TIMEOUT, PLAYER_CLEANUP_TIMEOUT, PLAYER_COUNT_LIMIT, ROWS, TURN_SECONDS, BACKEND_CLEANUP_INTERVAL_SECONDS, 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, EmoteEnum, Turn,
}; };
use crate::state::{board_from_string, new_string_board, string_from_board, BoardState, Turn};
use std::collections::HashMap;
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender}; use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
use std::time::Duration; use std::time::{Duration, Instant};
use std::{fmt, thread}; use std::{fmt, thread};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -27,7 +31,14 @@ pub type GetIDSenderType = (Option<u32>, Option<bool>);
/// third bool is if cyan player /// third bool is if cyan player
pub type CheckPairingType = (bool, bool, bool); pub type CheckPairingType = (bool, bool, bool);
pub type BoardStateType = (DBGameState, Option<String>); /// second String is board string, third String is date updated, fourth value
/// is EmoteEnum
pub type BoardStateType = (
DBGameState,
Option<String>,
Option<String>,
Option<EmoteEnum>,
);
pub type PlaceResultType = Result<(DBPlaceStatus, Option<String>), DBPlaceError>; pub type PlaceResultType = Result<(DBPlaceStatus, Option<String>), DBPlaceError>;
@ -117,7 +128,10 @@ impl fmt::Display for DBPlaceError {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DBHandlerRequest { pub enum DBHandlerRequest {
GetID(SyncSender<GetIDSenderType>), GetID {
response_sender: SyncSender<GetIDSenderType>,
phrase: Option<String>,
},
CheckPairing { CheckPairing {
id: u32, id: u32,
response_sender: SyncSender<CheckPairingType>, response_sender: SyncSender<CheckPairingType>,
@ -135,6 +149,11 @@ pub enum DBHandlerRequest {
pos: usize, pos: usize,
response_sender: SyncSender<PlaceResultType>, response_sender: SyncSender<PlaceResultType>,
}, },
SendEmote {
id: u32,
emote_type: EmoteEnum,
response_sender: SyncSender<Result<(), ()>>,
},
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -162,7 +181,10 @@ impl DBHandler {
} }
let db_request = rx_recv_result.unwrap(); let db_request = rx_recv_result.unwrap();
match db_request { match db_request {
DBHandlerRequest::GetID(player_tx) => { DBHandlerRequest::GetID {
response_sender,
phrase,
} => {
// got request to create new player, create new player // got request to create new player, create new player
let conn_result = self.get_conn(DBFirstRun::NotFirstRun); let conn_result = self.get_conn(DBFirstRun::NotFirstRun);
if let Err(e) = conn_result { if let Err(e) = conn_result {
@ -171,10 +193,10 @@ impl DBHandler {
} }
let conn = conn_result.unwrap(); let conn = conn_result.unwrap();
let create_player_result = self.create_new_player(Some(&conn)); let create_player_result = self.create_new_player(Some(&conn), phrase);
if let Err(e) = create_player_result { if let Err(e) = create_player_result {
println!("{}", e); println!("{}", e);
player_tx.send((None, None)).ok(); response_sender.send((None, None)).ok();
// don't stop server because player limit may have been reached // don't stop server because player limit may have been reached
return false; return false;
} }
@ -196,11 +218,11 @@ impl DBHandler {
if paired { if paired {
// don't stop server on send fail, may have timed // don't stop server on send fail, may have timed
// out and dropped the receiver // out and dropped the receiver
player_tx.send((Some(player_id), Some(is_cyan))).ok(); response_sender.send((Some(player_id), Some(is_cyan))).ok();
} else { } else {
// don't stop server on send fail, may have timed // don't stop server on send fail, may have timed
// out and dropped the receiver // out and dropped the receiver
player_tx.send((Some(player_id), None)).ok(); response_sender.send((Some(player_id), None)).ok();
} }
} else { } else {
println!("Internal error, created player doesn't exist"); println!("Internal error, created player doesn't exist");
@ -233,7 +255,9 @@ impl DBHandler {
println!("{}", e); println!("{}", e);
// don't stop server on send fail, may have timed out and // don't stop server on send fail, may have timed out and
// dropped the receiver // dropped the receiver
response_sender.send((DBGameState::UnknownID, None)).ok(); response_sender
.send((DBGameState::UnknownID, None, None, None))
.ok();
return false; return false;
} }
// don't stop server on send fail, may have timed out and // don't stop server on send fail, may have timed out and
@ -260,6 +284,23 @@ impl DBHandler {
// dropped the receiver // dropped the receiver
response_sender.send(place_result).ok(); response_sender.send(place_result).ok();
} }
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();
}
}
} // match db_request } // match db_request
false false
@ -274,6 +315,7 @@ impl DBHandler {
CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL, CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
game_id INTEGER, game_id INTEGER,
phrase TEXT,
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE); FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
", ",
[], [],
@ -284,6 +326,9 @@ impl DBHandler {
} }
} else if first_run == DBFirstRun::FirstRun { } else if first_run == DBFirstRun::FirstRun {
println!("\"players\" table exists"); println!("\"players\" table exists");
if let Err(e) = self.db_check_migration(&conn) {
println!("{}", e);
}
} }
let result = conn.execute( let result = conn.execute(
@ -307,17 +352,66 @@ impl DBHandler {
} else if first_run == DBFirstRun::FirstRun { } else if first_run == DBFirstRun::FirstRun {
println!("\"games\" table exists"); println!("\"games\" table exists");
} }
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");
}
Ok(conn) Ok(conn)
} else { } else {
Err(String::from("Failed to open connection")) Err(String::from("Failed to open connection"))
} }
} }
fn create_new_player(&self, conn: Option<&Connection>) -> Result<u32, String> { fn db_check_migration(&self, conn: &Connection) -> Result<(), String> {
if conn.is_none() { let mut table_entries_stmt = conn
return self.create_new_player(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); .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;
}
} }
let conn = conn.unwrap(); 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(())
}
fn create_new_player(
&self,
conn: Option<&Connection>,
phrase: Option<String>,
) -> Result<u32, 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()
};
let row_result: Result<usize, _> = let row_result: Result<usize, _> =
conn.query_row("SELECT count(id) FROM players;", [], |row| row.get(0)); conn.query_row("SELECT count(id) FROM players;", [], |row| row.get(0));
@ -349,7 +443,10 @@ impl DBHandler {
} }
} }
let insert_result = conn.execute("INSERT INTO players (id) VALUES (?);", [player_id]); let insert_result = conn.execute(
"INSERT INTO players (id, phrase) VALUES (?, ?);",
params![player_id, phrase],
);
if let Err(e) = insert_result { if let Err(e) = insert_result {
return Err(format!("Failed to insert player into db: {:?}", e)); return Err(format!("Failed to insert player into db: {:?}", e));
} }
@ -358,29 +455,49 @@ impl DBHandler {
} }
fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> { fn pair_up_players(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.pair_up_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); let conn = if let Some(c) = conn {
} c
let conn = conn.unwrap(); } else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
let mut to_pair: Option<u32> = None; let mut to_pair: Option<u32> = None;
let mut unpaired_players_stmt = conn let mut unpaired_players_stmt = conn
.prepare("SELECT id FROM players WHERE game_id ISNULL ORDER BY date_added;") .prepare("SELECT id, phrase FROM players WHERE game_id ISNULL ORDER BY date_added;")
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
let mut unpaired_players_rows = unpaired_players_stmt let mut unpaired_players_rows = unpaired_players_stmt
.query([]) .query([])
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
let mut phrase_map: HashMap<String, u32> = HashMap::new();
while let Some(row) = unpaired_players_rows while let Some(row) = unpaired_players_rows
.next() .next()
.map_err(|e| format!("{:?}", e))? .map_err(|e| format!("{:?}", e))?
{ {
if to_pair.is_none() { if let Ok(phrase_text) = row.get::<usize, String>(1) {
to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?); // 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))?);
}
} else { } else {
let players: [u32; 2] = [ // pair players that did not use a phrase
to_pair.take().unwrap(), if to_pair.is_none() {
row.get(0).map_err(|e| format!("{:?}", e))?, to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?);
]; } else {
self.create_game(Some(conn), &players)?; let players: [u32; 2] = [
to_pair.take().unwrap(),
row.get(0).map_err(|e| format!("{:?}", e))?,
];
self.create_game(Some(conn), &players)?;
}
} }
} }
@ -388,10 +505,14 @@ impl DBHandler {
} }
fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> { fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.create_game(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), players); let conn = if let Some(c) = conn {
} c
let conn = conn.unwrap(); } else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
let mut game_id: u32 = thread_rng().gen(); let mut game_id: u32 = thread_rng().gen();
{ {
let mut get_game_stmt = conn let mut get_game_stmt = conn
@ -435,13 +556,13 @@ impl DBHandler {
} }
} }
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.check_if_player_is_paired( let conn = if let Some(c) = conn {
Some(&self.get_conn(DBFirstRun::NotFirstRun)?), c
player_id, } else {
); _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
} _conn_result.as_ref().unwrap()
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)); 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 let Ok(cyan_player) = check_player_row {
@ -478,11 +599,14 @@ impl DBHandler {
conn: Option<&Connection>, conn: Option<&Connection>,
player_id: u32, player_id: u32,
) -> Result<bool, String> { ) -> Result<bool, String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self let conn = if let Some(c) = conn {
.check_if_player_exists(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id); c
} } else {
let conn = conn.unwrap(); _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
let check_player_row: Result<u32, _> = let check_player_row: Result<u32, _> =
conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| { conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| {
row.get(0) row.get(0)
@ -499,13 +623,13 @@ impl DBHandler {
conn: Option<&Connection>, conn: Option<&Connection>,
player_id: u32, player_id: u32,
) -> Result<bool, String> { ) -> Result<bool, String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.check_if_player_in_game( let conn = if let Some(c) = conn {
Some(&self.get_conn(DBFirstRun::NotFirstRun)?), c
player_id, } else {
); _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
} _conn_result.as_ref().unwrap()
let conn = conn.unwrap(); };
let check_player_game_row: Result<u32, _> = conn.query_row( 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;", "SELECT games.id FROM games JOIN players WHERE players.id = ? AND players.game_id NOTNULL AND players.game_id = games.id;",
@ -523,68 +647,128 @@ impl DBHandler {
conn: Option<&Connection>, conn: Option<&Connection>,
player_id: u32, player_id: u32,
) -> Result<BoardStateType, String> { ) -> Result<BoardStateType, String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.get_board_state(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id); let conn = if let Some(c) = conn {
c
} else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
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();
}
} }
let conn = conn.unwrap();
// TODO maybe handle "opponent_disconnected" case // TODO maybe handle "opponent_disconnected" case
let row_result: Result<(String, i64, Option<u32>, Option<u32>), RusqliteError> = conn.query_row( type ResultTuple = (String, i64, Option<u32>, Option<u32>, String);
"SELECT games.board, games.status, games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;", let row_result: Result<ResultTuple, RusqliteError> = conn.query_row(
"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;",
[player_id], [player_id],
|row| { |row| {
let board_result = row.get(0); let board_result = row.get(0);
let status_result = row.get(1); let status_result = row.get(1);
let cyan_player = row.get(2); let cyan_player = row.get(2);
let magenta_player = row.get(3); let magenta_player = row.get(3);
if board_result.is_ok() && status_result.is_ok() && cyan_player.is_ok() && magenta_player.is_ok() { let updated_time = row.get(4);
if let (Ok(board), Ok(status), Ok(cyan_id), Ok(magenta_id)) = (board_result, status_result, cyan_player, magenta_player) { if board_result.is_ok() && status_result.is_ok() && cyan_player.is_ok() && magenta_player.is_ok() && updated_time.is_ok() {
Ok((board, status, cyan_id, magenta_id)) 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))
} else { } else {
unreachable!("Both row items should be Ok"); unreachable!("All row items should be Ok");
} }
} else if board_result.is_err() { } else if board_result.is_err() {
board_result board_result
.map(|_| (String::from("this value should never be returned"), 0, None, None)) .map(|_| (String::from("this value should never be returned"), 0, None, None, String::new()))
} else if status_result.is_err() { } else if status_result.is_err() {
status_result status_result
.map(|_| (String::from("this value should never be returned"), 0, None, None)) .map(|_| (String::from("this value should never be returned"), 0, None, None, String::new()))
} else if cyan_player.is_err() { } else if cyan_player.is_err() {
cyan_player cyan_player
.map(|_| (String::from("this value should never be returned"), 0, None, None)) .map(|_| (String::from("this value should never be returned"), 0, None, None, String::new()))
} else { } else if magenta_player.is_err() {
magenta_player magenta_player
.map(|_| (String::from("this value should never be returned"), 0, None, None)) .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()))
} }
} }
); );
if let Ok((board, status, cyan_opt, magenta_opt)) = row_result { if let Ok((board, status, cyan_opt, magenta_opt, updated_time)) = row_result {
if board.len() != (ROWS * COLS) as usize { if board.len() != (ROWS * COLS) as usize {
// board is invalid size // board is invalid size
Ok((DBGameState::InternalError, None)) Ok((
DBGameState::InternalError,
None,
Some(updated_time),
received_emote,
))
} else if cyan_opt.is_none() || magenta_opt.is_none() { } else if cyan_opt.is_none() || magenta_opt.is_none() {
// One player disconnected // One player disconnected
self.disconnect_player(Some(conn), player_id).ok(); self.disconnect_player(Some(conn), player_id).ok();
// Remove the game(s) with disconnected players // Remove the game(s) with disconnected players
if self.clear_empty_games(Some(conn)).is_err() { if self.clear_empty_games(Some(conn)).is_err() {
Ok((DBGameState::InternalError, None)) Ok((
DBGameState::InternalError,
None,
Some(updated_time),
received_emote,
))
} else if status == 2 || status == 3 { } else if status == 2 || status == 3 {
Ok((DBGameState::from(status), Some(board))) Ok((
DBGameState::from(status),
Some(board),
Some(updated_time),
received_emote,
))
} else { } else {
Ok((DBGameState::OpponentDisconnected, Some(board))) Ok((
DBGameState::OpponentDisconnected,
Some(board),
Some(updated_time),
received_emote,
))
} }
} else { } else {
// Game in progress, or other state depending on "status" // Game in progress, or other state depending on "status"
Ok((DBGameState::from(status), Some(board))) Ok((
DBGameState::from(status),
Some(board),
Some(updated_time),
received_emote,
))
} }
} else if let Err(RusqliteError::QueryReturnedNoRows) = row_result { } else if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
// No rows is either player doesn't exist or not paired // No rows is either player doesn't exist or not paired
let (exists, is_paired, _is_cyan) = let (exists, is_paired, _is_cyan) =
self.check_if_player_is_paired(Some(conn), player_id)?; self.check_if_player_is_paired(Some(conn), player_id)?;
if !exists { if !exists {
Ok((DBGameState::UnknownID, None)) Ok((DBGameState::UnknownID, None, None, received_emote))
} else if !is_paired { } else if !is_paired {
Ok((DBGameState::NotPaired, None)) Ok((DBGameState::NotPaired, None, None, received_emote))
} else { } else {
unreachable!("either exists or is_paired must be false"); unreachable!("either exists or is_paired must be false");
} }
@ -595,11 +779,13 @@ impl DBHandler {
} }
fn disconnect_player(&self, conn: Option<&Connection>, player_id: u32) -> Result<(), String> { fn disconnect_player(&self, conn: Option<&Connection>, player_id: u32) -> Result<(), String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self let conn = if let Some(c) = conn {
.disconnect_player(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id); c
} } else {
let conn = conn.unwrap(); _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
let stmt_result = conn.execute("DELETE FROM players WHERE id = ?;", [player_id]); let stmt_result = conn.execute("DELETE FROM players WHERE id = ?;", [player_id]);
if let Ok(1) = stmt_result { if let Ok(1) = stmt_result {
@ -610,10 +796,13 @@ impl DBHandler {
} }
fn clear_empty_games(&self, conn: Option<&Connection>) -> Result<(), String> { fn clear_empty_games(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.clear_empty_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); let conn = if let Some(c) = conn {
} c
let conn = conn.unwrap(); } else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
// Only fails if no rows were removed, and that is not an issue // Only fails if no rows were removed, and that is not an issue
conn.execute( conn.execute(
@ -631,18 +820,13 @@ impl DBHandler {
player_id: u32, player_id: u32,
pos: usize, pos: usize,
) -> PlaceResultType { ) -> PlaceResultType {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.place_token( let conn = if let Some(c) = conn {
Some( c
&self } else {
.get_conn(DBFirstRun::NotFirstRun) _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
.map_err(|_| DBPlaceError::InternalError)?, _conn_result.as_ref().unwrap()
), };
player_id,
pos,
);
}
let conn = conn.unwrap();
// check if player exists // check if player exists
let player_exist_check_result = self.check_if_player_exists(Some(conn), player_id); let player_exist_check_result = self.check_if_player_exists(Some(conn), player_id);
@ -730,33 +914,21 @@ impl DBHandler {
} }
2 => { 2 => {
// game over, cyan won // 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))); return Ok((DBPlaceStatus::GameEndedCyanWon, Some(board_string)));
} }
3 => { 3 => {
// game over, magenta won // 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))); return Ok((DBPlaceStatus::GameEndedMagentaWon, Some(board_string)));
} }
4 => { 4 => {
// game over, draw // 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))); return Ok((DBPlaceStatus::GameEndedDraw, Some(board_string)));
} }
_ => (), _ => (),
} }
// get board state // get board state
let board = board_from_string(board_string); let board = board_from_string(&board_string);
// find placement position or return "illegal move" if unable to // find placement position or return "illegal move" if unable to
let mut final_pos = pos; let mut final_pos = pos;
@ -782,7 +954,7 @@ impl DBHandler {
} }
// board back to string // board back to string
let (board_string, ended_state_opt) = string_from_board(board, final_pos); let (board_string, ended_state_opt) = string_from_board(&board, final_pos);
// update DB // update DB
let update_result = if ended_state_opt.is_none() { let update_result = if ended_state_opt.is_none() {
@ -813,7 +985,6 @@ impl DBHandler {
} }
if let Some(ended_state) = ended_state_opt { if let Some(ended_state) = ended_state_opt {
self.disconnect_player(Some(conn), player_id).ok();
Ok(( Ok((
match ended_state { match ended_state {
BoardState::Empty => DBPlaceStatus::GameEndedDraw, BoardState::Empty => DBPlaceStatus::GameEndedDraw,
@ -862,7 +1033,7 @@ impl DBHandler {
for row_result in rows { for row_result in rows {
if let Ok((id, status, board)) = row_result { if let Ok((id, status, board)) = row_result {
self.have_ai_take_players_turn(Some(&conn), id, status, board)?; self.have_ai_take_players_turn(Some(&conn), id, status, &board)?;
} else { } else {
unreachable!("This part should never execute"); unreachable!("This part should never execute");
} }
@ -876,7 +1047,7 @@ impl DBHandler {
conn: Option<&Connection>, conn: Option<&Connection>,
game_id: u32, game_id: u32,
status: u32, status: u32,
board_string: String, board_string: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if status > 1 { if status > 1 {
return Err(String::from( return Err(String::from(
@ -884,15 +1055,13 @@ impl DBHandler {
)); ));
} }
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.have_ai_take_players_turn( let conn = if let Some(c) = conn {
Some(&self.get_conn(DBFirstRun::NotFirstRun)?), c
game_id, } else {
status, _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
board_string, _conn_result.as_ref().unwrap()
); };
}
let conn = conn.unwrap();
let is_cyan = status == 0; let is_cyan = status == 0;
let board = board_from_string(board_string); let board = board_from_string(board_string);
@ -931,7 +1100,7 @@ impl DBHandler {
}); });
// get board string from board while checking if game has ended // 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 (board_string, end_state_opt) = string_from_board(&board, ai_choice_pos);
let state; let state;
if let Some(board_state) = end_state_opt { if let Some(board_state) = end_state_opt {
@ -958,10 +1127,13 @@ impl DBHandler {
} }
fn cleanup_stale_games(&self, conn: Option<&Connection>) -> Result<(), String> { fn cleanup_stale_games(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.cleanup_stale_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); let conn = if let Some(c) = conn {
} c
let conn = conn.unwrap(); } else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
conn.execute( conn.execute(
"DELETE FROM games WHERE unixepoch() - unixepoch(date_added) > ?;", "DELETE FROM games WHERE unixepoch() - unixepoch(date_added) > ?;",
@ -973,10 +1145,13 @@ impl DBHandler {
} }
fn cleanup_stale_players(&self, conn: Option<&Connection>) -> Result<(), String> { fn cleanup_stale_players(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() { let mut _conn_result = Err(String::new());
return self.cleanup_stale_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?)); let conn = if let Some(c) = conn {
} c
let conn = conn.unwrap(); } else {
_conn_result = self.get_conn(DBFirstRun::NotFirstRun);
_conn_result.as_ref().unwrap()
};
conn.execute( conn.execute(
"DELETE FROM players WHERE unixepoch() - unixepoch(date_added) > ? AND game_id ISNULL;", "DELETE FROM players WHERE unixepoch() - unixepoch(date_added) > ? AND game_id ISNULL;",
@ -986,6 +1161,79 @@ impl DBHandler {
Ok(()) Ok(())
} }
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()
};
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;")
.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(())
}
} }
pub fn start_db_handler_thread( pub fn start_db_handler_thread(
@ -1007,6 +1255,8 @@ pub fn start_db_handler_thread(
return; return;
} }
let mut cleanup_instant = Instant::now();
let cleanup_duration = Duration::from_secs(BACKEND_CLEANUP_INTERVAL_SECONDS);
'outer: loop { 'outer: loop {
if handler.handle_request() { if handler.handle_request() {
handler.shutdown_tx.send(()).ok(); handler.shutdown_tx.send(()).ok();
@ -1017,11 +1267,21 @@ pub fn start_db_handler_thread(
println!("{}", e); println!("{}", e);
} }
if let Err(e) = handler.cleanup_stale_games(None) { if cleanup_instant.elapsed() > cleanup_duration {
println!("{}", e); let conn = handler.get_conn(DBFirstRun::NotFirstRun).ok();
} cleanup_instant = Instant::now();
if let Err(e) = handler.cleanup_stale_players(None) { if let Err(e) = handler.cleanup_stale_games(conn.as_ref()) {
println!("{}", e); println!("{}", e);
}
if let Err(e) = handler.cleanup_stale_players(conn.as_ref()) {
println!("{}", e);
}
if let Err(e) = handler.cleanup_stale_emotes(conn.as_ref()) {
println!("{}", e);
}
if let Err(e) = handler.clear_empty_games(conn.as_ref()) {
println!("{}", e);
}
} }
} }
}); });

View file

@ -6,7 +6,11 @@
//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. //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/>. //You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::db_handler::{CheckPairingType, DBHandlerRequest, GetIDSenderType}; use crate::{
constants::BACKEND_PHRASE_MAX_LENGTH,
db_handler::{CheckPairingType, DBHandlerRequest, GetIDSenderType},
state::EmoteEnum,
};
use std::{ use std::{
sync::mpsc::{sync_channel, SyncSender}, sync::mpsc::{sync_channel, SyncSender},
@ -24,11 +28,12 @@ pub fn handle_json(
) -> Result<String, String> { ) -> Result<String, String> {
if let Some(Value::String(type_str)) = root.get("type") { if let Some(Value::String(type_str)) = root.get("type") {
match type_str.as_str() { match type_str.as_str() {
"pairing_request" => handle_pairing_request(tx), "pairing_request" => handle_pairing_request(root, tx),
"check_pairing" => handle_check_pairing(root, tx), "check_pairing" => handle_check_pairing(root, tx),
"place_token" => handle_place_token(root, tx), "place_token" => handle_place_token(root, tx),
"disconnect" => handle_disconnect(root, tx), "disconnect" => handle_disconnect(root, tx),
"game_state" => handle_game_state(root, tx), "game_state" => handle_game_state(root, tx),
"send_emote" => handle_send_emote(root, tx),
_ => Err("{\"type\":\"invalid_type\"}".into()), _ => Err("{\"type\":\"invalid_type\"}".into()),
} }
} else { } else {
@ -36,9 +41,37 @@ pub fn handle_json(
} }
} }
fn handle_pairing_request(tx: SyncSender<DBHandlerRequest>) -> Result<String, String> { fn handle_pairing_request(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
let (player_tx, player_rx) = sync_channel::<GetIDSenderType>(1); let (player_tx, player_rx) = sync_channel::<GetIDSenderType>(1);
if tx.send(DBHandlerRequest::GetID(player_tx)).is_err() { let mut phrase: Option<String> = None;
if let Some(phrase_text) = root.get("phrase") {
if let Some(mut phrase_str) = phrase_text.as_str() {
if !phrase_str.is_empty() {
if phrase_str.len() > BACKEND_PHRASE_MAX_LENGTH {
let mut idx = BACKEND_PHRASE_MAX_LENGTH;
while idx > 0 && !phrase_str.is_char_boundary(idx) {
idx -= 1;
}
if idx == 0 {
phrase_str = "";
} else {
phrase_str = phrase_str.split_at(idx).0;
}
}
if !phrase_str.is_empty() {
phrase = Some(phrase_str.to_owned());
}
}
}
}
if tx
.send(DBHandlerRequest::GetID {
response_sender: player_tx,
phrase,
})
.is_err()
{
return Err("{\"type\":\"pairing_response\", \"status\":\"internal_error\"}".into()); return Err("{\"type\":\"pairing_response\", \"status\":\"internal_error\"}".into());
} }
if let Ok((pid_opt, is_cyan_opt)) = player_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if let Ok((pid_opt, is_cyan_opt)) = player_rx.recv_timeout(DB_REQUEST_TIMEOUT) {
@ -233,12 +266,27 @@ fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<St
return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into()); return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into());
} }
if let Ok((db_game_state, board_string_opt)) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if let Ok((db_game_state, board_string_opt, updated_time_opt, received_emote_opt)) =
resp_rx.recv_timeout(DB_REQUEST_TIMEOUT)
{
if let Some(board_string) = board_string_opt { if let Some(board_string) = board_string_opt {
Ok(format!( let updated_time = if let Some(time_string) = updated_time_opt {
"{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\"}}", time_string
db_game_state, board_string } else {
)) return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into());
};
if let Some(emote) = received_emote_opt {
Ok(format!(
"{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\", \"peer_emote\": \"{}\", \"updated_time\": \"{}\"}}",
db_game_state, board_string, emote, updated_time
))
} else {
Ok(format!(
"{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\", \"updated_time\": \"{}\"}}",
db_game_state, board_string, updated_time
))
}
} else { } else {
Ok(format!( Ok(format!(
"{{\"type\":\"game_state\", \"status\":\"{}\"}}", "{{\"type\":\"game_state\", \"status\":\"{}\"}}",
@ -249,3 +297,56 @@ fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<St
Err("{\"type\":\"game_state\", \"status\":\"internal_error_timeout\"}".into()) Err("{\"type\":\"game_state\", \"status\":\"internal_error_timeout\"}".into())
} }
} }
fn handle_send_emote(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
let id_option = root.get("id");
if id_option.is_none() {
return Err("{\"type\":\"invalid_syntax\"}".into());
}
let player_id = id_option
.unwrap()
.as_u64()
.ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?;
let player_id: u32 = player_id
.try_into()
.map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?;
let emote_type_option = root.get("emote");
if emote_type_option.is_none() {
return Err("{\"type\":\"invalid_syntax\"}".into());
}
let emote_type_option = emote_type_option.unwrap().as_str();
if emote_type_option.is_none() {
return Err("{\"type\":\"invalid_syntax\"}".into());
}
let emote_type = emote_type_option.unwrap();
let emote_enum: Result<EmoteEnum, ()> = emote_type.try_into();
if emote_enum.is_err() {
return Err("{\"type\":\"invalid_syntax\"}".into());
}
let emote_enum = emote_enum.unwrap();
let (resp_tx, resp_rx) = sync_channel(1);
if tx
.send(DBHandlerRequest::SendEmote {
id: player_id,
emote_type: emote_enum,
response_sender: resp_tx,
})
.is_err()
{
return Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into());
}
if let Ok(db_response) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) {
if db_response.is_ok() {
Ok("{\"type\":\"send_emote\", \"status\":\"ok\"}".into())
} else {
Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into())
}
} else {
Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into())
}
}

182
front_end/Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]] [[package]]
name = "boolinator" name = "boolinator"
@ -16,9 +16,9 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.9.1" version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -71,10 +71,11 @@ dependencies = [
[[package]] [[package]]
name = "gloo-console" name = "gloo-console"
version = "0.2.1" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3907f786f65bbb4f419e918b0c5674175ef1c231ecda93b2dbd65fd1e8882637" checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [ dependencies = [
"gloo-utils",
"js-sys", "js-sys",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
@ -83,9 +84,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-dialogs" name = "gloo-dialogs"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffb557a2ea2ed283f1334423d303a336fad55fb8572d51ae488f828b1464b40" checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@ -93,9 +94,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-events" name = "gloo-events"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f" checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@ -103,9 +104,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-file" name = "gloo-file"
version = "0.2.1" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa5d6084efa4a2b182ef3a8649cb6506cb4843f22cf907c6e0a799944248ae90" checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [ dependencies = [
"gloo-events", "gloo-events",
"js-sys", "js-sys",
@ -115,9 +116,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-render" name = "gloo-render"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b4cda6e149df3bb4a3c6a343873903e5bcc2448a9877d61bb8274806ad67f6e" checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@ -125,9 +126,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-storage" name = "gloo-storage"
version = "0.2.0" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5057761927af1b1929d02b1f49cf83553dd347a473ee7c8bb08420f2673ffc" checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [ dependencies = [
"gloo-utils", "gloo-utils",
"js-sys", "js-sys",
@ -140,9 +141,9 @@ dependencies = [
[[package]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.3" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e" checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -150,26 +151,28 @@ dependencies = [
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.1.2" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c77af6f96a4f9e27c8ac23a88407381a31f4a74c3fb985c85aa79b8d898136" checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde",
"serde_json",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.0" version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -177,15 +180,15 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.1" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.56" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -198,12 +201,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
dependencies = [
"cfg-if", [[package]]
] name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "oorandom" name = "oorandom"
@ -220,7 +226,7 @@ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.109",
"version_check", "version_check",
] ]
@ -237,59 +243,59 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.36" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [ dependencies = [
"unicode-xid", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.15" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.9" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]] [[package]]
name = "scoped-tls-hkt" name = "scoped-tls-hkt"
version = "0.1.2" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63" checksum = "3ddc765d3410d9f6c6ca071bf0b67f6b01e3ec4595dc3892f02677e75819dddc"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.136" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.136" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.58",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.79" version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -298,46 +304,60 @@ dependencies = [
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.5" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.86" version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-xid", "unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
] ]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.30" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.30" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.58",
] ]
[[package]] [[package]]
name = "unicode-xid" name = "unicode-ident"
version = "0.2.2" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]] [[package]]
name = "version_check" name = "version_check"
@ -347,9 +367,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.79" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"serde", "serde",
@ -359,24 +379,24 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.79" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static",
"log", "log",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.58",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.29" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@ -386,9 +406,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.79" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -396,22 +416,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.79" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.58",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.79" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]] [[package]]
name = "wasm-logger" name = "wasm-logger"
@ -426,9 +446,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.56" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -464,5 +484,5 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.109",
] ]

View file

@ -9,7 +9,7 @@ edition = "2021"
yew = "0.19" yew = "0.19"
log = "0.4.6" log = "0.4.6"
wasm-logger = "0.2.0" wasm-logger = "0.2.0"
web-sys = { version = "0.3.56", features = ["Window", "Document", "Element", "Request", "RequestInit", "Headers", "RequestMode", "Response", "ReadableStream", "AddEventListenerOptions", "EventTarget"] } web-sys = { version = "0.3.56", features = ["Window", "Document", "Element", "Request", "RequestInit", "Headers", "RequestMode", "Response", "ReadableStream", "AddEventListenerOptions", "EventListenerOptions", "EventTarget"] }
js-sys = "0.3.56" js-sys = "0.3.56"
oorandom = "11.1.3" oorandom = "11.1.3"
wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] }

View file

@ -42,9 +42,50 @@
grid-row: 2; grid-row: 2;
grid-column: 3; grid-column: 3;
} }
button.menuMultiplayer { div.multiplayerMenu {
grid-row: 2; grid-row: 2;
grid-column: 4; grid-column: 4;
display: grid;
}
div.emote_wrapper {
grid-row: 4;
grid-column: 8;
display: grid;
}
button.emote {
font-size: 2em;
}
b.emote {
font-size: 1.5em;
}
button#emote_smile {
grid-row: 1;
grid-column: 1;
}
button#emote_neutral {
grid-row: 1;
grid-column: 2;
}
button#emote_frown {
grid-row: 1;
grid-column: 3;
}
button#emote_think {
grid-row: 1;
grid-column: 4;
}
button.networkedMultiplayer {
grid-row: 1;
grid-column: 1;
}
button.NMPhrase {
grid-row: 2;
grid-column: 1;
}
input.NMPhrase {
grid-row: 3;
grid-column: 1;
} }
b.menuText { b.menuText {
color: #FFF; color: #FFF;
@ -188,6 +229,12 @@
opacity: 1; opacity: 1;
} }
} }
button#resetbutton {
background-color: #C55;
color: #FFF;
grid-row: 2;
grid-column: 8;
}
</style> </style>
</head> </head>
</html> </html>

View file

@ -13,6 +13,9 @@ use crate::game_logic::check_win_draw;
use crate::random_helper::get_seeded_random; use crate::random_helper::get_seeded_random;
use crate::state::{board_deep_clone, BoardState, BoardType, Turn}; use crate::state::{board_deep_clone, BoardState, BoardType, Turn};
const AI_THIRD_MAX_UTILITY: f64 = 0.89;
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AIDifficulty { pub enum AIDifficulty {
Easy, Easy,
@ -176,25 +179,25 @@ fn get_utility_for_slot(player: Turn, slot: SlotChoice, board: &BoardType) -> Op
// check if placing a token here connects 2 pieces // check if placing a token here connects 2 pieces
if get_block_amount(player.get_opposite(), idx, 2, board) { if get_block_amount(player.get_opposite(), idx, 2, board) {
utility *= 1.5; utility *= 1.22;
if utility >= 0.8 { if utility >= AI_THIRD_MAX_UTILITY {
utility = 0.8; utility = AI_THIRD_MAX_UTILITY;
} }
} }
// check if placing a token here blocks 2 pieces // check if placing a token here blocks 2 pieces
if get_block_amount(player, idx, 2, board) { if get_block_amount(player, idx, 2, board) {
utility *= 1.2; utility *= 1.11;
if utility >= 0.8 { if utility >= AI_THIRD_MAX_UTILITY {
utility = 0.8; utility = AI_THIRD_MAX_UTILITY;
} }
} }
// check if placing a token here connects 1 piece // check if placing a token here connects 1 piece
if get_block_amount(player.get_opposite(), idx, 1, board) { if get_block_amount(player.get_opposite(), idx, 1, board) {
utility *= 1.09; utility *= 1.05;
if utility >= 0.8 { if utility >= AI_THIRD_MAX_UTILITY {
utility = 0.8; utility = AI_THIRD_MAX_UTILITY;
} }
} }
@ -330,3 +333,24 @@ fn get_block_amount(player: Turn, idx: usize, amount: usize, board: &BoardType)
// exhausted all possible potential wins, therefore does not block a win // exhausted all possible potential wins, therefore does not block a win
false false
} }
#[cfg(test)]
mod tests {
use crate::state::new_empty_board;
use super::*;
#[test]
fn test_get_block_amount() {
let board = new_empty_board();
board[51].set(BoardState::Cyan);
board[52].set(BoardState::Cyan);
board[53].set(BoardState::Cyan);
assert!(!get_block_amount(Turn::MagentaPlayer, 50, 4, &board));
assert!(get_block_amount(Turn::MagentaPlayer, 50, 3, &board));
assert!(get_block_amount(Turn::MagentaPlayer, 50, 2, &board));
assert!(!get_block_amount(Turn::MagentaPlayer, 54, 4, &board));
assert!(get_block_amount(Turn::MagentaPlayer, 54, 3, &board));
assert!(get_block_amount(Turn::MagentaPlayer, 54, 2, &board));
}
}

View file

@ -9,22 +9,36 @@
pub const ROWS: u8 = 8; pub const ROWS: u8 = 8;
pub const COLS: u8 = 7; pub const COLS: u8 = 7;
#[allow(dead_code)]
pub const INFO_TEXT_MAX_ITEMS: u32 = 100; pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
pub const AI_EASY_MAX_CHOICES: usize = 5; pub const AI_EASY_MAX_CHOICES: usize = 5;
pub const AI_NORMAL_MAX_CHOICES: usize = 3; pub const AI_NORMAL_MAX_CHOICES: usize = 3;
#[allow(dead_code)]
pub const AI_CHOICE_DURATION_MILLIS: i32 = 1000; pub const AI_CHOICE_DURATION_MILLIS: i32 = 1000;
#[allow(dead_code)]
pub const PLAYER_COUNT_LIMIT: usize = 1000; pub const PLAYER_COUNT_LIMIT: usize = 1000;
#[allow(dead_code)]
pub const TURN_SECONDS: u64 = 25; pub const TURN_SECONDS: u64 = 25;
#[allow(dead_code)]
pub const GAME_CLEANUP_TIMEOUT: u64 = (TURN_SECONDS + 1) * ((ROWS * COLS) as u64 + 5u64); pub const GAME_CLEANUP_TIMEOUT: u64 = (TURN_SECONDS + 1) * ((ROWS * COLS) as u64 + 5u64);
#[allow(dead_code)]
pub const PLAYER_CLEANUP_TIMEOUT: u64 = 300; pub const PLAYER_CLEANUP_TIMEOUT: u64 = 300;
#[allow(dead_code)]
pub const BACKEND_TICK_DURATION_MILLIS: i32 = 500; pub const BACKEND_TICK_DURATION_MILLIS: i32 = 500;
#[allow(dead_code)]
pub const BACKEND_CLEANUP_INTERVAL_SECONDS: u64 = 120;
#[allow(dead_code)]
pub const BACKEND_PHRASE_MAX_LENGTH: usize = 128;
// TODO: Change this to "https://asdm.seodisparate.com/api" when backend is installed // TODO: Change this to "https://asdm.seodisparate.com/api" when backend is installed
#[allow(dead_code)]
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub const BACKEND_URL: &str = "http://testlocalhost/api"; pub const BACKEND_URL: &str = "http://testlocalhost/api";
#[allow(dead_code)]
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
pub const BACKEND_URL: &str = "https://asdm.seodisparate.com/api"; pub const BACKEND_URL: &str = "https://asdm.seodisparate.com/api";

View file

@ -10,7 +10,7 @@ use js_sys::{Function, JsString, Promise};
use std::collections::HashMap; use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{window, Document, Request, RequestInit, Window}; use web_sys::{window, Document, Window};
use crate::constants::BACKEND_URL; use crate::constants::BACKEND_URL;
@ -119,34 +119,6 @@ pub fn element_has_class(document: &Document, id: &str, class: &str) -> Result<b
Ok(element_class.contains(class)) Ok(element_class.contains(class))
} }
pub fn create_json_request(target_url: &str, json_body: &str) -> Result<Request, String> {
let mut req_init: RequestInit = RequestInit::new();
req_init.body(Some(&JsValue::from_str(json_body)));
req_init.method("POST");
// TODO omit the NoCors when hosted on website
req_init.mode(web_sys::RequestMode::NoCors);
// req_init.headers(
// &JsValue::from_str("{'Content-Type': 'application/json'}"),
// &JsValue::from_serde("{'Content-Type': 'application/json'}")
// .map_err(|e| format!("{}", e))?,
// &JsValue::from_serde("'headers': { 'Content-Type': 'application/json' }")
// .map_err(|e| format!("{}", e))?,
// );
let request: Request =
Request::new_with_str_and_init(target_url, &req_init).map_err(|e| format!("{:?}", e))?;
request
.headers()
.set("Content-Type", "application/json")
.map_err(|e| format!("{:?}", e))?;
request
.headers()
.set("Accept", "application/json")
.map_err(|e| format!("{:?}", e))?;
Ok(request)
}
pub async fn send_to_backend(entries: HashMap<String, String>) -> Result<String, String> { pub async fn send_to_backend(entries: HashMap<String, String>) -> Result<String, String> {
let mut send_json_string = String::from("{"); let mut send_json_string = String::from("{");
for (key, value) in entries { for (key, value) in entries {

View file

@ -12,12 +12,13 @@ use crate::game_logic::{check_win_draw, WinType};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cell::Cell; use std::cell::{Cell, RefCell};
use std::collections::hash_set::HashSet; use std::collections::hash_set::HashSet;
use std::fmt::Display; use std::fmt::Display;
use std::rc::Rc; use std::rc::Rc;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GameState { pub enum GameState {
MainMenu, MainMenu,
SinglePlayer(Turn, AIDifficulty), SinglePlayer(Turn, AIDifficulty),
@ -26,38 +27,45 @@ pub enum GameState {
paired: bool, paired: bool,
current_side: Option<Turn>, current_side: Option<Turn>,
current_turn: Turn, current_turn: Turn,
phrase: Option<String>,
}, },
PostGameResults(BoardState), PostGameResults(BoardState),
} }
impl GameState { impl GameState {
pub fn is_networked_multiplayer(self) -> bool { #[allow(dead_code)]
pub fn is_networked_multiplayer(&self) -> bool {
matches!( matches!(
self, *self,
GameState::NetworkedMultiplayer { GameState::NetworkedMultiplayer {
paired: _, paired: _,
current_side: _, current_side: _,
current_turn: _ current_turn: _,
phrase: _,
} }
) )
} }
#[allow(dead_code)]
pub fn set_networked_paired(&mut self) { pub fn set_networked_paired(&mut self) {
if let GameState::NetworkedMultiplayer { if let GameState::NetworkedMultiplayer {
ref mut paired, ref mut paired,
current_side: _, current_side: _,
current_turn: _, current_turn: _,
phrase: _,
} = self } = self
{ {
*paired = true; *paired = true;
} }
} }
#[allow(dead_code)]
pub fn get_networked_current_side(&self) -> Option<Turn> { pub fn get_networked_current_side(&self) -> Option<Turn> {
if let GameState::NetworkedMultiplayer { if let GameState::NetworkedMultiplayer {
paired: _, paired: _,
current_side, current_side,
current_turn: _, current_turn: _,
phrase: _,
} = *self } = *self
{ {
current_side current_side
@ -66,17 +74,20 @@ impl GameState {
} }
} }
#[allow(dead_code)]
pub fn set_networked_current_side(&mut self, side: Option<Turn>) { pub fn set_networked_current_side(&mut self, side: Option<Turn>) {
if let GameState::NetworkedMultiplayer { if let GameState::NetworkedMultiplayer {
paired: _, paired: _,
ref mut current_side, ref mut current_side,
current_turn: _, current_turn: _,
phrase: _,
} = self } = self
{ {
*current_side = side; *current_side = side;
} }
} }
#[allow(dead_code)]
pub fn get_current_turn(&self) -> Turn { pub fn get_current_turn(&self) -> Turn {
if let GameState::SinglePlayer(turn, _) = *self { if let GameState::SinglePlayer(turn, _) = *self {
turn turn
@ -84,6 +95,7 @@ impl GameState {
paired: _, paired: _,
current_side: _, current_side: _,
current_turn, current_turn,
phrase: _,
} = *self } = *self
{ {
current_turn current_turn
@ -92,29 +104,42 @@ impl GameState {
} }
} }
pub fn get_network_current_side(&self) -> Option<Turn> { #[allow(dead_code)]
if let GameState::NetworkedMultiplayer {
paired: _,
current_side,
current_turn: _,
} = *self
{
current_side
} else {
None
}
}
pub fn set_networked_current_turn(&mut self, turn: Turn) { pub fn set_networked_current_turn(&mut self, turn: Turn) {
if let GameState::NetworkedMultiplayer { if let GameState::NetworkedMultiplayer {
paired: _, paired: _,
current_side: _, current_side: _,
ref mut current_turn, ref mut current_turn,
phrase: _,
} = self } = self
{ {
*current_turn = turn; *current_turn = turn;
} }
} }
#[allow(dead_code)]
pub fn get_phrase(&self) -> Option<String> {
if let GameState::NetworkedMultiplayer {
paired: _,
current_side: _,
current_turn: _,
phrase,
} = self
{
phrase.clone()
} else {
None
}
}
#[allow(dead_code)]
pub fn get_singleplayer_current_side(&self) -> Option<Turn> {
if let GameState::SinglePlayer(turn, _) = *self {
Some(turn)
} else {
None
}
}
} }
impl Default for GameState { impl Default for GameState {
@ -128,15 +153,17 @@ impl From<MainMenuMessage> for GameState {
match msg { match msg {
MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai), MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai),
MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer, MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer,
MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer { MainMenuMessage::NetworkedMultiplayer(phrase) => GameState::NetworkedMultiplayer {
paired: false, paired: false,
current_side: None, current_side: None,
current_turn: Turn::CyanPlayer, current_turn: Turn::CyanPlayer,
phrase,
}, },
} }
} }
} }
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BoardState { pub enum BoardState {
Empty, Empty,
@ -174,10 +201,12 @@ impl From<Turn> for BoardState {
} }
impl BoardState { impl BoardState {
#[allow(dead_code)]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
*self == BoardState::Empty *self == BoardState::Empty
} }
#[allow(dead_code)]
pub fn is_win(self) -> bool { pub fn is_win(self) -> bool {
match self { match self {
BoardState::Empty | BoardState::Cyan | BoardState::Magenta => false, BoardState::Empty | BoardState::Cyan | BoardState::Magenta => false,
@ -185,6 +214,7 @@ impl BoardState {
} }
} }
#[allow(dead_code)]
pub fn into_win(self) -> Self { pub fn into_win(self) -> Self {
match self { match self {
BoardState::Empty => BoardState::Empty, BoardState::Empty => BoardState::Empty,
@ -193,8 +223,9 @@ impl BoardState {
} }
} }
pub fn from_win(&self) -> Self { #[allow(dead_code, clippy::wrong_self_convention)]
match *self { pub fn from_win(self) -> Self {
match self {
BoardState::Empty => BoardState::Empty, BoardState::Empty => BoardState::Empty,
BoardState::Cyan | BoardState::CyanWin => BoardState::Cyan, BoardState::Cyan | BoardState::CyanWin => BoardState::Cyan,
BoardState::Magenta | BoardState::MagentaWin => BoardState::Magenta, BoardState::Magenta | BoardState::MagentaWin => BoardState::Magenta,
@ -202,6 +233,7 @@ impl BoardState {
} }
} }
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Turn { pub enum Turn {
CyanPlayer, CyanPlayer,
@ -227,6 +259,7 @@ impl From<BoardState> for Turn {
} }
impl Turn { impl Turn {
#[allow(dead_code)]
pub fn get_color(&self) -> &str { pub fn get_color(&self) -> &str {
match *self { match *self {
Turn::CyanPlayer => "cyan", Turn::CyanPlayer => "cyan",
@ -244,6 +277,7 @@ impl Turn {
pub type BoardType = [Rc<Cell<BoardState>>; 56]; pub type BoardType = [Rc<Cell<BoardState>>; 56];
#[allow(dead_code)]
pub fn new_empty_board() -> BoardType { pub fn new_empty_board() -> BoardType {
[ [
Rc::new(Cell::new(BoardState::default())), Rc::new(Cell::new(BoardState::default())),
@ -305,6 +339,7 @@ pub fn new_empty_board() -> BoardType {
] ]
} }
#[allow(dead_code)]
pub fn board_deep_clone(board: &BoardType) -> BoardType { pub fn board_deep_clone(board: &BoardType) -> BoardType {
let cloned_board = new_empty_board(); let cloned_board = new_empty_board();
for i in 0..board.len() { for i in 0..board.len() {
@ -316,6 +351,7 @@ pub fn board_deep_clone(board: &BoardType) -> BoardType {
pub type PlacedType = [Rc<Cell<bool>>; 56]; pub type PlacedType = [Rc<Cell<bool>>; 56];
#[allow(dead_code)]
pub fn new_placed() -> PlacedType { pub fn new_placed() -> PlacedType {
[ [
Rc::new(Cell::new(false)), Rc::new(Cell::new(false)),
@ -377,10 +413,11 @@ pub fn new_placed() -> PlacedType {
] ]
} }
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct SharedState { pub struct SharedState {
pub board: BoardType, pub board: BoardType,
pub game_state: Rc<Cell<GameState>>, pub game_state: Rc<RefCell<GameState>>,
pub turn: Rc<Cell<Turn>>, pub turn: Rc<Cell<Turn>>,
pub placed: PlacedType, pub placed: PlacedType,
} }
@ -390,7 +427,7 @@ impl Default for SharedState {
Self { Self {
// cannot use [<type>; 56] because Rc does not impl Copy // cannot use [<type>; 56] because Rc does not impl Copy
board: new_empty_board(), board: new_empty_board(),
game_state: Rc::new(Cell::new(GameState::default())), game_state: Rc::new(RefCell::new(GameState::default())),
turn: Rc::new(Cell::new(Turn::CyanPlayer)), turn: Rc::new(Cell::new(Turn::CyanPlayer)),
placed: new_placed(), placed: new_placed(),
} }
@ -399,13 +436,15 @@ impl Default for SharedState {
// This enum moved from yew_components module so that this module would have no // This enum moved from yew_components module so that this module would have no
// dependencies on the yew_components module // dependencies on the yew_components module
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MainMenuMessage { pub enum MainMenuMessage {
SinglePlayer(Turn, AIDifficulty), SinglePlayer(Turn, AIDifficulty),
LocalMultiplayer, LocalMultiplayer,
NetworkedMultiplayer, NetworkedMultiplayer(Option<String>),
} }
#[allow(dead_code)]
pub fn new_string_board() -> String { pub fn new_string_board() -> String {
let mut board = String::with_capacity(56); let mut board = String::with_capacity(56);
for _i in 0..56 { for _i in 0..56 {
@ -414,7 +453,8 @@ pub fn new_string_board() -> String {
board board
} }
pub fn board_from_string(board_string: String) -> BoardType { #[allow(dead_code)]
pub fn board_from_string(board_string: &str) -> BoardType {
let board = new_empty_board(); let board = new_empty_board();
for (idx, c) in board_string.chars().enumerate() { for (idx, c) in board_string.chars().enumerate() {
@ -433,12 +473,13 @@ pub fn board_from_string(board_string: String) -> BoardType {
/// Returns the board as a String, and None if game has not ended, Empty if game /// Returns the board as a String, and None if game has not ended, Empty if game
/// ended in a draw, or a player if that player has won /// ended in a draw, or a player if that player has won
pub fn string_from_board(board: BoardType, placed: usize) -> (String, Option<BoardState>) { #[allow(dead_code)]
pub fn string_from_board(board: &BoardType, placed: usize) -> (String, Option<BoardState>) {
let mut board_string = String::with_capacity(56); let mut board_string = String::with_capacity(56);
// check for winning pieces // check for winning pieces
let mut win_set: HashSet<usize> = HashSet::new(); let mut win_set: HashSet<usize> = HashSet::new();
let win_opt = check_win_draw(&board); let win_opt = check_win_draw(board);
if let Some((_board_state, win_type)) = win_opt { if let Some((_board_state, win_type)) = win_opt {
match win_type { match win_type {
WinType::Horizontal(pos) => { WinType::Horizontal(pos) => {
@ -505,9 +546,11 @@ pub fn string_from_board(board: BoardType, placed: usize) -> (String, Option<Boa
if is_full && win_set.is_empty() { if is_full && win_set.is_empty() {
(board_string, Some(BoardState::Empty)) (board_string, Some(BoardState::Empty))
} else if !win_set.is_empty() { } else if !win_set.is_empty() {
let winning_char: char =
board_string.chars().collect::<Vec<char>>()[*win_set.iter().next().unwrap()];
( (
board_string.clone(), board_string.clone(),
if board_string.chars().collect::<Vec<char>>()[*win_set.iter().next().unwrap()] == 'd' { if winning_char == 'd' || winning_char == 'h' {
Some(BoardState::CyanWin) Some(BoardState::CyanWin)
} else { } else {
Some(BoardState::MagentaWin) Some(BoardState::MagentaWin)
@ -518,6 +561,7 @@ pub fn string_from_board(board: BoardType, placed: usize) -> (String, Option<Boa
} }
} }
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PairingRequestResponse { pub struct PairingRequestResponse {
pub r#type: String, pub r#type: String,
@ -526,6 +570,7 @@ pub struct PairingRequestResponse {
pub color: Option<String>, pub color: Option<String>,
} }
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PairingStatusResponse { pub struct PairingStatusResponse {
pub r#type: String, pub r#type: String,
@ -533,13 +578,17 @@ pub struct PairingStatusResponse {
pub color: Option<String>, pub color: Option<String>,
} }
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct GameStateResponse { pub struct GameStateResponse {
pub r#type: String, pub r#type: String,
pub status: String, pub status: String,
pub board: Option<String>, pub board: Option<String>,
pub peer_emote: Option<String>,
pub updated_time: Option<String>,
} }
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PlaceTokenResponse { pub struct PlaceTokenResponse {
pub r#type: String, pub r#type: String,
@ -547,6 +596,14 @@ pub struct PlaceTokenResponse {
pub board: String, pub board: String,
} }
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SendEmoteRequestResponse {
pub r#type: String,
pub status: String,
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NetworkedGameState { pub enum NetworkedGameState {
CyanTurn, CyanTurn,
@ -560,6 +617,7 @@ pub enum NetworkedGameState {
UnknownID, UnknownID,
} }
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PlacedEnum { pub enum PlacedEnum {
Accepted, Accepted,
@ -568,6 +626,62 @@ pub enum PlacedEnum {
Other(NetworkedGameState), Other(NetworkedGameState),
} }
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EmoteEnum {
Smile,
Neutral,
Frown,
Think,
}
impl Display for EmoteEnum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
EmoteEnum::Smile => f.write_str("smile"),
EmoteEnum::Neutral => f.write_str("neutral"),
EmoteEnum::Frown => f.write_str("frown"),
EmoteEnum::Think => f.write_str("think"),
}
}
}
impl TryFrom<&str> for EmoteEnum {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"smile" => Ok(Self::Smile),
"neutral" => Ok(Self::Neutral),
"frown" => Ok(Self::Frown),
"think" => Ok(Self::Think),
_ => Err(()),
}
}
}
impl From<EmoteEnum> for String {
fn from(e: EmoteEnum) -> Self {
match e {
EmoteEnum::Smile => "smile".into(),
EmoteEnum::Neutral => "neutral".into(),
EmoteEnum::Frown => "frown".into(),
EmoteEnum::Think => "think".into(),
}
}
}
impl EmoteEnum {
pub fn get_unicode(&self) -> char {
match *self {
EmoteEnum::Smile => '🙂',
EmoteEnum::Neutral => '😐',
EmoteEnum::Frown => '🙁',
EmoteEnum::Think => '🤔',
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -582,12 +696,59 @@ mod tests {
paired: false, paired: false,
current_side: None, current_side: None,
current_turn: Turn::CyanPlayer, current_turn: Turn::CyanPlayer,
phrase: None,
}; };
assert!(state.is_networked_multiplayer()); assert!(state.is_networked_multiplayer());
let state = GameState::NetworkedMultiplayer { let state = GameState::NetworkedMultiplayer {
paired: true, paired: true,
current_side: Some(Turn::CyanPlayer), current_side: Some(Turn::CyanPlayer),
current_turn: Turn::MagentaPlayer, current_turn: Turn::MagentaPlayer,
phrase: None,
}; };
assert!(state.is_networked_multiplayer());
}
#[test]
fn test_board_string() {
let board = new_empty_board();
board[49].set(BoardState::Cyan);
board[51].set(BoardState::Cyan);
board[52].set(BoardState::Cyan);
board[53].set(BoardState::Cyan);
board[54].set(BoardState::Cyan);
board[55].set(BoardState::Magenta);
let (board_string, state_opt) = string_from_board(&board, 51);
let board_chars: Vec<char> = board_string.chars().collect();
assert_eq!(board_chars[49], 'b');
assert_eq!(board_chars[50], 'a');
assert_eq!(board_chars[51], 'h');
assert_eq!(board_chars[52], 'd');
assert_eq!(board_chars[53], 'd');
assert_eq!(board_chars[54], 'd');
assert_eq!(board_chars[55], 'c');
assert_eq!(state_opt, Some(BoardState::CyanWin));
board[49].set(BoardState::Magenta);
board[51].set(BoardState::Magenta);
board[52].set(BoardState::Magenta);
board[53].set(BoardState::Magenta);
board[54].set(BoardState::Magenta);
board[55].set(BoardState::Cyan);
let (board_string, state_opt) = string_from_board(&board, 51);
let board_chars: Vec<char> = board_string.chars().collect();
assert_eq!(board_chars[49], 'c');
assert_eq!(board_chars[50], 'a');
assert_eq!(board_chars[51], 'i');
assert_eq!(board_chars[52], 'e');
assert_eq!(board_chars[53], 'e');
assert_eq!(board_chars[54], 'e');
assert_eq!(board_chars[55], 'b');
assert_eq!(state_opt, Some(BoardState::MagentaWin));
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
{\rtf1\ansi\deff3\adeflang1025
{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f5\fnil\fprq2\fcharset0 Noto Sans CJK SC;}{\f6\fnil\fprq2\fcharset0 Noto Sans Devanagari;}{\f7\fswiss\fprq0\fcharset128 Noto Sans Devanagari;}}
{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}
{\stylesheet{\s0\snext0\rtlch\af6\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af8\langfe2052 Normal;}
{\s1\sbasedon15\snext16\rtlch\af6\afs36\ab \ltrch\hich\af4\loch\ilvl0\outlinelevel0\sb240\sa120\keepn\f4\fs36\b\dbch\af5 Heading 1;}
{\s15\sbasedon0\snext16\rtlch\af6\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af5 Heading;}
{\s16\sbasedon0\snext16\loch\sl276\slmult1\sb0\sa140 Text Body;}
{\s17\sbasedon16\snext17\rtlch\af7 \ltrch\loch\sl276\slmult1\sb0\sa140 List;}
{\s18\sbasedon0\snext18\rtlch\af7\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i Caption;}
{\s19\sbasedon0\snext19\rtlch\af7\alang255 \ltrch\lang255\langfe255\loch\noline\lang255\dbch\langfe255 Index;}
{\s20\sbasedon15\snext16\rtlch\af6\afs56\ab \ltrch\hich\af4\loch\qc\sb240\sa120\keepn\f4\fs56\b\dbch\af5 Title;}
}{\*\listtable{\list\listtemplateid1
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}\listid1}
}{\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}{\*\generator LibreOffice/7.3.2.2$Linux_X86_64 LibreOffice_project/30$Build-2}{\info{\creatim\yr2022\mo5\dy2\hr13\min45}{\revtim\yr2022\mo5\dy2\hr13\min53}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709
\hyphauto1\viewscale160
{\*\pgdsctbl
{\pgdsc0\pgdscuse451\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\pgdscnxt0 Default Page Style;}}
\formshade\paperh15840\paperw12240\margl1134\margr1134\margt1134\margb1134\sectd\sbknone\pgndec\sftnnar\saftnnrlc\sectunlocked1\pgwsxn12240\pghsxn15840\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\ftnbj\ftnstart1\ftnrstcont\ftnnar\aenddoc\aftnrstcont\aftnstart1\aftnnrlc
{\*\ftnsep\chftnsep}\pgndec\pard\plain \s20\rtlch\af6\afs56\ab \ltrch\hich\af4\loch\qc\sb240\sa120\keepn\f4\fs56\b\dbch\af5\loch\sb240\sa120\ltrpar{\loch
Sprint 6 Retrospective}
\par \pard\plain \s1\rtlch\af6\afs36\ab \ltrch\hich\af4\loch\ilvl0\outlinelevel0\sb240\sa120\keepn\f4\fs36\b\dbch\af5\loch{\listtext\pard\plain \tab}\ls1 \li0\ri0\lin0\rin0\fi0\ql\ltrpar{\loch
What was completed}
\par \pard\plain \s16\loch\sl276\slmult1\sb0\sa140\loch\ql\ltrpar{\loch
By the end of Sprint 6, almost all User Stories have been marked as done, except for one \u8220\'93Exciter\u8221\'94 User Story (\u8220\'93Board Column Emotes\u8221\'94).}
\par \pard\plain \s16\loch\sl276\slmult1\sb0\sa140\loch\ql\ltrpar{\loch
This Sprint marked the following User Stories as DONE: \u8220\'93End Game Options\u8221\'94, \u8220\'93AI Implementation\u8221\'94 (fine tuning AI), \u8220\'93In-game Emotes\u8221\'94, and \u8220\'93Multiplayer Phrase Pairing\u8221\'94.}
\par \pard\plain \s1\rtlch\af6\afs36\ab \ltrch\hich\af4\loch\ilvl0\outlinelevel0\sb240\sa120\keepn\f4\fs36\b\dbch\af5\loch{\listtext\pard\plain \tab}\ls1 \li0\ri0\lin0\rin0\fi0\ql\ltrpar{\loch
Thoughts}
\par \pard\plain \s16\loch\sl276\slmult1\sb0\sa140\loch\ql\ltrpar{\loch
It took some work adding the additional functionality for \u8220\'93In-game Emotes\u8221\'94 and \u8220\'93Multiplayer Phrase Pairing\u8221\'94 such that both protocol and database specifications had to be updated (and their usage in the back-end/front-end). In the end, it was nice to get nearly all of the User Stories marked as done. Setting up a MVP (Minimum Viable Product) and building upon it proved to be a good strategy for building up this project. The Scrum process facilitated the organization and set-up for the programming work, and has proven its usefulness.}
\par \pard\plain \s16\loch\sl276\slmult1\sb0\sa140\loch\ql\sb0\sa140\ltrpar{\loch
As this final Sprint focused more on \u8220\'93refinement\u8221\'94, the Sprint resulted in a few new (add-on) features and some refactoring/fixes.}
\par }

View file

@ -10,9 +10,14 @@ IDs, and paired state), and games in progress.
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
// fields should be self explanatory for the players table // fields should be self explanatory for the players table
// "phrase" is used to connect players with identical "phrase" text to make it
// easier to connect with the player one wants to play with
CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL, CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
game_id INTEGER, game_id INTEGER,
phrase TEXT,
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE); FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
// "cyan_player" and "magenta_player" should correspond to an existing entry in // "cyan_player" and "magenta_player" should correspond to an existing entry in
@ -32,6 +37,14 @@ CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL, FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL); FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
// "type" is one of the four possible emotes
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);
``` ```
"date" entries are used for garbage collection of the database. A predefined "date" entries are used for garbage collection of the database. A predefined

View file

@ -14,6 +14,15 @@ of the request, and the backend will respond with JSON.
} }
``` ```
An optional "phrase" parameter can be sent to match against other players with
the same "phrase".
```
{
"type": "pairing_request",
"phrase": "user_defined_phrase",
}
```
2. Check pairing status 2. Check pairing status
``` ```
@ -51,6 +60,16 @@ of the request, and the backend will respond with JSON.
} }
``` ```
6. Chat Emote Send:
```
{
"id": "id given by backend",
"type": "send_emote",
"emote": "smile", // or "frown", or "neutral", or "think"
}
```
## Responses ## Responses
1. Request ID Response 1. Request ID Response
@ -130,7 +149,7 @@ then the back-end will respond with "too\_many\_players".
// "opponent_disconnected", "internal_error" // "opponent_disconnected", "internal_error"
// "board" may not be in the response if "unknown_id" is the status // "board" may not be in the response if "unknown_id" is the status
"board": "abcdefg..." // 56-char long string with mapping: "board": "abcdefg...",// 56-char long string with mapping:
// a - empty // a - empty
// b - cyan // b - cyan
// c - magenta // c - magenta
@ -140,6 +159,21 @@ then the back-end will respond with "too\_many\_players".
// g - magenta placed // g - magenta placed
// h - cyan winning and placed piece // h - cyan winning and placed piece
// i - magenta winning and placed piece // i - magenta winning and placed piece
// optional "peer_emote" entry is message from opponent
"peer_emote": "smile",// or "frown", or "neutral", or "think"
// should always be available when "board" is available
"updated_time": "2022-04-30 12:00:00"
}
```
6. Send Emote Request Response
```
{
"type": "send_emote",
"status": "ok", // or "invalid_emote", "peer_disconnected",
// "internal_error"
} }
``` ```

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.