]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
WIP Utility based AI, some impl. progress
authorStephen Seo <seo.disparate@gmail.com>
Mon, 7 Mar 2022 05:23:39 +0000 (14:23 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Mon, 7 Mar 2022 05:23:39 +0000 (14:23 +0900)
front_end/Cargo.lock
front_end/Cargo.toml
front_end/src/ai/mod.rs
front_end/src/constants.rs

index 05c9009937911883cfceca84b0a064ea80336e5f..4a61525f0ebc3b2d9e7c454ba84fd62f8ea1659e 100644 (file)
@@ -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"
index 6a6b1cd3902137aa1825dc76c45f9c0825503e29..a089c9a0421987a5277f4a9acc93e58ca26de855 100644 (file)
@@ -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"
index 6cbb07efe334a5bbd0ad9ed0cad85e032993e566..bca1b7fb381b49da606fd134d7b292f120055624 100644 (file)
@@ -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,
+}
+
+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
 }
index 43ae0e792eb1452f0563fb690da398af47ac003b..3b111941df507b3b2f6f410a11e5574baf1e3131 100644 (file)
@@ -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;