diff --git a/front_end/Cargo.lock b/front_end/Cargo.lock index 05c9009..4a61525 100644 --- a/front_end/Cargo.lock +++ b/front_end/Cargo.lock @@ -41,11 +41,23 @@ name = "four_line_dropper_frontend" version = "0.1.0" dependencies = [ "log", + "rand", "wasm-logger", "web-sys", "yew", ] +[[package]] +name = "getrandom" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gloo" version = "0.4.2" @@ -189,6 +201,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + [[package]] name = "log" version = "0.4.14" @@ -198,6 +216,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -240,6 +264,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "ryu" version = "1.0.9" @@ -332,6 +386,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "wasm-bindgen" version = "0.2.79" diff --git a/front_end/Cargo.toml b/front_end/Cargo.toml index 6a6b1cd..a089c9a 100644 --- a/front_end/Cargo.toml +++ b/front_end/Cargo.toml @@ -10,3 +10,4 @@ yew = "0.19" log = "0.4.6" wasm-logger = "0.2.0" web-sys = { version = "0.3.56", features = ["Window", "Document", "Element"] } +rand = "0.8.5" diff --git a/front_end/src/ai/mod.rs b/front_end/src/ai/mod.rs index 6cbb07e..bca1b7f 100644 --- a/front_end/src/ai/mod.rs +++ b/front_end/src/ai/mod.rs @@ -1,4 +1,9 @@ -use crate::state::BoardType; +use std::collections::BTreeMap; + +use crate::constants::{AI_EASY_MAX_CHOICES, AI_NORMAL_MAX_CHOICES, COLS, ROWS}; +use crate::state::{BoardState, BoardType, Turn}; + +use rand::{thread_rng, Rng}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum AIDifficulty { @@ -16,8 +21,233 @@ pub enum SlotChoice { Slot4, Slot5, Slot6, + Invalid, } -pub fn get_ai_choice(difficulty: AIDifficulty, board: &BoardType) -> Result { - Err("Unimplemented".into()) +impl From for usize { + fn from(slot_choice: SlotChoice) -> Self { + match slot_choice { + SlotChoice::Slot0 => 0, + SlotChoice::Slot1 => 1, + SlotChoice::Slot2 => 2, + SlotChoice::Slot3 => 3, + SlotChoice::Slot4 => 4, + SlotChoice::Slot5 => 5, + SlotChoice::Slot6 => 6, + SlotChoice::Invalid => 10, + } + } +} + +impl From for SlotChoice { + fn from(idx: usize) -> Self { + if idx >= (ROWS * COLS) as usize { + return SlotChoice::Invalid; + } + + match idx % (COLS as usize) { + 0 => SlotChoice::Slot0, + 1 => SlotChoice::Slot1, + 2 => SlotChoice::Slot2, + 3 => SlotChoice::Slot3, + 4 => SlotChoice::Slot4, + 5 => SlotChoice::Slot5, + 6 => SlotChoice::Slot6, + _ => SlotChoice::Invalid, + } + } +} + +pub fn get_ai_choice( + difficulty: AIDifficulty, + player: Turn, + board: &BoardType, +) -> Result { + let mut utilities = Vec::with_capacity(COLS as usize); + for i in 0..(COLS as usize) { + let slot = i.into(); + if slot == SlotChoice::Invalid { + return Err("Internal error: get_ai_choice() iterated to SlotChoice::Invalid".into()); + } + if let Some(utility) = get_utility_for_slot(player, slot, board) { + utilities.push(utility); + } + } + + let pick_some_of_choices = |amount: usize| -> Result { + let mut maximums: BTreeMap = BTreeMap::new(); + for (idx, utility) in utilities.iter().enumerate() { + if *utility <= 0.0 { + continue; + } + maximums.insert((utility * 10000.0) as i64, idx); + } + let mod_amount = if maximums.len() < amount { + maximums.len() + } else { + amount + }; + let random_number: usize = thread_rng().gen::() % mod_amount; + let rand_idx = maximums.len() - 1 - random_number; + // turns the map into a vector of (key, value), then pick out of the + // last few values by index the "value" which is the SlotChoice. + Ok((*maximums.iter().collect::>()[rand_idx].1).into()) + }; + + match difficulty { + AIDifficulty::Easy => pick_some_of_choices(AI_EASY_MAX_CHOICES), + AIDifficulty::Normal => pick_some_of_choices(AI_NORMAL_MAX_CHOICES), + AIDifficulty::Hard => { + // only pick the best option all the time + let mut max = 0.0f64; + let mut max_idx: usize = 0; + for (idx, utility) in utilities.iter().enumerate() { + if *utility > max { + max = *utility; + max_idx = idx; + } + } + Ok(max_idx.into()) + } + } +} + +/// Returns a value between 0.0 and 1.0 where 1.0 is highest utility +/// "None" indicates it is impossible to place at the given slot +fn get_utility_for_slot(player: Turn, slot: SlotChoice, board: &BoardType) -> Option { + // get idx of location where dropped token will reside in + let mut idx: usize = slot.into(); + if board[idx].get() != BoardState::Empty { + // slot is full, cannot place in slot + return None; + } + while idx < (ROWS * COLS) as usize && board[idx + COLS as usize].get() == BoardState::Empty { + idx += COLS as usize; + } + + // check if placing a token here blocks a win + if get_block_win(player, idx, board) { + return Some(1.0); + } + + // TODO more impl here + + Some(0.0) +} + +/// Returns true if placing a token at idx will block the opposite player from winning +fn get_block_win(player: Turn, idx: usize, board: &BoardType) -> bool { + let opposite = player.get_opposite(); + + // setup for checks + let mut count = 0; + let mut temp_idx = idx; + + // check left + while temp_idx % (COLS as usize) > 0 { + temp_idx -= 1; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check right + count = 0; + temp_idx = idx; + while temp_idx % (COLS as usize) < (COLS - 1) as usize { + temp_idx += 1; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check down + count = 0; + temp_idx = idx; + while temp_idx / (COLS as usize) < (ROWS - 1) as usize { + temp_idx += COLS as usize; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check diagonal left down + count = 0; + temp_idx = idx; + while temp_idx % (COLS as usize) > 0 && temp_idx / (COLS as usize) < (ROWS - 1) as usize { + temp_idx = temp_idx - 1 + COLS as usize; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check diagonal right down + count = 0; + temp_idx = idx; + while temp_idx % (COLS as usize) < (COLS - 1) as usize + && temp_idx / (COLS as usize) < (ROWS - 1) as usize + { + temp_idx = temp_idx + 1 + COLS as usize; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check diagonal left up + count = 0; + temp_idx = idx; + while temp_idx % (COLS as usize) > 0 && temp_idx / (COLS as usize) > 0 { + temp_idx = temp_idx - 1 - COLS as usize; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // check diagonal right up + count = 0; + temp_idx = idx; + while temp_idx % (COLS as usize) < (COLS - 1) as usize && temp_idx / (COLS as usize) > 0 { + temp_idx = temp_idx + 1 - COLS as usize; + if board[temp_idx].get() == opposite.into() { + count += 1; + if count >= 3 { + return true; + } + } else { + break; + } + } + + // exhausted all possible potential wins, therefore does not block a win + false } diff --git a/front_end/src/constants.rs b/front_end/src/constants.rs index 43ae0e7..3b11194 100644 --- a/front_end/src/constants.rs +++ b/front_end/src/constants.rs @@ -2,3 +2,6 @@ pub const ROWS: u8 = 8; pub const COLS: u8 = 7; pub const INFO_TEXT_MAX_ITEMS: u32 = 100; + +pub const AI_EASY_MAX_CHOICES: usize = 5; +pub const AI_NORMAL_MAX_CHOICES: usize = 3;