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"
version = "0.1.0"
dependencies = [
"js-sys",
"log",
"oorandom",
"wasm-logger",

View 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"

View file

@ -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;

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
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;
}

View 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))
}

View 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,
}

View 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"}
<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)