Compare commits

...

54 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 b7e0b522af Add "Refactoring and Code Improvement" constraint 2022-04-25 13:55:12 +09:00
Stephen Seo e54d239260 Update user stories, backlogs, prep for Sprint 6 2022-04-25 13:50:06 +09:00
Stephen Seo a7c67fd098 Redo NonTechnical_SimpleModel 2022-04-21 17:04:39 +09:00
Stephen Seo 975a878eb6 Update README.md 2022-04-21 13:38:48 +09:00
Stephen Seo f8da5f4997 Organize plans/specs 2022-04-21 13:37:09 +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
Stephen Seo 27ff10293a Update release_plan.md 2022-04-20 14:13:45 +09:00
Stephen Seo c98e4723bb Create non-technical release-plan and simple-model 2022-04-20 14:06:26 +09:00
Stephen Seo 73ac99b7cc Rename Scrum artifacts to be more accurate 2022-04-20 14:06:26 +09:00
Stephen Seo 381c5d3b29 Convert spreadsheets to ".xlsx" format (MS Office) 2022-04-16 18:48:57 +09:00
Stephen Seo fdbef5f6df Update product backlog 2022-04-15 17:33:50 +09:00
Stephen Seo 7a4821ac26 Add Sprint 5 Retrospective 2022-04-15 17:32:23 +09:00
Stephen Seo bb7d150196 Update Product backlog 2022-04-10 13:08:52 +09:00
Stephen Seo 6aa2927b42 Update Sprint 5 backlog 2022-04-10 13:04:41 +09:00
35 changed files with 5691 additions and 1277 deletions

View File

@ -12,15 +12,25 @@ The directory `back_end` holds a Rust project for the "back\_end" code. It holds
the server-side code for the project. It is mainly used to handle "Networked
Multiplayer" mode for the game.
The directory `specifications` holds defined specifications. It currently holds
the back-end specifications (database and protocol).
The directory `spreadsheets` hold LibreOffice Calc documents that are
spreadsheets organizing the work. There is a document for User Stories, a
document for the Product Backlogs, and there will be a document for each Sprint.
The directory `retrospectives` holds the retrospectives of Sprints 3 and onward.
The directory `plans` contains the release plans.
The directory `pictures` holds pertinent images to the project. It includes the
"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?
Four-Line Dropper is a game where two players take turns dropping tokens into
@ -29,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
Dropper" to avoid clashing with the game's original name that is trademarked
(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]
name = "four_line_dropper_backend"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
# 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/>.
use crate::ai::{get_ai_choice, AIDifficulty};
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::time::Duration;
use std::time::{Duration, Instant};
use std::{fmt, thread};
use rand::{thread_rng, Rng};
@ -27,7 +31,14 @@ pub type GetIDSenderType = (Option<u32>, Option<bool>);
/// third bool is if cyan player
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>;
@ -117,7 +128,10 @@ impl fmt::Display for DBPlaceError {
#[derive(Clone, Debug)]
pub enum DBHandlerRequest {
GetID(SyncSender<GetIDSenderType>),
GetID {
response_sender: SyncSender<GetIDSenderType>,
phrase: Option<String>,
},
CheckPairing {
id: u32,
response_sender: SyncSender<CheckPairingType>,
@ -135,6 +149,11 @@ pub enum DBHandlerRequest {
pos: usize,
response_sender: SyncSender<PlaceResultType>,
},
SendEmote {
id: u32,
emote_type: EmoteEnum,
response_sender: SyncSender<Result<(), ()>>,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -162,7 +181,10 @@ impl DBHandler {
}
let db_request = rx_recv_result.unwrap();
match db_request {
DBHandlerRequest::GetID(player_tx) => {
DBHandlerRequest::GetID {
response_sender,
phrase,
} => {
// got request to create new player, create new player
let conn_result = self.get_conn(DBFirstRun::NotFirstRun);
if let Err(e) = conn_result {
@ -171,10 +193,10 @@ impl DBHandler {
}
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 {
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
return false;
}
@ -196,11 +218,11 @@ impl DBHandler {
if paired {
// don't stop server on send fail, may have timed
// 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 {
// don't stop server on send fail, may have timed
// out and dropped the receiver
player_tx.send((Some(player_id), None)).ok();
response_sender.send((Some(player_id), None)).ok();
}
} else {
println!("Internal error, created player doesn't exist");
@ -233,7 +255,9 @@ impl DBHandler {
println!("{}", e);
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender.send((DBGameState::UnknownID, None)).ok();
response_sender
.send((DBGameState::UnknownID, None, None, None))
.ok();
return false;
}
// don't stop server on send fail, may have timed out and
@ -260,6 +284,23 @@ impl DBHandler {
// dropped the receiver
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
false
@ -274,6 +315,7 @@ impl DBHandler {
CREATE TABLE players (id INTEGER PRIMARY KEY NOT NULL,
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
game_id INTEGER,
phrase TEXT,
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
",
[],
@ -284,6 +326,9 @@ impl DBHandler {
}
} else if first_run == DBFirstRun::FirstRun {
println!("\"players\" table exists");
if let Err(e) = self.db_check_migration(&conn) {
println!("{}", e);
}
}
let result = conn.execute(
@ -307,17 +352,66 @@ impl DBHandler {
} else if first_run == DBFirstRun::FirstRun {
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)
} else {
Err(String::from("Failed to open connection"))
}
}
fn create_new_player(&self, conn: Option<&Connection>) -> Result<u32, String> {
if conn.is_none() {
return self.create_new_player(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
fn db_check_migration(&self, conn: &Connection) -> Result<(), String> {
let mut table_entries_stmt = conn
.prepare("PRAGMA table_info(players);")
.map_err(|e| format!("{:?}", e))?;
let mut table_entries_rows = table_entries_stmt
.query([])
.map_err(|e| format!("{:?}", e))?;
// check if "phrase" column exists
let mut phrase_exists = false;
while let Some(row) = table_entries_rows.next().map_err(|e| format!("{:?}", e))? {
let column_name: String = row.get(1).map_err(|e| format!("{:?}", e))?;
if column_name.contains("phrase") {
phrase_exists = true;
}
}
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, _> =
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 {
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> {
if conn.is_none() {
return self.pair_up_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
}
let conn = conn.unwrap();
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 to_pair: Option<u32> = None;
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))?;
let mut unpaired_players_rows = unpaired_players_stmt
.query([])
.map_err(|e| format!("{:?}", e))?;
let mut phrase_map: HashMap<String, u32> = HashMap::new();
while let Some(row) = unpaired_players_rows
.next()
.map_err(|e| format!("{:?}", e))?
{
if to_pair.is_none() {
to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?);
if let Ok(phrase_text) = row.get::<usize, String>(1) {
// pair players with matching phrases
if let Some(matching_player_id) = phrase_map.get(&phrase_text) {
let players: [u32; 2] = [
*matching_player_id,
row.get(0).map_err(|e| format!("{:?}", e))?,
];
self.create_game(Some(conn), &players)?;
phrase_map.remove(&phrase_text);
} else {
phrase_map.insert(phrase_text, row.get(0).map_err(|e| format!("{:?}", e))?);
}
} else {
let players: [u32; 2] = [
to_pair.take().unwrap(),
row.get(0).map_err(|e| format!("{:?}", e))?,
];
self.create_game(Some(conn), &players)?;
// pair players that did not use a phrase
if to_pair.is_none() {
to_pair = Some(row.get(0).map_err(|e| format!("{:?}", e))?);
} else {
let players: [u32; 2] = [
to_pair.take().unwrap(),
row.get(0).map_err(|e| format!("{:?}", e))?,
];
self.create_game(Some(conn), &players)?;
}
}
}
@ -388,10 +505,14 @@ impl DBHandler {
}
fn create_game(&self, conn: Option<&Connection>, players: &[u32; 2]) -> Result<u32, String> {
if conn.is_none() {
return self.create_game(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), players);
}
let conn = conn.unwrap();
let mut _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 game_id: u32 = thread_rng().gen();
{
let mut get_game_stmt = conn
@ -435,13 +556,13 @@ impl DBHandler {
}
}
if conn.is_none() {
return self.check_if_player_is_paired(
Some(&self.get_conn(DBFirstRun::NotFirstRun)?),
player_id,
);
}
let conn = conn.unwrap();
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 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 {
@ -478,11 +599,14 @@ impl DBHandler {
conn: Option<&Connection>,
player_id: u32,
) -> Result<bool, String> {
if conn.is_none() {
return self
.check_if_player_exists(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id);
}
let conn = conn.unwrap();
let 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 check_player_row: Result<u32, _> =
conn.query_row("SELECT id FROM players WHERE id = ?;", [player_id], |row| {
row.get(0)
@ -499,13 +623,13 @@ impl DBHandler {
conn: Option<&Connection>,
player_id: u32,
) -> Result<bool, String> {
if conn.is_none() {
return self.check_if_player_in_game(
Some(&self.get_conn(DBFirstRun::NotFirstRun)?),
player_id,
);
}
let conn = conn.unwrap();
let 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 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;",
@ -523,68 +647,128 @@ impl DBHandler {
conn: Option<&Connection>,
player_id: u32,
) -> Result<BoardStateType, String> {
if conn.is_none() {
return self.get_board_state(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id);
let 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 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
let row_result: Result<(String, i64, Option<u32>, Option<u32>), RusqliteError> = conn.query_row(
"SELECT games.board, games.status, games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;",
type ResultTuple = (String, i64, Option<u32>, Option<u32>, String);
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],
|row| {
let board_result = row.get(0);
let status_result = row.get(1);
let cyan_player = row.get(2);
let magenta_player = row.get(3);
if board_result.is_ok() && status_result.is_ok() && cyan_player.is_ok() && magenta_player.is_ok() {
if let (Ok(board), Ok(status), Ok(cyan_id), Ok(magenta_id)) = (board_result, status_result, cyan_player, magenta_player) {
Ok((board, status, cyan_id, magenta_id))
let updated_time = row.get(4);
if board_result.is_ok() && status_result.is_ok() && cyan_player.is_ok() && magenta_player.is_ok() && updated_time.is_ok() {
if let (Ok(board), Ok(status), Ok(cyan_id), Ok(magenta_id), Ok(updated_time)) = (board_result, status_result, cyan_player, magenta_player, updated_time) {
Ok((board, status, cyan_id, magenta_id, updated_time))
} else {
unreachable!("Both row items should be Ok");
unreachable!("All row items should be Ok");
}
} else if board_result.is_err() {
board_result
.map(|_| (String::from("this value should never be returned"), 0, None, None))
.map(|_| (String::from("this value should never be returned"), 0, None, None, String::new()))
} else if status_result.is_err() {
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() {
cyan_player
.map(|_| (String::from("this value should never be returned"), 0, None, None))
} else {
.map(|_| (String::from("this value should never be returned"), 0, None, None, String::new()))
} else if magenta_player.is_err() {
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 {
// 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() {
// One player disconnected
self.disconnect_player(Some(conn), player_id).ok();
// Remove the game(s) with disconnected players
if self.clear_empty_games(Some(conn)).is_err() {
Ok((DBGameState::InternalError, None))
Ok((
DBGameState::InternalError,
None,
Some(updated_time),
received_emote,
))
} else if status == 2 || status == 3 {
Ok((DBGameState::from(status), Some(board)))
Ok((
DBGameState::from(status),
Some(board),
Some(updated_time),
received_emote,
))
} else {
Ok((DBGameState::OpponentDisconnected, Some(board)))
Ok((
DBGameState::OpponentDisconnected,
Some(board),
Some(updated_time),
received_emote,
))
}
} else {
// 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 {
// No rows is either player doesn't exist or not paired
let (exists, is_paired, _is_cyan) =
self.check_if_player_is_paired(Some(conn), player_id)?;
if !exists {
Ok((DBGameState::UnknownID, None))
Ok((DBGameState::UnknownID, None, None, received_emote))
} else if !is_paired {
Ok((DBGameState::NotPaired, None))
Ok((DBGameState::NotPaired, None, None, received_emote))
} else {
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> {
if conn.is_none() {
return self
.disconnect_player(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id);
}
let conn = conn.unwrap();
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 stmt_result = conn.execute("DELETE FROM players WHERE id = ?;", [player_id]);
if let Ok(1) = stmt_result {
@ -610,10 +796,13 @@ impl DBHandler {
}
fn clear_empty_games(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() {
return self.clear_empty_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
}
let conn = conn.unwrap();
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()
};
// Only fails if no rows were removed, and that is not an issue
conn.execute(
@ -631,18 +820,13 @@ impl DBHandler {
player_id: u32,
pos: usize,
) -> PlaceResultType {
if conn.is_none() {
return self.place_token(
Some(
&self
.get_conn(DBFirstRun::NotFirstRun)
.map_err(|_| DBPlaceError::InternalError)?,
),
player_id,
pos,
);
}
let conn = conn.unwrap();
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()
};
// check if player exists
let player_exist_check_result = self.check_if_player_exists(Some(conn), player_id);
@ -730,33 +914,21 @@ impl DBHandler {
}
2 => {
// game over, cyan won
self.disconnect_player(Some(conn), player_id).ok();
if self.clear_empty_games(Some(conn)).is_err() {
return Err(DBPlaceError::InternalError);
}
return Ok((DBPlaceStatus::GameEndedCyanWon, Some(board_string)));
}
3 => {
// game over, magenta won
self.disconnect_player(Some(conn), player_id).ok();
if self.clear_empty_games(Some(conn)).is_err() {
return Err(DBPlaceError::InternalError);
}
return Ok((DBPlaceStatus::GameEndedMagentaWon, Some(board_string)));
}
4 => {
// game over, draw
self.disconnect_player(Some(conn), player_id).ok();
if self.clear_empty_games(Some(conn)).is_err() {
return Err(DBPlaceError::InternalError);
}
return Ok((DBPlaceStatus::GameEndedDraw, Some(board_string)));
}
_ => (),
}
// get board state
let board = board_from_string(board_string);
let board = board_from_string(&board_string);
// find placement position or return "illegal move" if unable to
let mut final_pos = pos;
@ -782,7 +954,7 @@ impl DBHandler {
}
// 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
let update_result = if ended_state_opt.is_none() {
@ -813,7 +985,6 @@ impl DBHandler {
}
if let Some(ended_state) = ended_state_opt {
self.disconnect_player(Some(conn), player_id).ok();
Ok((
match ended_state {
BoardState::Empty => DBPlaceStatus::GameEndedDraw,
@ -862,7 +1033,7 @@ impl DBHandler {
for row_result in rows {
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 {
unreachable!("This part should never execute");
}
@ -876,7 +1047,7 @@ impl DBHandler {
conn: Option<&Connection>,
game_id: u32,
status: u32,
board_string: String,
board_string: &str,
) -> Result<(), String> {
if status > 1 {
return Err(String::from(
@ -884,15 +1055,13 @@ impl DBHandler {
));
}
if conn.is_none() {
return self.have_ai_take_players_turn(
Some(&self.get_conn(DBFirstRun::NotFirstRun)?),
game_id,
status,
board_string,
);
}
let conn = conn.unwrap();
let 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 is_cyan = status == 0;
let board = board_from_string(board_string);
@ -931,7 +1100,7 @@ impl DBHandler {
});
// 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;
if let Some(board_state) = end_state_opt {
@ -958,10 +1127,13 @@ impl DBHandler {
}
fn cleanup_stale_games(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() {
return self.cleanup_stale_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
}
let conn = conn.unwrap();
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 games WHERE unixepoch() - unixepoch(date_added) > ?;",
@ -973,10 +1145,13 @@ impl DBHandler {
}
fn cleanup_stale_players(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() {
return self.cleanup_stale_players(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
}
let conn = conn.unwrap();
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 players WHERE unixepoch() - unixepoch(date_added) > ? AND game_id ISNULL;",
@ -986,6 +1161,79 @@ impl DBHandler {
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(
@ -1007,6 +1255,8 @@ pub fn start_db_handler_thread(
return;
}
let mut cleanup_instant = Instant::now();
let cleanup_duration = Duration::from_secs(BACKEND_CLEANUP_INTERVAL_SECONDS);
'outer: loop {
if handler.handle_request() {
handler.shutdown_tx.send(()).ok();
@ -1017,11 +1267,21 @@ pub fn start_db_handler_thread(
println!("{}", e);
}
if let Err(e) = handler.cleanup_stale_games(None) {
println!("{}", e);
}
if let Err(e) = handler.cleanup_stale_players(None) {
println!("{}", e);
if cleanup_instant.elapsed() > cleanup_duration {
let conn = handler.get_conn(DBFirstRun::NotFirstRun).ok();
cleanup_instant = Instant::now();
if let Err(e) = handler.cleanup_stale_games(conn.as_ref()) {
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.
//
//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::{
sync::mpsc::{sync_channel, SyncSender},
@ -24,11 +28,12 @@ pub fn handle_json(
) -> Result<String, String> {
if let Some(Value::String(type_str)) = root.get("type") {
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),
"place_token" => handle_place_token(root, tx),
"disconnect" => handle_disconnect(root, tx),
"game_state" => handle_game_state(root, tx),
"send_emote" => handle_send_emote(root, tx),
_ => Err("{\"type\":\"invalid_type\"}".into()),
}
} 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);
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());
}
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());
}
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 {
Ok(format!(
"{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\"}}",
db_game_state, board_string
))
let updated_time = if let Some(time_string) = updated_time_opt {
time_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 {
Ok(format!(
"{{\"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())
}
}
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]]
name = "autocfg"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "boolinator"
@ -16,9 +16,9 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bumpalo"
version = "3.9.1"
version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "cfg-if"
@ -71,10 +71,11 @@ dependencies = [
[[package]]
name = "gloo-console"
version = "0.2.1"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3907f786f65bbb4f419e918b0c5674175ef1c231ecda93b2dbd65fd1e8882637"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"wasm-bindgen",
@ -83,9 +84,9 @@ dependencies = [
[[package]]
name = "gloo-dialogs"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffb557a2ea2ed283f1334423d303a336fad55fb8572d51ae488f828b1464b40"
checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
dependencies = [
"wasm-bindgen",
"web-sys",
@ -93,9 +94,9 @@ dependencies = [
[[package]]
name = "gloo-events"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [
"wasm-bindgen",
"web-sys",
@ -103,9 +104,9 @@ dependencies = [
[[package]]
name = "gloo-file"
version = "0.2.1"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa5d6084efa4a2b182ef3a8649cb6506cb4843f22cf907c6e0a799944248ae90"
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [
"gloo-events",
"js-sys",
@ -115,9 +116,9 @@ dependencies = [
[[package]]
name = "gloo-render"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b4cda6e149df3bb4a3c6a343873903e5bcc2448a9877d61bb8274806ad67f6e"
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
dependencies = [
"wasm-bindgen",
"web-sys",
@ -125,9 +126,9 @@ dependencies = [
[[package]]
name = "gloo-storage"
version = "0.2.0"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5057761927af1b1929d02b1f49cf83553dd347a473ee7c8bb08420f2673ffc"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils",
"js-sys",
@ -140,9 +141,9 @@ dependencies = [
[[package]]
name = "gloo-timers"
version = "0.2.3"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -150,26 +151,28 @@ dependencies = [
[[package]]
name = "gloo-utils"
version = "0.1.2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c77af6f96a4f9e27c8ac23a88407381a31f4a74c3fb985c85aa79b8d898136"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "indexmap"
version = "1.8.0"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
@ -177,15 +180,15 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.1"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.56"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
@ -198,12 +201,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "log"
version = "0.4.14"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oorandom"
@ -220,7 +226,7 @@ dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"syn 1.0.109",
"version_check",
]
@ -237,59 +243,59 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.36"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [
"unicode-xid",
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.15"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.9"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "scoped-tls-hkt"
version = "0.1.2"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
checksum = "3ddc765d3410d9f6c6ca071bf0b67f6b01e3ec4595dc3892f02677e75819dddc"
[[package]]
name = "serde"
version = "1.0.136"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.58",
]
[[package]]
name = "serde_json"
version = "1.0.79"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
@ -298,46 +304,60 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.5"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "1.0.86"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"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]]
name = "thiserror"
version = "1.0.30"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.58",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
@ -347,9 +367,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"serde",
@ -359,24 +379,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 2.0.58",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.29"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395"
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
@ -386,9 +406,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -396,22 +416,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.58",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "wasm-logger"
@ -426,9 +446,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.56"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -464,5 +484,5 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
"syn 1.0.109",
]

View File

@ -9,7 +9,7 @@ edition = "2021"
yew = "0.19"
log = "0.4.6"
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"
oorandom = "11.1.3"
wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] }

View File

@ -42,9 +42,50 @@
grid-row: 2;
grid-column: 3;
}
button.menuMultiplayer {
div.multiplayerMenu {
grid-row: 2;
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 {
color: #FFF;
@ -188,6 +229,12 @@
opacity: 1;
}
}
button#resetbutton {
background-color: #C55;
color: #FFF;
grid-row: 2;
grid-column: 8;
}
</style>
</head>
</html>

View File

@ -13,6 +13,9 @@ use crate::game_logic::check_win_draw;
use crate::random_helper::get_seeded_random;
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)]
pub enum AIDifficulty {
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
if get_block_amount(player.get_opposite(), idx, 2, board) {
utility *= 1.5;
if utility >= 0.8 {
utility = 0.8;
utility *= 1.22;
if utility >= AI_THIRD_MAX_UTILITY {
utility = AI_THIRD_MAX_UTILITY;
}
}
// check if placing a token here blocks 2 pieces
if get_block_amount(player, idx, 2, board) {
utility *= 1.2;
if utility >= 0.8 {
utility = 0.8;
utility *= 1.11;
if utility >= AI_THIRD_MAX_UTILITY {
utility = AI_THIRD_MAX_UTILITY;
}
}
// check if placing a token here connects 1 piece
if get_block_amount(player.get_opposite(), idx, 1, board) {
utility *= 1.09;
if utility >= 0.8 {
utility = 0.8;
utility *= 1.05;
if utility >= AI_THIRD_MAX_UTILITY {
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
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 COLS: u8 = 7;
#[allow(dead_code)]
pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
pub const AI_EASY_MAX_CHOICES: usize = 5;
pub const AI_NORMAL_MAX_CHOICES: usize = 3;
#[allow(dead_code)]
pub const AI_CHOICE_DURATION_MILLIS: i32 = 1000;
#[allow(dead_code)]
pub const PLAYER_COUNT_LIMIT: usize = 1000;
#[allow(dead_code)]
pub const TURN_SECONDS: u64 = 25;
#[allow(dead_code)]
pub const GAME_CLEANUP_TIMEOUT: u64 = (TURN_SECONDS + 1) * ((ROWS * COLS) as u64 + 5u64);
#[allow(dead_code)]
pub const PLAYER_CLEANUP_TIMEOUT: u64 = 300;
#[allow(dead_code)]
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
#[allow(dead_code)]
#[cfg(debug_assertions)]
pub const BACKEND_URL: &str = "http://testlocalhost/api";
#[allow(dead_code)]
#[cfg(not(debug_assertions))]
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 wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, Document, Request, RequestInit, Window};
use web_sys::{window, Document, Window};
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))
}
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> {
let mut send_json_string = String::from("{");
for (key, value) in entries {

View File

@ -12,12 +12,13 @@ use crate::game_logic::{check_win_draw, WinType};
use serde::{Deserialize, Serialize};
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use std::collections::hash_set::HashSet;
use std::fmt::Display;
use std::rc::Rc;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GameState {
MainMenu,
SinglePlayer(Turn, AIDifficulty),
@ -26,38 +27,45 @@ pub enum GameState {
paired: bool,
current_side: Option<Turn>,
current_turn: Turn,
phrase: Option<String>,
},
PostGameResults(BoardState),
}
impl GameState {
pub fn is_networked_multiplayer(self) -> bool {
#[allow(dead_code)]
pub fn is_networked_multiplayer(&self) -> bool {
matches!(
self,
*self,
GameState::NetworkedMultiplayer {
paired: _,
current_side: _,
current_turn: _
current_turn: _,
phrase: _,
}
)
}
#[allow(dead_code)]
pub fn set_networked_paired(&mut self) {
if let GameState::NetworkedMultiplayer {
ref mut paired,
current_side: _,
current_turn: _,
phrase: _,
} = self
{
*paired = true;
}
}
#[allow(dead_code)]
pub fn get_networked_current_side(&self) -> Option<Turn> {
if let GameState::NetworkedMultiplayer {
paired: _,
current_side,
current_turn: _,
phrase: _,
} = *self
{
current_side
@ -66,17 +74,20 @@ impl GameState {
}
}
#[allow(dead_code)]
pub fn set_networked_current_side(&mut self, side: Option<Turn>) {
if let GameState::NetworkedMultiplayer {
paired: _,
ref mut current_side,
current_turn: _,
phrase: _,
} = self
{
*current_side = side;
}
}
#[allow(dead_code)]
pub fn get_current_turn(&self) -> Turn {
if let GameState::SinglePlayer(turn, _) = *self {
turn
@ -84,6 +95,7 @@ impl GameState {
paired: _,
current_side: _,
current_turn,
phrase: _,
} = *self
{
current_turn
@ -92,29 +104,42 @@ impl GameState {
}
}
pub fn get_network_current_side(&self) -> Option<Turn> {
if let GameState::NetworkedMultiplayer {
paired: _,
current_side,
current_turn: _,
} = *self
{
current_side
} else {
None
}
}
#[allow(dead_code)]
pub fn set_networked_current_turn(&mut self, turn: Turn) {
if let GameState::NetworkedMultiplayer {
paired: _,
current_side: _,
ref mut current_turn,
phrase: _,
} = self
{
*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 {
@ -128,15 +153,17 @@ impl From<MainMenuMessage> for GameState {
match msg {
MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai),
MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer,
MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer {
MainMenuMessage::NetworkedMultiplayer(phrase) => GameState::NetworkedMultiplayer {
paired: false,
current_side: None,
current_turn: Turn::CyanPlayer,
phrase,
},
}
}
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum BoardState {
Empty,
@ -174,10 +201,12 @@ impl From<Turn> for BoardState {
}
impl BoardState {
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
*self == BoardState::Empty
}
#[allow(dead_code)]
pub fn is_win(self) -> bool {
match self {
BoardState::Empty | BoardState::Cyan | BoardState::Magenta => false,
@ -185,6 +214,7 @@ impl BoardState {
}
}
#[allow(dead_code)]
pub fn into_win(self) -> Self {
match self {
BoardState::Empty => BoardState::Empty,
@ -193,8 +223,9 @@ impl BoardState {
}
}
pub fn from_win(&self) -> Self {
match *self {
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn from_win(self) -> Self {
match self {
BoardState::Empty => BoardState::Empty,
BoardState::Cyan | BoardState::CyanWin => BoardState::Cyan,
BoardState::Magenta | BoardState::MagentaWin => BoardState::Magenta,
@ -202,6 +233,7 @@ impl BoardState {
}
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Turn {
CyanPlayer,
@ -227,6 +259,7 @@ impl From<BoardState> for Turn {
}
impl Turn {
#[allow(dead_code)]
pub fn get_color(&self) -> &str {
match *self {
Turn::CyanPlayer => "cyan",
@ -244,6 +277,7 @@ impl Turn {
pub type BoardType = [Rc<Cell<BoardState>>; 56];
#[allow(dead_code)]
pub fn new_empty_board() -> BoardType {
[
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 {
let cloned_board = new_empty_board();
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];
#[allow(dead_code)]
pub fn new_placed() -> PlacedType {
[
Rc::new(Cell::new(false)),
@ -377,10 +413,11 @@ pub fn new_placed() -> PlacedType {
]
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub struct SharedState {
pub board: BoardType,
pub game_state: Rc<Cell<GameState>>,
pub game_state: Rc<RefCell<GameState>>,
pub turn: Rc<Cell<Turn>>,
pub placed: PlacedType,
}
@ -390,7 +427,7 @@ impl Default for SharedState {
Self {
// cannot use [<type>; 56] because Rc does not impl Copy
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)),
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
// dependencies on the yew_components module
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MainMenuMessage {
SinglePlayer(Turn, AIDifficulty),
LocalMultiplayer,
NetworkedMultiplayer,
NetworkedMultiplayer(Option<String>),
}
#[allow(dead_code)]
pub fn new_string_board() -> String {
let mut board = String::with_capacity(56);
for _i in 0..56 {
@ -414,7 +453,8 @@ pub fn new_string_board() -> String {
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();
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
/// 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);
// check for winning pieces
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 {
match win_type {
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() {
(board_string, Some(BoardState::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(),
if board_string.chars().collect::<Vec<char>>()[*win_set.iter().next().unwrap()] == 'd' {
if winning_char == 'd' || winning_char == 'h' {
Some(BoardState::CyanWin)
} else {
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)]
pub struct PairingRequestResponse {
pub r#type: String,
@ -526,6 +570,7 @@ pub struct PairingRequestResponse {
pub color: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)]
pub struct PairingStatusResponse {
pub r#type: String,
@ -533,13 +578,17 @@ pub struct PairingStatusResponse {
pub color: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)]
pub struct GameStateResponse {
pub r#type: String,
pub status: String,
pub board: Option<String>,
pub peer_emote: Option<String>,
pub updated_time: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)]
pub struct PlaceTokenResponse {
pub r#type: String,
@ -547,6 +596,14 @@ pub struct PlaceTokenResponse {
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)]
pub enum NetworkedGameState {
CyanTurn,
@ -560,6 +617,7 @@ pub enum NetworkedGameState {
UnknownID,
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PlacedEnum {
Accepted,
@ -568,6 +626,62 @@ pub enum PlacedEnum {
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)]
mod tests {
use super::*;
@ -582,12 +696,59 @@ mod tests {
paired: false,
current_side: None,
current_turn: Turn::CyanPlayer,
phrase: None,
};
assert!(state.is_networked_multiplayer());
let state = GameState::NetworkedMultiplayer {
paired: true,
current_side: Some(Turn::CyanPlayer),
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

18
plans/release_plan.md Normal file
View File

@ -0,0 +1,18 @@
# Release Plan
The first Sprint will focus on developing the Minimum Viable Product (MVP).
Every Sprint afterwards will iterate on the MVP until it reaches enough
maturity, or in other words, has implemented all the main features.
After the main features have been implemented, every Sprint afterwards will
refine the product. See the `technical_release_plan.md` for technical details.
# Sprints
The first Sprint will focus on creating the prototype that will serve as the
Minimum Viable Product (MVP).
Every Sprint afterwards will choose and work on User Stories that are ideally
the highest priority (at the time) and of lowest estimated size (priority takes
precedence, but low sizes are ideal).

View File

@ -4,7 +4,7 @@ This Release Plan was created in April, a few weeks after the project weas
actually started. However, the intended plan for releases has not changed during
the creation of the project.
# Release Plan
# Technical Release Plan
## First Stage: Web-app Minimum Viable Product front-end

View File

@ -0,0 +1,42 @@
{\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\fswiss\fprq0\fcharset128 Noto Sans Devanagari;}{\f7\fnil\fprq2\fcharset0 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\af7\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af8\langfe2052 Normal;}
{\s1\sbasedon16\snext17\rtlch\af7\afs36\ab \ltrch\hich\af4\loch\ilvl0\outlinelevel0\sb240\sa120\keepn\f4\fs36\b\dbch\af5 Heading 1;}
{\*\cs15\snext15\rtlch\alang255 \ltrch\lang255\langfe255\loch\cf9\lang255\ul\ulc0\dbch\langfe255 Hyperlink;}
{\s16\sbasedon0\snext17\rtlch\af7\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af5 Heading;}
{\s17\sbasedon0\snext17\loch\sl276\slmult1\sb0\sa140 Text Body;}
{\s18\sbasedon17\snext18\rtlch\af6 \ltrch\loch\sl276\slmult1\sb0\sa140 List;}
{\s19\sbasedon0\snext19\rtlch\af6\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i Caption;}
{\s20\sbasedon0\snext20\rtlch\af6\alang255 \ltrch\lang255\langfe255\loch\noline\lang255\dbch\langfe255 Index;}
{\s21\sbasedon16\snext17\rtlch\af7\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\mo4\dy15\hr17\min25}{\revtim\yr2022\mo4\dy15\hr17\min31}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709
\hyphauto1\viewscale130
{\*\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 \s21\rtlch\af7\afs56\ab \ltrch\hich\af4\loch\qc\sb240\sa120\keepn\f4\fs56\b\dbch\af5\loch\sb240\sa120\ltrpar{\loch
Sprint 5 Retrospective}
\par \pard\plain \s1\rtlch\af7\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 done this Sprint}
\par \pard\plain \s17\loch\sl276\slmult1\sb0\sa140\loch\ql\ltrpar{\loch
The connection between the back-end and the front-end was set up (with some bug-fixes along the way). This part wasn\u8217\'92t difficult because of the previously planned out back-end protocol made it easier to integrate the connection to the back-end in the front-end. There was an issue at first connecting to the back-end, but it was discovered that it was a browser \u8220\'93feature\u8221\'94 involving \u8220\'93CORS\u8221\'94 (I think) where basically the back-end and the front-end needed to be hosted by the same server (and port), otherwise the front-end would fail to connect to the back-end. This required setting up a testing environment that set this up properly, which also would reflect how the deployed set-up would work (front-end at \u8220\'93}{{\field{\*\fldinst HYPERLINK "https://asdm.seodisparate.com/game" }{\fldrslt {\loch\rtlch\alang255 \ltrch\lang255\langfe255\loch\cf9\lang255\ul\ulc0\dbch\langfe255\loch
https://asdm.seodisparate.com/game}{}}}\loch
\u8221\'94 and back-end at \u8220\'93}{{\field{\*\fldinst HYPERLINK "https://asdm.seodisparate.com/api" }{\fldrslt {\loch\rtlch\alang255 \ltrch\lang255\langfe255\loch\cf9\lang255\ul\ulc0\dbch\langfe255\loch
https://asdm.seodisparate.com/api}{}}}\loch
\u8221\'94).}
\par \pard\plain \s1\rtlch\af7\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\u8217\'92s Left}
\par \pard\plain \s17\loch\sl276\slmult1\sb0\sa140\loch\ql\sb0\sa140\ltrpar{\loch
Now that all the main features of the game have been implemented, what\u8217\'92s left is refinement and possible implementation of \u8220\'93nice-to-have\u8221\'94 features.}
\par }

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;
// 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,
date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
game_id INTEGER,
phrase TEXT,
FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE);
// "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,
FOREIGN KEY(cyan_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

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
```
@ -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
1. Request ID Response
@ -130,7 +149,7 @@ then the back-end will respond with "too\_many\_players".
// "opponent_disconnected", "internal_error"
// "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
// b - cyan
// c - magenta
@ -140,6 +159,21 @@ then the back-end will respond with "too\_many\_players".
// g - magenta placed
// h - cyan 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.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rpt="http://openoffice.org/2005/report" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:xforms="http://www.w3.org/2002/xforms" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
<office:meta><meta:creation-date>2022-02-27T14:56:15.671560282</meta:creation-date><meta:generator>LibreOffice/7.3.1.3$Linux_X86_64 LibreOffice_project/30$Build-3</meta:generator><dc:date>2022-03-13T22:01:54.524481726</dc:date><meta:editing-duration>PT2H8M22S</meta:editing-duration><meta:editing-cycles>38</meta:editing-cycles><meta:document-statistic meta:table-count="1" meta:cell-count="298" meta:object-count="0"/></office:meta>
<office:meta><meta:creation-date>2022-02-27T14:56:15.671560282</meta:creation-date><meta:generator>LibreOffice/7.3.2.2$Linux_X86_64 LibreOffice_project/30$Build-2</meta:generator><dc:date>2022-04-25T13:54:57.087569254</dc:date><meta:editing-duration>PT2H15M22S</meta:editing-duration><meta:editing-cycles>40</meta:editing-cycles><meta:document-statistic meta:table-count="1" meta:cell-count="316" meta:object-count="0"/></office:meta>
<office:settings>
<config:config-item-set config:name="ooo:view-settings">
<config:config-item config:name="VisibleAreaTop" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaLeft" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaWidth" config:type="int">46383</config:config-item>
<config:config-item config:name="VisibleAreaHeight" config:type="int">36844</config:config-item>
<config:config-item config:name="VisibleAreaHeight" config:type="int">40502</config:config-item>
<config:config-item-map-indexed config:name="Views">
<config:config-item-map-entry>
<config:config-item config:name="ViewId" config:type="string">view1</config:config-item>
<config:config-item-map-named config:name="Tables">
<config:config-item-map-entry config:name="User Stories">
<config:config-item config:name="CursorPositionX" config:type="int">12</config:config-item>
<config:config-item config:name="CursorPositionY" config:type="int">23</config:config-item>
<config:config-item config:name="CursorPositionX" config:type="int">5</config:config-item>
<config:config-item config:name="CursorPositionY" config:type="int">27</config:config-item>
<config:config-item config:name="HorizontalSplitMode" config:type="short">2</config:config-item>
<config:config-item config:name="VerticalSplitMode" config:type="short">2</config:config-item>
<config:config-item config:name="HorizontalSplitPosition" config:type="int">1</config:config-item>
<config:config-item config:name="VerticalSplitPosition" config:type="int">1</config:config-item>
<config:config-item config:name="ActiveSplitRange" config:type="short">3</config:config-item>
<config:config-item config:name="PositionLeft" config:type="int">0</config:config-item>
<config:config-item config:name="PositionRight" config:type="int">5</config:config-item>
<config:config-item config:name="PositionRight" config:type="int">1</config:config-item>
<config:config-item config:name="PositionTop" config:type="int">0</config:config-item>
<config:config-item config:name="PositionBottom" config:type="int">1</config:config-item>
<config:config-item config:name="PositionBottom" config:type="int">18</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
@ -32,7 +32,7 @@
</config:config-item-map-entry>
</config:config-item-map-named>
<config:config-item config:name="ActiveTable" config:type="string">User Stories</config:config-item>
<config:config-item config:name="HorizontalScrollbarWidth" config:type="int">1203</config:config-item>
<config:config-item config:name="HorizontalScrollbarWidth" config:type="int">2483</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
@ -357,7 +357,7 @@
<text:p><text:sheet-name>???</text:sheet-name><text:s/>(<text:title>???</text:title>)</text:p>
</style:region-left>
<style:region-right>
<text:p><text:date style:data-style-name="N2" text:date-value="2022-03-13">00/00/0000</text:date>, <text:time style:data-style-name="N2" text:time-value="21:56:11.588245896">00:00:00</text:time></text:p>
<text:p><text:date style:data-style-name="N2" text:date-value="2022-04-25">00/00/0000</text:date>, <text:time style:data-style-name="N2" text:time-value="13:53:35.055125671">00:00:00</text:time></text:p>
</style:region-right>
</style:header>
<style:header-left style:display="false"/>
@ -442,17 +442,17 @@
<table:table-cell table:style-name="ce45" office:value-type="string" calcext:value-type="string">
<text:p>Mandatory</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$G3:.$G100])" office:value-type="float" office:value="800" calcext:value-type="float">
<text:p>800</text:p>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$G3:.$G100])" office:value-type="float" office:value="845" calcext:value-type="float">
<text:p>845</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$H3:.$H100])" office:value-type="float" office:value="174" calcext:value-type="float">
<text:p>174</text:p>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$H3:.$H100])" office:value-type="float" office:value="182" calcext:value-type="float">
<text:p>182</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$I3:.$I100])" office:value-type="float" office:value="148" calcext:value-type="float">
<text:p>148</text:p>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$I3:.$I100])" office:value-type="float" office:value="151" calcext:value-type="float">
<text:p>151</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$H2:.$I2])" office:value-type="float" office:value="322" calcext:value-type="float">
<text:p>322</text:p>
<table:table-cell table:style-name="ce45" table:formula="of:=SUM([.$H2:.$I2])" office:value-type="float" office:value="333" calcext:value-type="float">
<text:p>333</text:p>
</table:table-cell>
<table:table-cell table:number-columns-repeated="2" table:style-name="ce51" office:value-type="percentage" office:value="1" calcext:value-type="percentage">
<text:p>100.00%</text:p>
@ -490,14 +490,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H3:.$I3])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J3]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J3]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G3]/[.$G$2]" office:value-type="percentage" office:value="0.0125" calcext:value-type="percentage">
<text:p>1.25%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G3]/[.$G$2]" office:value-type="percentage" office:value="0.0118343195266272" calcext:value-type="percentage">
<text:p>1.18%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K3]/[.$L3]" office:value-type="float" office:value="3.9751552795031" calcext:value-type="float">
<text:p>3.9751552795031</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K3]/[.$L3]" office:value-type="float" office:value="4.06006006006006" calcext:value-type="float">
<text:p>4.06006006006006</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -527,14 +527,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H4:.$I4])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J4]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J4]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G4]/[.$G$2]" office:value-type="percentage" office:value="0.05" calcext:value-type="percentage">
<text:p>5.00%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G4]/[.$G$2]" office:value-type="percentage" office:value="0.0473372781065089" calcext:value-type="percentage">
<text:p>4.73%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K4]/[.$L4]" office:value-type="float" office:value="0.993788819875776" calcext:value-type="float">
<text:p>0.993788819875776</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K4]/[.$L4]" office:value-type="float" office:value="1.01501501501502" calcext:value-type="float">
<text:p>1.01501501501502</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -567,14 +567,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H5:.$I5])" office:value-type="float" office:value="8" calcext:value-type="float">
<text:p>8</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J5]/[.$J$2]" office:value-type="percentage" office:value="0.0248447204968944" calcext:value-type="percentage">
<text:p>2.48%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J5]/[.$J$2]" office:value-type="percentage" office:value="0.024024024024024" calcext:value-type="percentage">
<text:p>2.40%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G5]/[.$G$2]" office:value-type="percentage" office:value="0.04625" calcext:value-type="percentage">
<text:p>4.63%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G5]/[.$G$2]" office:value-type="percentage" office:value="0.0437869822485207" calcext:value-type="percentage">
<text:p>4.38%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K5]/[.$L5]" office:value-type="float" office:value="0.537183145878798" calcext:value-type="float">
<text:p>0.537183145878798</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K5]/[.$L5]" office:value-type="float" office:value="0.548656764872981" calcext:value-type="float">
<text:p>0.548656764872981</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -607,14 +607,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H6:.$I6])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J6]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J6]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G6]/[.$G$2]" office:value-type="percentage" office:value="0.01625" calcext:value-type="percentage">
<text:p>1.63%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G6]/[.$G$2]" office:value-type="percentage" office:value="0.0153846153846154" calcext:value-type="percentage">
<text:p>1.54%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K6]/[.$L6]" office:value-type="float" office:value="3.05781175346393" calcext:value-type="float">
<text:p>3.05781175346393</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K6]/[.$L6]" office:value-type="float" office:value="3.12312312312312" calcext:value-type="float">
<text:p>3.12312312312312</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -644,14 +644,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H7:.$I7])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J7]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J7]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G7]/[.$G$2]" office:value-type="percentage" office:value="0.0225" calcext:value-type="percentage">
<text:p>2.25%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G7]/[.$G$2]" office:value-type="percentage" office:value="0.021301775147929" calcext:value-type="percentage">
<text:p>2.13%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K7]/[.$L7]" office:value-type="float" office:value="2.20841959972395" calcext:value-type="float">
<text:p>2.20841959972395</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K7]/[.$L7]" office:value-type="float" office:value="2.25558892225559" calcext:value-type="float">
<text:p>2.25558892225559</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -686,14 +686,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H8:.$I8])" office:value-type="float" office:value="17" calcext:value-type="float">
<text:p>17</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J8]/[.$J$2]" office:value-type="percentage" office:value="0.0527950310559006" calcext:value-type="percentage">
<text:p>5.28%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J8]/[.$J$2]" office:value-type="percentage" office:value="0.0510510510510511" calcext:value-type="percentage">
<text:p>5.11%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G8]/[.$G$2]" office:value-type="percentage" office:value="0.03125" calcext:value-type="percentage">
<text:p>3.13%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G8]/[.$G$2]" office:value-type="percentage" office:value="0.029585798816568" calcext:value-type="percentage">
<text:p>2.96%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K8]/[.$L8]" office:value-type="float" office:value="1.68944099378882" calcext:value-type="float">
<text:p>1.68944099378882</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K8]/[.$L8]" office:value-type="float" office:value="1.72552552552553" calcext:value-type="float">
<text:p>1.72552552552553</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -725,14 +725,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H9:.$I9])" office:value-type="float" office:value="18" calcext:value-type="float">
<text:p>18</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J9]/[.$J$2]" office:value-type="percentage" office:value="0.0559006211180124" calcext:value-type="percentage">
<text:p>5.59%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J9]/[.$J$2]" office:value-type="percentage" office:value="0.0540540540540541" calcext:value-type="percentage">
<text:p>5.41%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G9]/[.$G$2]" office:value-type="percentage" office:value="0.02875" calcext:value-type="percentage">
<text:p>2.88%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G9]/[.$G$2]" office:value-type="percentage" office:value="0.0272189349112426" calcext:value-type="percentage">
<text:p>2.72%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K9]/[.$L9]" office:value-type="float" office:value="1.94436943019174" calcext:value-type="float">
<text:p>1.94436943019174</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K9]/[.$L9]" office:value-type="float" office:value="1.98589894242068" calcext:value-type="float">
<text:p>1.98589894242068</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -762,14 +762,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H10:.$I10])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J10]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J10]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G10]/[.$G$2]" office:value-type="percentage" office:value="0.025" calcext:value-type="percentage">
<text:p>2.50%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G10]/[.$G$2]" office:value-type="percentage" office:value="0.0236686390532544" calcext:value-type="percentage">
<text:p>2.37%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K10]/[.$L10]" office:value-type="float" office:value="1.98757763975155" calcext:value-type="float">
<text:p>1.98757763975155</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K10]/[.$L10]" office:value-type="float" office:value="2.03003003003003" calcext:value-type="float">
<text:p>2.03003003003003</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -802,14 +802,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H11:.$I11])" office:value-type="float" office:value="9" calcext:value-type="float">
<text:p>9</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J11]/[.$J$2]" office:value-type="percentage" office:value="0.0279503105590062" calcext:value-type="percentage">
<text:p>2.80%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J11]/[.$J$2]" office:value-type="percentage" office:value="0.027027027027027" calcext:value-type="percentage">
<text:p>2.70%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G11]/[.$G$2]" office:value-type="percentage" office:value="0.01625" calcext:value-type="percentage">
<text:p>1.63%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G11]/[.$G$2]" office:value-type="percentage" office:value="0.0153846153846154" calcext:value-type="percentage">
<text:p>1.54%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K11]/[.$L11]" office:value-type="float" office:value="1.72001911132346" calcext:value-type="float">
<text:p>1.72001911132346</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K11]/[.$L11]" office:value-type="float" office:value="1.75675675675676" calcext:value-type="float">
<text:p>1.75675675675676</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -842,14 +842,14 @@
<table:table-cell table:style-name="ce48" table:formula="of:=SUM([.$H12:.$I12])" office:value-type="float" office:value="17" calcext:value-type="float">
<text:p>17</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J12]/[.$J$2]" office:value-type="percentage" office:value="0.0527950310559006" calcext:value-type="percentage">
<text:p>5.28%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J12]/[.$J$2]" office:value-type="percentage" office:value="0.0510510510510511" calcext:value-type="percentage">
<text:p>5.11%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G12]/[.$G$2]" office:value-type="percentage" office:value="0.03" calcext:value-type="percentage">
<text:p>3.00%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G12]/[.$G$2]" office:value-type="percentage" office:value="0.0284023668639053" calcext:value-type="percentage">
<text:p>2.84%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K12]/[.$L12]" office:value-type="float" office:value="1.75983436853002" calcext:value-type="float">
<text:p>1.75983436853002</text:p>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K12]/[.$L12]" office:value-type="float" office:value="1.79742242242242" calcext:value-type="float">
<text:p>1.79742242242242</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:number-columns-repeated="1011"/>
</table:table-row>
@ -884,14 +884,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H13:.$I13])" office:value-type="float" office:value="11" calcext:value-type="float">
<text:p>11</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J13]/[.$J$2]" office:value-type="percentage" office:value="0.0341614906832298" calcext:value-type="percentage">
<text:p>3.42%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J13]/[.$J$2]" office:value-type="percentage" office:value="0.033033033033033" calcext:value-type="percentage">
<text:p>3.30%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G13]/[.$G$2]" office:value-type="percentage" office:value="0.0875" calcext:value-type="percentage">
<text:p>8.75%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G13]/[.$G$2]" office:value-type="percentage" office:value="0.0828402366863905" calcext:value-type="percentage">
<text:p>8.28%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K13]/[.$L13]" office:value-type="float" office:value="0.390417036379769" calcext:value-type="float">
<text:p>0.390417036379769</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K13]/[.$L13]" office:value-type="float" office:value="0.398755898755899" calcext:value-type="float">
<text:p>0.398755898755899</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -921,14 +921,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H14:.$I14])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J14]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J14]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G14]/[.$G$2]" office:value-type="percentage" office:value="0.0375" calcext:value-type="percentage">
<text:p>3.75%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G14]/[.$G$2]" office:value-type="percentage" office:value="0.0355029585798817" calcext:value-type="percentage">
<text:p>3.55%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K14]/[.$L14]" office:value-type="float" office:value="1.32505175983437" calcext:value-type="float">
<text:p>1.32505175983437</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K14]/[.$L14]" office:value-type="float" office:value="1.35335335335335" calcext:value-type="float">
<text:p>1.35335335335335</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -958,14 +958,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H15:.$I15])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J15]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J15]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G15]/[.$G$2]" office:value-type="percentage" office:value="0.08125" calcext:value-type="percentage">
<text:p>8.13%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G15]/[.$G$2]" office:value-type="percentage" office:value="0.0769230769230769" calcext:value-type="percentage">
<text:p>7.69%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K15]/[.$L15]" office:value-type="float" office:value="0.611562350692785" calcext:value-type="float">
<text:p>0.611562350692785</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K15]/[.$L15]" office:value-type="float" office:value="0.624624624624625" calcext:value-type="float">
<text:p>0.624624624624625</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1000,14 +1000,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H16:.$I16])" office:value-type="float" office:value="15" calcext:value-type="float">
<text:p>15</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J16]/[.$J$2]" office:value-type="percentage" office:value="0.046583850931677" calcext:value-type="percentage">
<text:p>4.66%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J16]/[.$J$2]" office:value-type="percentage" office:value="0.045045045045045" calcext:value-type="percentage">
<text:p>4.50%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G16]/[.$G$2]" office:value-type="percentage" office:value="0.0875" calcext:value-type="percentage">
<text:p>8.75%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G16]/[.$G$2]" office:value-type="percentage" office:value="0.0828402366863905" calcext:value-type="percentage">
<text:p>8.28%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K16]/[.$L16]" office:value-type="float" office:value="0.532386867790595" calcext:value-type="float">
<text:p>0.532386867790595</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K16]/[.$L16]" office:value-type="float" office:value="0.543758043758044" calcext:value-type="float">
<text:p>0.543758043758044</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1040,14 +1040,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H17:.$I17])" office:value-type="float" office:value="13" calcext:value-type="float">
<text:p>13</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J17]/[.$J$2]" office:value-type="percentage" office:value="0.0403726708074534" calcext:value-type="percentage">
<text:p>4.04%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J17]/[.$J$2]" office:value-type="percentage" office:value="0.039039039039039" calcext:value-type="percentage">
<text:p>3.90%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G17]/[.$G$2]" office:value-type="percentage" office:value="0.09625" calcext:value-type="percentage">
<text:p>9.63%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G17]/[.$G$2]" office:value-type="percentage" office:value="0.0911242603550296" calcext:value-type="percentage">
<text:p>9.11%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K17]/[.$L17]" office:value-type="float" office:value="0.419456320077438" calcext:value-type="float">
<text:p>0.419456320077438</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K17]/[.$L17]" office:value-type="float" office:value="0.428415428415428" calcext:value-type="float">
<text:p>0.428415428415428</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1077,14 +1077,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H18:.$I18])" office:value-type="float" office:value="18" calcext:value-type="float">
<text:p>18</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J18]/[.$J$2]" office:value-type="percentage" office:value="0.0559006211180124" calcext:value-type="percentage">
<text:p>5.59%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J18]/[.$J$2]" office:value-type="percentage" office:value="0.0540540540540541" calcext:value-type="percentage">
<text:p>5.41%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G18]/[.$G$2]" office:value-type="percentage" office:value="0.03125" calcext:value-type="percentage">
<text:p>3.13%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G18]/[.$G$2]" office:value-type="percentage" office:value="0.029585798816568" calcext:value-type="percentage">
<text:p>2.96%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K18]/[.$L18]" office:value-type="float" office:value="1.7888198757764" calcext:value-type="float">
<text:p>1.7888198757764</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K18]/[.$L18]" office:value-type="float" office:value="1.82702702702703" calcext:value-type="float">
<text:p>1.82702702702703</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1117,14 +1117,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H19:.$I19])" office:value-type="float" office:value="17" calcext:value-type="float">
<text:p>17</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J19]/[.$J$2]" office:value-type="percentage" office:value="0.0527950310559006" calcext:value-type="percentage">
<text:p>5.28%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J19]/[.$J$2]" office:value-type="percentage" office:value="0.0510510510510511" calcext:value-type="percentage">
<text:p>5.11%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G19]/[.$G$2]" office:value-type="percentage" office:value="0.025" calcext:value-type="percentage">
<text:p>2.50%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G19]/[.$G$2]" office:value-type="percentage" office:value="0.0236686390532544" calcext:value-type="percentage">
<text:p>2.37%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K19]/[.$L19]" office:value-type="float" office:value="2.11180124223602" calcext:value-type="float">
<text:p>2.11180124223602</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K19]/[.$L19]" office:value-type="float" office:value="2.15690690690691" calcext:value-type="float">
<text:p>2.15690690690691</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1154,14 +1154,14 @@
<table:table-cell table:style-name="ce46" table:formula="of:=SUM([.$H20:.$I20])" office:value-type="float" office:value="16" calcext:value-type="float">
<text:p>16</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J20]/[.$J$2]" office:value-type="percentage" office:value="0.0496894409937888" calcext:value-type="percentage">
<text:p>4.97%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$J20]/[.$J$2]" office:value-type="percentage" office:value="0.0480480480480481" calcext:value-type="percentage">
<text:p>4.80%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G20]/[.$G$2]" office:value-type="percentage" office:value="0.025" calcext:value-type="percentage">
<text:p>2.50%</text:p>
<table:table-cell table:style-name="ce52" table:formula="of:=[.$G20]/[.$G$2]" office:value-type="percentage" office:value="0.0236686390532544" calcext:value-type="percentage">
<text:p>2.37%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K20]/[.$L20]" office:value-type="float" office:value="1.98757763975155" calcext:value-type="float">
<text:p>1.98757763975155</text:p>
<table:table-cell table:style-name="ce46" table:formula="of:=[.$K20]/[.$L20]" office:value-type="float" office:value="2.03003003003003" calcext:value-type="float">
<text:p>2.03003003003003</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce46" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1191,14 +1191,14 @@
<table:table-cell table:style-name="ce48" table:formula="of:=SUM([.$H21:.$I21])" office:value-type="float" office:value="18" calcext:value-type="float">
<text:p>18</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J21]/[.$J$2]" office:value-type="percentage" office:value="0.0559006211180124" calcext:value-type="percentage">
<text:p>5.59%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J21]/[.$J$2]" office:value-type="percentage" office:value="0.0540540540540541" calcext:value-type="percentage">
<text:p>5.41%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G21]/[.$G$2]" office:value-type="percentage" office:value="0.06625" calcext:value-type="percentage">
<text:p>6.63%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G21]/[.$G$2]" office:value-type="percentage" office:value="0.0627218934911243" calcext:value-type="percentage">
<text:p>6.27%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K21]/[.$L21]" office:value-type="float" office:value="0.843782960271886" calcext:value-type="float">
<text:p>0.843782960271886</text:p>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K21]/[.$L21]" office:value-type="float" office:value="0.861805201427843" calcext:value-type="float">
<text:p>0.861805201427843</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1231,14 +1231,14 @@
<table:table-cell table:style-name="ce48" table:formula="of:=SUM([.$H22:.$I22])" office:value-type="float" office:value="15" calcext:value-type="float">
<text:p>15</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J22]/[.$J$2]" office:value-type="percentage" office:value="0.046583850931677" calcext:value-type="percentage">
<text:p>4.66%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$J22]/[.$J$2]" office:value-type="percentage" office:value="0.045045045045045" calcext:value-type="percentage">
<text:p>4.50%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G22]/[.$G$2]" office:value-type="percentage" office:value="0.06" calcext:value-type="percentage">
<text:p>6.00%</text:p>
<table:table-cell table:style-name="ce54" table:formula="of:=[.$G22]/[.$G$2]" office:value-type="percentage" office:value="0.0568047337278107" calcext:value-type="percentage">
<text:p>5.68%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K22]/[.$L22]" office:value-type="float" office:value="0.77639751552795" calcext:value-type="float">
<text:p>0.77639751552795</text:p>
<table:table-cell table:style-name="ce48" table:formula="of:=[.$K22]/[.$L22]" office:value-type="float" office:value="0.792980480480481" calcext:value-type="float">
<text:p>0.792980480480481</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce48" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1271,14 +1271,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H23:.$I23])" office:value-type="float" office:value="9" calcext:value-type="float">
<text:p>9</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J23]/[.$J$2]" office:value-type="percentage" office:value="0.0279503105590062" calcext:value-type="percentage">
<text:p>2.80%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J23]/[.$J$2]" office:value-type="percentage" office:value="0.027027027027027" calcext:value-type="percentage">
<text:p>2.70%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G23]/[.$G$2]" office:value-type="percentage" office:value="0.05875" calcext:value-type="percentage">
<text:p>5.88%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G23]/[.$G$2]" office:value-type="percentage" office:value="0.0556213017751479" calcext:value-type="percentage">
<text:p>5.56%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K23]/[.$L23]" office:value-type="float" office:value="0.475749966961808" calcext:value-type="float">
<text:p>0.475749966961808</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K23]/[.$L23]" office:value-type="float" office:value="0.485911443358252" calcext:value-type="float">
<text:p>0.485911443358252</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1311,14 +1311,14 @@
<table:table-cell table:style-name="ce47" table:formula="of:=SUM([.$H24:.$I24])" office:value-type="float" office:value="9" calcext:value-type="float">
<text:p>9</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J24]/[.$J$2]" office:value-type="percentage" office:value="0.0279503105590062" calcext:value-type="percentage">
<text:p>2.80%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J24]/[.$J$2]" office:value-type="percentage" office:value="0.027027027027027" calcext:value-type="percentage">
<text:p>2.70%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G24]/[.$G$2]" office:value-type="percentage" office:value="0.065" calcext:value-type="percentage">
<text:p>6.50%</text:p>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G24]/[.$G$2]" office:value-type="percentage" office:value="0.0615384615384615" calcext:value-type="percentage">
<text:p>6.15%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K24]/[.$L24]" office:value-type="float" office:value="0.430004777830865" calcext:value-type="float">
<text:p>0.430004777830865</text:p>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K24]/[.$L24]" office:value-type="float" office:value="0.439189189189189" calcext:value-type="float">
<text:p>0.439189189189189</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
@ -1343,7 +1343,70 @@
<table:table-cell table:style-name="ce21" table:number-columns-repeated="2"/>
<table:table-cell table:style-name="ce13" table:number-columns-repeated="1012"/>
</table:table-row>
<table:table-row table:style-name="ro1" table:number-rows-repeated="1048550">
<table:table-row table:style-name="ro6">
<table:table-cell table:style-name="ce41" office:value-type="string" calcext:value-type="string">
<text:p>Multiplayer Phrase Pairing</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce41" office:value-type="string" calcext:value-type="string">
<text:p>Networking</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce41" office:value-type="string" calcext:value-type="string">
<text:p>As a player, I would like to be able to pair with another Player I know, by setting up a common phrase that will match me with the Player I know, so that I can play a game with the Player I know.</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce41" office:value-type="string" calcext:value-type="string">
<text:p>Set up a “room code” system where Players can specify a “phrase” or “code word” that will only pair them with other Players that use the same “phrase”.</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce41" office:value-type="string" calcext:value-type="string">
<text:p>The back-end should be able to pair Players with identical “phrases” or “code words”. The front-end should allow the Player to set a “phrase” or “code word”.</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" office:value-type="string" calcext:value-type="string">
<text:p>Exciter</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" office:value-type="float" office:value="45" calcext:value-type="float">
<text:p>45</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" office:value-type="float" office:value="8" calcext:value-type="float">
<text:p>8</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" office:value-type="float" office:value="3" calcext:value-type="float">
<text:p>3</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" office:value-type="float" office:value="11" calcext:value-type="float">
<text:p>11</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$J26]/[.$J$2]" office:value-type="percentage" office:value="0.033033033033033" calcext:value-type="percentage">
<text:p>3.30%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce53" table:formula="of:=[.$G26]/[.$G$2]" office:value-type="percentage" office:value="0.0532544378698225" calcext:value-type="percentage">
<text:p>5.33%</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:formula="of:=[.$K26]/[.$L26]" office:value-type="float" office:value="0.620286953620287" calcext:value-type="float">
<text:p>0.620286953620287</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce47" table:number-columns-repeated="1011"/>
</table:table-row>
<table:table-row table:style-name="ro4">
<table:table-cell table:style-name="ce6" office:value-type="string" calcext:value-type="string">
<text:p>Refactoring and Code Improvement</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce6" office:value-type="string" calcext:value-type="string">
<text:p>Constraint</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce6" office:value-type="string" calcext:value-type="string">
<text:p>As I developer, I expect the code to be improved when improvements can be made to improve code quality and prevent technical debt.</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce6"/>
<table:table-cell table:style-name="ce6" office:value-type="string" calcext:value-type="string">
<text:p>The front-end and back-end code should be reasonably maintained, with fixes and/or refactorings where possible.</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce13" office:value-type="string" calcext:value-type="string">
<text:p>Mandatory</text:p>
</table:table-cell>
<table:table-cell table:style-name="ce13" table:number-columns-repeated="4"/>
<table:table-cell table:style-name="ce21" table:number-columns-repeated="2"/>
<table:table-cell table:style-name="ce13" table:number-columns-repeated="1012"/>
</table:table-row>
<table:table-row table:style-name="ro1" table:number-rows-repeated="1048548">
<table:table-cell table:number-columns-repeated="1024"/>
</table:table-row>
<table:table-row table:style-name="ro1">

Binary file not shown.

Binary file not shown.