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:
parent
e35870b240
commit
89a12623b4
7 changed files with 91 additions and 17 deletions
1
front_end/Cargo.lock
generated
1
front_end/Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}>
|
||||||
|
{"Singleplayer Easy"}
|
||||||
</button>
|
</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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue