Incorporate game AI into game

Can select from three difficulties, and the AI makes their move when it
is their turn. AI probably still needs some tweaking..
This commit is contained in:
Stephen Seo 2022-03-10 16:17:16 +09:00
parent e35870b240
commit 89a12623b4
7 changed files with 91 additions and 17 deletions

1
front_end/Cargo.lock generated
View file

@ -40,6 +40,7 @@ dependencies = [
name = "four_line_dropper_frontend" name = "four_line_dropper_frontend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"js-sys",
"log", "log",
"oorandom", "oorandom",
"wasm-logger", "wasm-logger",

View file

@ -10,4 +10,5 @@ yew = "0.19"
log = "0.4.6" log = "0.4.6"
wasm-logger = "0.2.0" 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"] }
js-sys = "0.3.56"
oorandom = "11.1.3" oorandom = "11.1.3"

View file

@ -20,9 +20,23 @@
gap: 8px; gap: 8px;
grid-auto-rows: 33%; grid-auto-rows: 33%;
} }
button.menuSinglePlayer { div.singlePlayerMenu {
grid-row: 2; grid-row: 2;
grid-column: 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 { button.menuLocalMultiplayer {
grid-row: 2; grid-row: 2;

View file

@ -148,7 +148,9 @@ fn get_utility_for_slot(player: Turn, slot: SlotChoice, board: &BoardType) -> Op
// slot is full, cannot place in slot // slot is full, cannot place in slot
return None; 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; idx += COLS as usize;
} }

View file

@ -1,12 +1,6 @@
use js_sys::Math::random;
use oorandom::Rand32; use oorandom::Rand32;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn get_seeded_random() -> Result<Rand32, String> { pub fn get_seeded_random() -> Result<Rand32, String> {
let now = SystemTime::now(); Ok(Rand32::new((random() * u64::MAX as f64) as u64))
let duration = now
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("{}", e))?;
Ok(Rand32::new(duration.as_secs()))
} }

View file

@ -1,3 +1,4 @@
use crate::ai::AIDifficulty;
use crate::yew_components::MainMenuMessage; use crate::yew_components::MainMenuMessage;
use std::cell::Cell; use std::cell::Cell;
use std::fmt::Display; use std::fmt::Display;
@ -6,7 +7,7 @@ use std::rc::Rc;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GameState { pub enum GameState {
MainMenu, MainMenu,
SinglePlayer, SinglePlayer(Turn, AIDifficulty),
LocalMultiplayer, LocalMultiplayer,
NetworkedMultiplayer, NetworkedMultiplayer,
PostGameResults(BoardState), PostGameResults(BoardState),
@ -21,7 +22,7 @@ impl Default for GameState {
impl From<MainMenuMessage> for GameState { impl From<MainMenuMessage> for GameState {
fn from(msg: MainMenuMessage) -> Self { fn from(msg: MainMenuMessage) -> Self {
match msg { match msg {
MainMenuMessage::SinglePlayer => GameState::SinglePlayer, MainMenuMessage::SinglePlayer(t, ai) => GameState::SinglePlayer(t, ai),
MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer, MainMenuMessage::LocalMultiplayer => GameState::LocalMultiplayer,
MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer, MainMenuMessage::NetworkedMultiplayer => GameState::NetworkedMultiplayer,
} }

View file

@ -1,8 +1,10 @@
use crate::ai::{get_ai_choice, AIDifficulty};
use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS}; use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS};
use crate::game_logic::{check_win_draw, WinType}; use crate::game_logic::{check_win_draw, WinType};
use crate::html_helper::{ use crate::html_helper::{
append_to_info_text, element_append_class, element_remove_class, get_window_document, 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 crate::state::{BoardState, GameState, SharedState, Turn};
use std::cell::Cell; use std::cell::Cell;
@ -13,7 +15,7 @@ use yew::prelude::*;
pub struct MainMenu {} pub struct MainMenu {}
pub enum MainMenuMessage { pub enum MainMenuMessage {
SinglePlayer, SinglePlayer(Turn, AIDifficulty),
LocalMultiplayer, LocalMultiplayer,
NetworkedMultiplayer, NetworkedMultiplayer,
} }
@ -33,14 +35,47 @@ impl Component for MainMenu {
.expect("state to be set"); .expect("state to be set");
match shared.game_state.get() { match shared.game_state.get() {
GameState::MainMenu => { 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 = let onclick_local_multiplayer =
ctx.link().callback(|_| MainMenuMessage::LocalMultiplayer); ctx.link().callback(|_| MainMenuMessage::LocalMultiplayer);
html! { html! {
<div class={"menu"} id={"mainmenu"}> <div class={"menu"} id={"mainmenu"}>
<b class={"menuText"}>{"Please pick a game mode."}</b> <b class={"menuText"}>{"Please pick a game mode."}</b>
<button class={"menuSinglePlayer"}> <div class={"singlePlayerMenu"}>
{"Singleplayer"} <button class={"menuSinglePlayerEasy"} onclick={onclick_singleplayer_easy}>
</button> {"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}> <button class={"menuLocalMultiplayer"} onclick={onclick_local_multiplayer}>
{"Local Multiplayer"} {"Local Multiplayer"}
</button> </button>
@ -77,6 +112,20 @@ impl Component for MainMenu {
.get_element_by_id("info_text1") .get_element_by_id("info_text1")
.expect("info_text1 should exist"); .expect("info_text1 should exist");
info_text_turn.set_inner_html("<p><b class=\"cyan\">It is CyanPlayer's Turn</b></p>"); 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 true
@ -131,7 +180,7 @@ impl Component for Slot {
match shared.game_state.get() { match shared.game_state.get() {
GameState::MainMenu => return false, GameState::MainMenu => return false,
GameState::SinglePlayer GameState::SinglePlayer(_, _)
| GameState::LocalMultiplayer | GameState::LocalMultiplayer
| GameState::NetworkedMultiplayer => (), | GameState::NetworkedMultiplayer => (),
GameState::PostGameResults(_) => return false, GameState::PostGameResults(_) => return false,
@ -664,6 +713,18 @@ impl Component for Wrapper {
} }
} }
} // else: game is still ongoing after logic check } // 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) => } // WrapperMsg::Pressed(idx) =>
} // match (msg) } // match (msg)