WIP Utility based AI, some impl. progress
This commit is contained in:
parent
b2fbd8cdcc
commit
93af9c234d
4 changed files with 297 additions and 3 deletions
60
front_end/Cargo.lock
generated
60
front_end/Cargo.lock
generated
|
@ -41,11 +41,23 @@ name = "four_line_dropper_frontend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"wasm-logger",
|
"wasm-logger",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"yew",
|
"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]]
|
[[package]]
|
||||||
name = "gloo"
|
name = "gloo"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -189,6 +201,12 @@ version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
@ -198,6 +216,12 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -240,6 +264,36 @@ dependencies = [
|
||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -332,6 +386,12 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
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]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.79"
|
version = "0.2.79"
|
||||||
|
|
|
@ -10,3 +10,4 @@ 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"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
|
@ -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)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum AIDifficulty {
|
pub enum AIDifficulty {
|
||||||
|
@ -16,8 +21,233 @@ pub enum SlotChoice {
|
||||||
Slot4,
|
Slot4,
|
||||||
Slot5,
|
Slot5,
|
||||||
Slot6,
|
Slot6,
|
||||||
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ai_choice(difficulty: AIDifficulty, board: &BoardType) -> Result<SlotChoice, String> {
|
impl From<SlotChoice> for usize {
|
||||||
Err("Unimplemented".into())
|
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,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,6 @@ pub const ROWS: u8 = 8;
|
||||||
pub const COLS: u8 = 7;
|
pub const COLS: u8 = 7;
|
||||||
|
|
||||||
pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
|
pub const INFO_TEXT_MAX_ITEMS: u32 = 100;
|
||||||
|
|
||||||
|
pub const AI_EASY_MAX_CHOICES: usize = 5;
|
||||||
|
pub const AI_NORMAL_MAX_CHOICES: usize = 3;
|
||||||
|
|
Loading…
Reference in a new issue