diff --git a/front_end/Cargo.lock b/front_end/Cargo.lock index e89084f..c11d6f9 100644 --- a/front_end/Cargo.lock +++ b/front_end/Cargo.lock @@ -40,6 +40,7 @@ dependencies = [ name = "four_line_dropper_frontend" version = "0.1.0" dependencies = [ + "js-sys", "log", "oorandom", "wasm-logger", diff --git a/front_end/Cargo.toml b/front_end/Cargo.toml index f27b539..e394755 100644 --- a/front_end/Cargo.toml +++ b/front_end/Cargo.toml @@ -10,4 +10,5 @@ yew = "0.19" log = "0.4.6" wasm-logger = "0.2.0" web-sys = { version = "0.3.56", features = ["Window", "Document", "Element"] } +js-sys = "0.3.56" oorandom = "11.1.3" diff --git a/front_end/index.html b/front_end/index.html index 40fbfff..022cc84 100644 --- a/front_end/index.html +++ b/front_end/index.html @@ -20,9 +20,23 @@ gap: 8px; grid-auto-rows: 33%; } - button.menuSinglePlayer { + div.singlePlayerMenu { grid-row: 2; grid-column: 2; + + display: grid; + } + button.menuSinglePlayerEasy { + grid-row: 1; + grid-column: 1; + } + button.menuSinglePlayerNormal { + grid-row: 2; + grid-column: 1; + } + button.menuSinglePlayerHard { + grid-row: 3; + grid-column: 1; } button.menuLocalMultiplayer { grid-row: 2; diff --git a/front_end/src/ai.rs b/front_end/src/ai.rs index 05476e8..65da7a9 100644 --- a/front_end/src/ai.rs +++ b/front_end/src/ai.rs @@ -148,7 +148,9 @@ fn get_utility_for_slot(player: Turn, slot: SlotChoice, board: &BoardType) -> Op // slot is full, cannot place in slot return None; } - while idx < (ROWS * COLS) as usize && board[idx + COLS as usize].get() == BoardState::Empty { + while idx < ((ROWS - 1) * COLS) as usize + && board[idx + COLS as usize].get() == BoardState::Empty + { idx += COLS as usize; } diff --git a/front_end/src/random_helper.rs b/front_end/src/random_helper.rs index 17bde78..16456db 100644 --- a/front_end/src/random_helper.rs +++ b/front_end/src/random_helper.rs @@ -1,12 +1,6 @@ +use js_sys::Math::random; use oorandom::Rand32; -use std::time::{SystemTime, UNIX_EPOCH}; - pub fn get_seeded_random() -> Result { - let now = SystemTime::now(); - let duration = now - .duration_since(UNIX_EPOCH) - .map_err(|e| format!("{}", e))?; - - Ok(Rand32::new(duration.as_secs())) + Ok(Rand32::new((random() * u64::MAX as f64) as u64)) } diff --git a/front_end/src/state.rs b/front_end/src/state.rs index 38b76bf..e921471 100644 --- a/front_end/src/state.rs +++ b/front_end/src/state.rs @@ -1,3 +1,4 @@ +use crate::ai::AIDifficulty; use crate::yew_components::MainMenuMessage; use std::cell::Cell; use std::fmt::Display; @@ -6,7 +7,7 @@ use std::rc::Rc; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum GameState { MainMenu, - SinglePlayer, + SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, NetworkedMultiplayer, PostGameResults(BoardState), @@ -21,7 +22,7 @@ impl Default for GameState { impl From for GameState { fn from(msg: MainMenuMessage) -> Self { match msg { - MainMenuMessage::SinglePlayer => GameState::SinglePlayer, + MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai), MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer, MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer, } diff --git a/front_end/src/yew_components.rs b/front_end/src/yew_components.rs index e19706c..3f9a97c 100644 --- a/front_end/src/yew_components.rs +++ b/front_end/src/yew_components.rs @@ -1,8 +1,10 @@ +use crate::ai::{get_ai_choice, AIDifficulty}; use crate::constants::{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, }; +use crate::random_helper::get_seeded_random; use crate::state::{BoardState, GameState, SharedState, Turn}; use std::cell::Cell; @@ -13,7 +15,7 @@ use yew::prelude::*; pub struct MainMenu {} pub enum MainMenuMessage { - SinglePlayer, + SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, NetworkedMultiplayer, } @@ -33,14 +35,47 @@ impl Component for MainMenu { .expect("state to be set"); match shared.game_state.get() { GameState::MainMenu => { + let player_type: Turn; + { + let mut rng = get_seeded_random().expect("Random should be available"); + player_type = if rng.rand_range(0..2) == 0 { + Turn::CyanPlayer + } else { + Turn::MagentaPlayer + }; + } + + let easy_player_type = player_type; + let normal_player_type = player_type; + let hard_player_type = player_type; + + let onclick_singleplayer_easy = ctx.link().callback(move |_| { + MainMenuMessage::SinglePlayer(easy_player_type, AIDifficulty::Easy) + }); + let onclick_singleplayer_normal = ctx.link().callback(move |_| { + MainMenuMessage::SinglePlayer(normal_player_type, AIDifficulty::Normal) + }); + let onclick_singleplayer_hard = ctx.link().callback(move |_| { + MainMenuMessage::SinglePlayer(hard_player_type, AIDifficulty::Hard) + }); + let onclick_local_multiplayer = ctx.link().callback(|_| MainMenuMessage::LocalMultiplayer); + html! {
{"Please pick a game mode."} - +
+ + + +
@@ -77,6 +112,20 @@ impl Component for MainMenu { .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) = + 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)); + } } true @@ -131,7 +180,7 @@ impl Component for Slot { match shared.game_state.get() { GameState::MainMenu => return false, - GameState::SinglePlayer + GameState::SinglePlayer(_, _) | GameState::LocalMultiplayer | GameState::NetworkedMultiplayer => (), GameState::PostGameResults(_) => return false, @@ -664,6 +713,18 @@ impl Component for Wrapper { } } } // else: game is still ongoing after logic check + + // 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 { + // get AI's choice + let choice = get_ai_choice(ai_difficulty, Turn::CyanPlayer, &shared.board) + .expect("AI should have an available choice"); + ctx.link() + .send_message(WrapperMsg::Pressed(usize::from(choice) as u8)); + } + } } // WrapperMsg::Pressed(idx) => } // match (msg)