From ebf0cb5bb8aa9c7d84e088a2eb91f1c2b53c1174 Mon Sep 17 00:00:00 2001 From: Stephen Seo Date: Tue, 15 Mar 2022 13:16:09 +0900 Subject: [PATCH] Impl async delay on AI choice This commit is also a stepping-stone towards handling http requests which will require deferred callbacks on Yew Components. By figuring out how to delay callbacks in this commit, it should be easier to figure out how to handle http requests that may require a deferred callback. --- front_end/src/async_js_helper.rs | 15 +++++ front_end/src/deferred_helper.js | 3 + front_end/src/main.rs | 1 + front_end/src/state.rs | 4 +- front_end/src/yew_components.rs | 109 ++++++++++++++++++++++++++----- 5 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 front_end/src/async_js_helper.rs create mode 100644 front_end/src/deferred_helper.js 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