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"
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"
"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"
"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"
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"
-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 {
Slot4,
Slot5,
Slot6,
+ Invalid,
+}
+
+impl From<SlotChoice> 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<usize> 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, board: &BoardType) -> Result<SlotChoice, String> {
- Err("Unimplemented".into())
+pub fn get_ai_choice(
+ difficulty: AIDifficulty,
+ player: Turn,
+ board: &BoardType,
+) -> Result<SlotChoice, String> {
+ 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<SlotChoice, String> {
+ let mut maximums: BTreeMap<i64, usize> = 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::<usize>() % 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::<Vec<(&i64, &usize)>>()[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<f64> {
+ // 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
}