]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
Incorporate game AI into game
authorStephen Seo <seo.disparate@gmail.com>
Thu, 10 Mar 2022 07:17:16 +0000 (16:17 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Thu, 10 Mar 2022 07:17:16 +0000 (16:17 +0900)
Can select from three difficulties, and the AI makes their move when it
is their turn. AI probably still needs some tweaking..

front_end/Cargo.lock
front_end/Cargo.toml
front_end/index.html
front_end/src/ai.rs
front_end/src/random_helper.rs
front_end/src/state.rs
front_end/src/yew_components.rs

index e89084f4fb6f186a27189b7059e8b50796b87596..c11d6f968d286742f3963005af7d1ae6243aded8 100644 (file)
@@ -40,6 +40,7 @@ dependencies = [
 name = "four_line_dropper_frontend"
 version = "0.1.0"
 dependencies = [
+ "js-sys",
  "log",
  "oorandom",
  "wasm-logger",
index f27b53957cec7a67012f6006297b1ad5202f7486..e394755f7f1112ad1e0289601585e56fc7033d60 100644 (file)
@@ -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"
index 40fbfff33b815ab09771689392aa7168f9e63430..022cc844bb4613ffe0181fe86f72469333594ad7 100644 (file)
         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;
index 05476e8cd0b7acc00e740d3e1c4e64f6b67a3415..65da7a96c9d0f894a2b7654a04b4b69484250e77 100644 (file)
@@ -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;
     }
 
index 17bde789bebd768633daf66330b25c2dfb786a64..16456dbed55b0c70ac6b5abd5b9e162c056d9747 100644 (file)
@@ -1,12 +1,6 @@
+use js_sys::Math::random;
 use oorandom::Rand32;
 
-use std::time::{SystemTime, UNIX_EPOCH};
-
 pub fn get_seeded_random() -> Result<Rand32, String> {
-    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))
 }
index 38b76bf778254d15a132284aa5159dee0bedd1bc..e9214715a27a310525d41e21d05896bcbdd90344 100644 (file)
@@ -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<MainMenuMessage> 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,
         }
index e19706cc1f9fd518b616b32db0c308acd29a074d..3f9a97c34497de8305b74eaf9ab51116f9e07aea 100644 (file)
@@ -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! {
                     <div class={"menu"} id={"mainmenu"}>
                         <b class={"menuText"}>{"Please pick a game mode."}</b>
-                        <button class={"menuSinglePlayer"}>
-                            {"Singleplayer"}
-                        </button>
+                        <div class={"singlePlayerMenu"}>
+                            <button class={"menuSinglePlayerEasy"} onclick={onclick_singleplayer_easy}>
+                                {"Singleplayer Easy"}
+                            </button>
+                            <button class={"menuSinglePlayerNormal"} onclick={onclick_singleplayer_normal}>
+                                {"Singleplayer Normal"}
+                            </button>
+                            <button class={"menuSinglePlayerHard"} onclick={onclick_singleplayer_hard}>
+                                {"Singleplayer Hard"}
+                            </button>
+                        </div>
                         <button class={"menuLocalMultiplayer"} onclick={onclick_local_multiplayer}>
                             {"Local Multiplayer"}
                         </button>
@@ -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("<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
+                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::<Wrapper>()
+                    .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)