diff --git a/front_end/src/async_js_helper.rs b/front_end/src/async_js_helper.rs new file mode 100644 index 0000000..2a846e3 --- /dev/null +++ b/front_end/src/async_js_helper.rs @@ -0,0 +1,15 @@ +use js_sys::Promise; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +#[wasm_bindgen(module = "/src/deferred_helper.js")] +extern "C" { + fn async_sleep(ms: u32) -> Promise; +} + +pub async fn rust_async_sleep(ms: u32) -> Result<(), JsValue> { + let promise = async_sleep(ms); + let js_fut = JsFuture::from(promise); + js_fut.await?; + Ok(()) +} diff --git a/front_end/src/deferred_helper.js b/front_end/src/deferred_helper.js new file mode 100644 index 0000000..fdabc9b --- /dev/null +++ b/front_end/src/deferred_helper.js @@ -0,0 +1,3 @@ +export function async_sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/front_end/src/main.rs b/front_end/src/main.rs index a200b4e..99e89e6 100644 --- a/front_end/src/main.rs +++ b/front_end/src/main.rs @@ -1,4 +1,5 @@ mod ai; +mod async_js_helper; mod constants; mod game_logic; mod html_helper; diff --git a/front_end/src/state.rs b/front_end/src/state.rs index ea90969..f455099 100644 --- a/front_end/src/state.rs +++ b/front_end/src/state.rs @@ -9,7 +9,7 @@ pub enum GameState { MainMenu, SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, - NetworkedMultiplayer, + NetworkedMultiplayer(Turn), PostGameResults(BoardState), } @@ -24,7 +24,7 @@ impl From for GameState { match msg { MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai), MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer, - MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer, + MainMenuMessage::NetworkedMultiplayer(t) => GameState::NetworkedMultiplayer(t), } } } diff --git a/front_end/src/yew_components.rs b/front_end/src/yew_components.rs index ab54a16..0679a08 100644 --- a/front_end/src/yew_components.rs +++ b/front_end/src/yew_components.rs @@ -1,4 +1,5 @@ use crate::ai::{get_ai_choice, AIDifficulty}; +use crate::async_js_helper; use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS}; use crate::game_logic::{check_win_draw, WinType}; use crate::html_helper::{ @@ -17,7 +18,7 @@ pub struct MainMenu {} pub enum MainMenuMessage { SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, - NetworkedMultiplayer, + NetworkedMultiplayer(Turn), } impl Component for MainMenu { @@ -111,20 +112,32 @@ impl Component for MainMenu { let info_text_turn = document .get_element_by_id("info_text1") .expect("info_text1 should exist"); - info_text_turn.set_inner_html("

It is CyanPlayer's Turn

"); - if let GameState::SinglePlayer(Turn::MagentaPlayer, ai_difficulty) = + if let GameState::SinglePlayer(turn, _) = shared.game_state.get() { + if shared.turn.get() == turn { + info_text_turn.set_inner_html( + "

It is CyanPlayer's (player) Turn

", + ); + } else { + info_text_turn.set_inner_html( + "

It is CyanPlayer's (ai) Turn

", + ); + } + } else { + info_text_turn + .set_inner_html("

It is CyanPlayer's Turn

"); + } + + if let GameState::SinglePlayer(Turn::MagentaPlayer, _ai_difficulty) = shared.game_state.get() { // AI player starts first - let choice = get_ai_choice(ai_difficulty, Turn::CyanPlayer, &shared.board) - .expect("AI should have an available choice"); ctx.link() .get_parent() .expect("Wrapper should be parent of MainMenu") .clone() .downcast::() - .send_message(WrapperMsg::Pressed(usize::from(choice) as u8)); + .send_message(WrapperMsg::AIChoice); } } @@ -182,7 +195,7 @@ impl Component for Slot { GameState::MainMenu => return false, GameState::SinglePlayer(_, _) | GameState::LocalMultiplayer - | GameState::NetworkedMultiplayer => (), + | GameState::NetworkedMultiplayer(_) => (), GameState::PostGameResults(_) => return false, } if shared.game_state.get() == GameState::MainMenu { @@ -205,8 +218,18 @@ impl Component for Slot { pub struct Wrapper {} +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum WrapperMsg { Pressed(u8), + AIPressed(u8), + AIChoice, + AIChoiceImpl, +} + +impl WrapperMsg { + fn is_ai_pressed(self) -> bool { + matches!(self, WrapperMsg::AIPressed(_)) + } } impl Component for Wrapper { @@ -296,15 +319,34 @@ impl Component for Wrapper { .link() .context::(Callback::noop()) .expect("state to be set"); - let (window, document) = + let (_window, document) = get_window_document().expect("Should be able to get Window and Document"); match msg { - WrapperMsg::Pressed(idx) => { + WrapperMsg::Pressed(idx) | WrapperMsg::AIPressed(idx) => { let mut bottom_idx = idx; let mut placed = false; let current_player = shared.turn.get(); + // check if player can make a move + if !msg.is_ai_pressed() { + match shared.game_state.get() { + GameState::MainMenu => (), + GameState::SinglePlayer(turn, _) => { + if current_player != turn { + return false; + } + } + GameState::LocalMultiplayer => (), + GameState::NetworkedMultiplayer(turn) => { + if current_player != turn { + return false; + } + } + GameState::PostGameResults(_) => (), + } + } + // check if clicked on empty slot if shared.board[idx as usize].get().is_empty() { // get bottom-most empty slot @@ -358,11 +400,28 @@ impl Component for Wrapper { // info text right of the grid { let turn = shared.turn.get(); - let output_str = format!( - "It is {}'s turn", - turn.get_color(), - turn - ); + let output_str = + if let GameState::SinglePlayer(player_turn, _) = shared.game_state.get() { + if shared.turn.get() == player_turn { + format!( + "It is {}'s (player) turn", + turn.get_color(), + turn + ) + } else { + format!( + "It is {}'s (ai) turn", + turn.get_color(), + turn + ) + } + } else { + format!( + "It is {}'s turn", + turn.get_color(), + turn + ) + }; let text_append_result = append_to_info_text(&document, "info_text1", &output_str, 1); @@ -720,18 +779,34 @@ impl Component for Wrapper { } // if: check for win or draw // check if it is AI's turn + if let GameState::SinglePlayer(player_type, _ai_difficulty) = + shared.game_state.get() + { + if shared.turn.get() != player_type { + ctx.link().send_message(WrapperMsg::AIChoice); + } + } + } // WrapperMsg::Pressed(idx) => + WrapperMsg::AIChoice => { + // defer by 1 second + ctx.link().send_future(async { + async_js_helper::rust_async_sleep(1000).await.unwrap(); + WrapperMsg::AIChoiceImpl + }); + } + WrapperMsg::AIChoiceImpl => { + // get AI's choice if let GameState::SinglePlayer(player_type, ai_difficulty) = shared.game_state.get() { if shared.turn.get() != player_type { - // get AI's choice let choice = get_ai_choice(ai_difficulty, player_type.get_opposite(), &shared.board) .expect("AI should have an available choice"); ctx.link() - .send_message(WrapperMsg::Pressed(usize::from(choice) as u8)); + .send_message(WrapperMsg::AIPressed(usize::from(choice) as u8)); } } - } // WrapperMsg::Pressed(idx) => + } } // match (msg) true