frontend: WIP! in progress setting up id request
This commit is contained in:
parent
39c8177914
commit
83a9ab2ea0
6 changed files with 315 additions and 62 deletions
1
front_end/Cargo.lock
generated
1
front_end/Cargo.lock
generated
|
@ -43,6 +43,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"log",
|
||||
"oorandom",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
|
|
|
@ -9,8 +9,9 @@ edition = "2021"
|
|||
yew = "0.19"
|
||||
log = "0.4.6"
|
||||
wasm-logger = "0.2.0"
|
||||
web-sys = { version = "0.3.56", features = ["Window", "Document", "Element"] }
|
||||
web-sys = { version = "0.3.56", features = ["Window", "Document", "Element", "Request", "RequestInit"] }
|
||||
js-sys = "0.3.56"
|
||||
oorandom = "11.1.3"
|
||||
wasm-bindgen = "0.2.79"
|
||||
wasm-bindgen-futures = "0.4.29"
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -5,8 +5,14 @@ pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
|
|||
|
||||
pub const AI_EASY_MAX_CHOICES: usize = 5;
|
||||
pub const AI_NORMAL_MAX_CHOICES: usize = 3;
|
||||
pub const AI_CHOICE_DURATION_MILLIS: i32 = 1000;
|
||||
|
||||
pub const PLAYER_COUNT_LIMIT: usize = 1000;
|
||||
pub const TURN_SECONDS: u64 = 25;
|
||||
pub const GAME_CLEANUP_TIMEOUT: u64 = (TURN_SECONDS + 1) * ((ROWS * COLS) as u64 + 5u64);
|
||||
pub const PLAYER_CLEANUP_TIMEOUT: u64 = 300;
|
||||
|
||||
pub const BACKEND_TICK_DURATION_MILLIS: i32 = 500;
|
||||
|
||||
// TODO: Change this to "https://asdm.seodisparate.com/api" when backend is installed
|
||||
pub const BACKEND_URL: &str = "http://localhost:1237/";
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use web_sys::{window, Document, Window};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{window, Document, Request, RequestInit, Window};
|
||||
|
||||
pub fn get_window_document() -> Result<(Window, Document), String> {
|
||||
let window = window().ok_or_else(|| String::from("Failed to get window"))?;
|
||||
|
@ -77,3 +78,14 @@ pub fn element_remove_class(document: &Document, id: &str, class: &str) -> Resul
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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.headers(
|
||||
&JsValue::from_serde("'headers': { 'Content-Type': 'application/json' }")
|
||||
.map_err(|e| format!("{}", e))?,
|
||||
);
|
||||
|
||||
Ok(Request::new_with_str_and_init(target_url, &req_init).map_err(|e| format!("{:?}", e))?)
|
||||
}
|
||||
|
|
|
@ -12,10 +12,27 @@ pub enum GameState {
|
|||
MainMenu,
|
||||
SinglePlayer(Turn, AIDifficulty),
|
||||
LocalMultiplayer,
|
||||
NetworkedMultiplayer(Turn),
|
||||
NetworkedMultiplayer {
|
||||
paired: bool,
|
||||
current_side: Option<Turn>,
|
||||
current_turn: Turn,
|
||||
},
|
||||
PostGameResults(BoardState),
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
pub fn is_networked_multiplayer(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
GameState::NetworkedMultiplayer {
|
||||
paired,
|
||||
current_side,
|
||||
current_turn
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GameState {
|
||||
fn default() -> Self {
|
||||
GameState::MainMenu
|
||||
|
@ -27,7 +44,11 @@ impl From<MainMenuMessage> for GameState {
|
|||
match msg {
|
||||
MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai),
|
||||
MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer,
|
||||
MainMenuMessage::NetworkedMultiplayer(t) => GameState::NetworkedMultiplayer(t),
|
||||
MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer {
|
||||
paired: false,
|
||||
current_side: None,
|
||||
current_turn: Turn::CyanPlayer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +319,7 @@ impl Default for SharedState {
|
|||
pub enum MainMenuMessage {
|
||||
SinglePlayer(Turn, AIDifficulty),
|
||||
LocalMultiplayer,
|
||||
NetworkedMultiplayer(Turn),
|
||||
NetworkedMultiplayer,
|
||||
}
|
||||
|
||||
pub fn new_string_board() -> String {
|
||||
|
@ -402,3 +423,27 @@ pub fn string_from_board(board: BoardType, placed: usize) -> (String, Option<Boa
|
|||
(board_string, None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_networked_multiplayer_enum() {
|
||||
let state = GameState::MainMenu;
|
||||
assert!(!state.is_networked_multiplayer());
|
||||
let state = GameState::LocalMultiplayer;
|
||||
assert!(!state.is_networked_multiplayer());
|
||||
let state = GameState::NetworkedMultiplayer {
|
||||
paired: false,
|
||||
current_side: None,
|
||||
current_turn: Turn::CyanPlayer,
|
||||
};
|
||||
assert!(state.is_networked_multiplayer());
|
||||
let state = GameState::NetworkedMultiplayer {
|
||||
paired: true,
|
||||
current_side: Some(Turn::CyanPlayer),
|
||||
current_turn: Turn::MagentaPlayer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use crate::ai::{get_ai_choice, AIDifficulty};
|
||||
use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS};
|
||||
use crate::constants::{
|
||||
AI_CHOICE_DURATION_MILLIS, BACKEND_TICK_DURATION_MILLIS, BACKEND_URL, COLS,
|
||||
INFO_TEXT_MAX_ITEMS, ROWS,
|
||||
};
|
||||
use crate::game_logic::{check_win_draw, WinType};
|
||||
use crate::html_helper::{
|
||||
append_to_info_text, element_append_class, element_remove_class, get_window_document,
|
||||
append_to_info_text, create_json_request, element_append_class, element_remove_class,
|
||||
get_window_document,
|
||||
};
|
||||
use crate::random_helper::get_seeded_random;
|
||||
use crate::state::{BoardState, GameState, MainMenuMessage, SharedState, Turn};
|
||||
|
@ -10,8 +14,11 @@ use crate::state::{BoardState, GameState, MainMenuMessage, SharedState, Turn};
|
|||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use js_sys::Promise;
|
||||
use js_sys::{JsString, Promise};
|
||||
|
||||
use serde_json::Value as SerdeJSONValue;
|
||||
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
use yew::prelude::*;
|
||||
|
@ -60,6 +67,10 @@ impl Component for MainMenu {
|
|||
let onclick_local_multiplayer =
|
||||
ctx.link().callback(|_| MainMenuMessage::LocalMultiplayer);
|
||||
|
||||
let onclick_networked_multiplayer = ctx
|
||||
.link()
|
||||
.callback(|_| MainMenuMessage::NetworkedMultiplayer);
|
||||
|
||||
html! {
|
||||
<div class={"menu"} id={"mainmenu"}>
|
||||
<b class={"menuText"}>{"Please pick a game mode."}</b>
|
||||
|
@ -77,7 +88,7 @@ impl Component for MainMenu {
|
|||
<button class={"menuLocalMultiplayer"} onclick={onclick_local_multiplayer}>
|
||||
{"Local Multiplayer"}
|
||||
</button>
|
||||
<button class={"menuMultiplayer"}>
|
||||
<button class={"menuMultiplayer"} onclick={onclick_networked_multiplayer}>
|
||||
{"Networked Multiplayer"}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -110,31 +121,42 @@ impl Component for MainMenu {
|
|||
.get_element_by_id("info_text1")
|
||||
.expect("info_text1 should exist");
|
||||
|
||||
if let GameState::SinglePlayer(turn, _) = shared.game_state.get() {
|
||||
if shared.turn.get() == turn {
|
||||
info_text_turn.set_inner_html(
|
||||
"<p><b class=\"cyan\">It is CyanPlayer's (player) Turn</b></p>",
|
||||
);
|
||||
} else {
|
||||
info_text_turn.set_inner_html(
|
||||
"<p><b class=\"cyan\">It is CyanPlayer's (ai) Turn</b></p>",
|
||||
);
|
||||
match shared.game_state.get() {
|
||||
GameState::SinglePlayer(turn, _) => {
|
||||
if shared.turn.get() == turn {
|
||||
info_text_turn.set_inner_html(
|
||||
"<p><b class=\"cyan\">It is CyanPlayer's (player) Turn</b></p>",
|
||||
);
|
||||
} else {
|
||||
info_text_turn.set_inner_html(
|
||||
"<p><b class=\"cyan\">It is CyanPlayer's (ai) Turn</b></p>",
|
||||
);
|
||||
// AI player starts first
|
||||
ctx.link()
|
||||
.get_parent()
|
||||
.expect("Wrapper should be parent of MainMenu")
|
||||
.clone()
|
||||
.downcast::<Wrapper>()
|
||||
.send_message(WrapperMsg::AIChoice);
|
||||
}
|
||||
}
|
||||
GameState::NetworkedMultiplayer {
|
||||
paired: _,
|
||||
current_side: _,
|
||||
current_turn: _,
|
||||
} => {
|
||||
// start the Wrapper Tick loop
|
||||
ctx.link()
|
||||
.get_parent()
|
||||
.expect("Wrapper should be a parent of MainMenu")
|
||||
.clone()
|
||||
.downcast::<Wrapper>()
|
||||
.send_message(WrapperMsg::BackendTick);
|
||||
}
|
||||
_ => {
|
||||
info_text_turn
|
||||
.set_inner_html("<p><b class=\"cyan\">It is CyanPlayer's Turn</b></p>");
|
||||
}
|
||||
} else {
|
||||
info_text_turn
|
||||
.set_inner_html("<p><b class=\"cyan\">It is CyanPlayer's Turn</b></p>");
|
||||
}
|
||||
|
||||
if let GameState::SinglePlayer(Turn::MagentaPlayer, _ai_difficulty) =
|
||||
shared.game_state.get()
|
||||
{
|
||||
// AI player starts first
|
||||
ctx.link()
|
||||
.get_parent()
|
||||
.expect("Wrapper should be parent of MainMenu")
|
||||
.clone()
|
||||
.downcast::<Wrapper>()
|
||||
.send_message(WrapperMsg::AIChoice);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,9 +212,21 @@ impl Component for Slot {
|
|||
|
||||
match shared.game_state.get() {
|
||||
GameState::MainMenu => return false,
|
||||
GameState::SinglePlayer(_, _)
|
||||
| GameState::LocalMultiplayer
|
||||
| GameState::NetworkedMultiplayer(_) => (),
|
||||
GameState::SinglePlayer(_, _) | GameState::LocalMultiplayer => (),
|
||||
GameState::NetworkedMultiplayer {
|
||||
paired,
|
||||
current_side,
|
||||
current_turn,
|
||||
} => {
|
||||
// notify Wrapper with picked slot
|
||||
if let Some(p) = ctx.link().get_parent() {
|
||||
p.clone()
|
||||
.downcast::<Wrapper>()
|
||||
.send_message(WrapperMsg::BackendRequest {
|
||||
place: ctx.props().idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
GameState::PostGameResults(_) => return false,
|
||||
}
|
||||
if shared.game_state.get() == GameState::MainMenu {
|
||||
|
@ -213,14 +247,166 @@ impl Component for Slot {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Wrapper {}
|
||||
pub struct Wrapper {
|
||||
player_id: Option<u32>,
|
||||
place_request: Option<u8>,
|
||||
do_backend_tick: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
impl Wrapper {
|
||||
fn defer_message(
|
||||
&self,
|
||||
ctx: &Context<Self>,
|
||||
msg: <Wrapper as Component>::Message,
|
||||
millis: i32,
|
||||
) {
|
||||
ctx.link().send_future(async move {
|
||||
let promise = Promise::new(&mut |resolve: js_sys::Function, _reject| {
|
||||
let window = web_sys::window();
|
||||
if window.is_none() {
|
||||
resolve.call0(&resolve).ok();
|
||||
return;
|
||||
}
|
||||
let window = window.unwrap();
|
||||
if window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, millis)
|
||||
.is_err()
|
||||
{
|
||||
resolve.call0(&resolve).ok();
|
||||
}
|
||||
});
|
||||
let js_fut = JsFuture::from(promise);
|
||||
js_fut.await.ok();
|
||||
msg
|
||||
});
|
||||
}
|
||||
|
||||
fn get_networked_player_id(&mut self, ctx: &Context<Self>) {
|
||||
// make a request to get the player_id
|
||||
ctx.link().send_future(async {
|
||||
// get window
|
||||
let window = web_sys::window().expect("Should be able to get Window");
|
||||
// get request
|
||||
let request = create_json_request(BACKEND_URL, "'type': 'pairing_request'")
|
||||
.expect("Should be able to create the JSON request for player_id");
|
||||
// send request
|
||||
let promise = window.fetch_with_request(&request);
|
||||
// get request result
|
||||
let response_result = JsFuture::from(promise)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e));
|
||||
if let Err(e) = response_result {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(format!("ERROR: {:?}", e)));
|
||||
}
|
||||
// get response from request result
|
||||
let response = response_result.unwrap();
|
||||
let json_value_result: Result<SerdeJSONValue, _> = response.into_serde();
|
||||
if let Err(e) = json_value_result {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(format!("ERROR: {:?}", e)));
|
||||
}
|
||||
// get serde json Value from result
|
||||
let json_value = json_value_result.unwrap();
|
||||
|
||||
// get and check "type" in JSON
|
||||
let type_opt = json_value.get("type");
|
||||
if type_opt.is_none() {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: No \"type\" entry in JSON".into(),
|
||||
));
|
||||
}
|
||||
let json_type = type_opt.unwrap();
|
||||
if let Some(type_string) = json_type.as_str() {
|
||||
if type_string != "pairing_response" {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: Invalid \"type\" from response JSON".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: Missing \"type\" from response JSON".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// get and check "id" in JSON
|
||||
let player_id: u32;
|
||||
if let Some(wrapped_player_id) = json_value.get("id") {
|
||||
if let Some(player_id_u64) = wrapped_player_id.as_u64() {
|
||||
let player_id_conv_result: Result<u32, _> = player_id_u64.try_into();
|
||||
if player_id_conv_result.is_err() {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: \"id\" is too large".into(),
|
||||
));
|
||||
}
|
||||
player_id = player_id_conv_result.unwrap();
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: \"id\" is not a u64".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: Missing \"id\" from response JSON".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// get and check status
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum Status {
|
||||
Waiting,
|
||||
Paired,
|
||||
}
|
||||
let mut status: Status;
|
||||
if let Some(status_value) = json_value.get("status") {
|
||||
if let Some(status_str) = status_value.as_str() {
|
||||
if status_str == "waiting" {
|
||||
status = Status::Waiting;
|
||||
} else if status_str == "paired" {
|
||||
status = Status::Paired;
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: Got invalid \"status\" response in JSON".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: \"status\" response in JSON is not a str".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return WrapperMsg::BackendResponse(BREnum::Error(
|
||||
"ERROR: \"status\" response is missing in JSON".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// TODO set "disconnect" callback here so that the client sends
|
||||
// disconnect message when the page is closed
|
||||
|
||||
if status == Status::Paired {
|
||||
// Get which side current player is on if paired
|
||||
// TODO
|
||||
return WrapperMsg::BackendResponse(BREnum::Error("ERROR: unimplemented".into()));
|
||||
} else {
|
||||
WrapperMsg::BackendResponse(BREnum::GotID(player_id, None))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BREnum {
|
||||
Error(String),
|
||||
GotID(u32, Option<Turn>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum WrapperMsg {
|
||||
Pressed(u8),
|
||||
AIPressed(u8),
|
||||
AIChoice,
|
||||
AIChoiceImpl,
|
||||
BackendTick,
|
||||
BackendRequest { place: u8 },
|
||||
BackendResponse(BREnum),
|
||||
}
|
||||
|
||||
impl WrapperMsg {
|
||||
|
@ -234,7 +420,11 @@ impl Component for Wrapper {
|
|||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
Self {
|
||||
player_id: None,
|
||||
place_request: None,
|
||||
do_backend_tick: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
|
@ -335,10 +525,12 @@ impl Component for Wrapper {
|
|||
}
|
||||
}
|
||||
GameState::LocalMultiplayer => (),
|
||||
GameState::NetworkedMultiplayer(turn) => {
|
||||
if current_player != turn {
|
||||
return false;
|
||||
}
|
||||
GameState::NetworkedMultiplayer {
|
||||
paired,
|
||||
current_side,
|
||||
current_turn,
|
||||
} => {
|
||||
// TODO
|
||||
}
|
||||
GameState::PostGameResults(_) => (),
|
||||
}
|
||||
|
@ -786,25 +978,7 @@ impl Component for Wrapper {
|
|||
} // WrapperMsg::Pressed(idx) =>
|
||||
WrapperMsg::AIChoice => {
|
||||
// defer by 1 second
|
||||
ctx.link().send_future(async {
|
||||
let promise = Promise::new(&mut |resolve: js_sys::Function, _reject| {
|
||||
let window = web_sys::window();
|
||||
if window.is_none() {
|
||||
resolve.call0(&resolve).ok();
|
||||
return;
|
||||
}
|
||||
let window = window.unwrap();
|
||||
if window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 1000)
|
||||
.is_err()
|
||||
{
|
||||
resolve.call0(&resolve).ok();
|
||||
}
|
||||
});
|
||||
let js_fut = JsFuture::from(promise);
|
||||
js_fut.await.ok();
|
||||
WrapperMsg::AIChoiceImpl
|
||||
});
|
||||
self.defer_message(ctx, WrapperMsg::AIChoiceImpl, AI_CHOICE_DURATION_MILLIS);
|
||||
}
|
||||
WrapperMsg::AIChoiceImpl => {
|
||||
// get AI's choice
|
||||
|
@ -819,6 +993,20 @@ impl Component for Wrapper {
|
|||
}
|
||||
}
|
||||
}
|
||||
WrapperMsg::BackendTick => {
|
||||
if self.player_id.is_none() {
|
||||
self.get_networked_player_id(ctx);
|
||||
}
|
||||
|
||||
// repeat BackendTick handling while "connected" to backend
|
||||
if self.do_backend_tick {
|
||||
self.defer_message(ctx, WrapperMsg::BackendTick, BACKEND_TICK_DURATION_MILLIS);
|
||||
}
|
||||
}
|
||||
WrapperMsg::BackendRequest { place } => {
|
||||
self.place_request = Some(place);
|
||||
}
|
||||
WrapperMsg::BackendResponse(string) => {}
|
||||
} // match (msg)
|
||||
|
||||
true
|
||||
|
|
Loading…
Reference in a new issue