2022-04-06 11:49:54 +00:00
//Four Line Dropper Frontend/Backend - A webapp that allows one to play a game of Four Line Dropper
//Copyright (C) 2022 Stephen Seo
//
//This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
2022-03-07 05:23:39 +00:00
use std ::collections ::BTreeMap ;
use crate ::constants ::{ AI_EASY_MAX_CHOICES , AI_NORMAL_MAX_CHOICES , COLS , ROWS } ;
2022-03-10 07:44:01 +00:00
use crate ::game_logic ::check_win_draw ;
2022-03-09 07:22:01 +00:00
use crate ::random_helper ::get_seeded_random ;
2022-03-10 07:44:01 +00:00
use crate ::state ::{ board_deep_clone , BoardState , BoardType , Turn } ;
2022-03-07 05:23:39 +00:00
2022-03-07 04:12:05 +00:00
#[ derive(Copy, Clone, Debug, PartialEq, Eq) ]
pub enum AIDifficulty {
Easy ,
Normal ,
Hard ,
}
#[ derive(Copy, Clone, Debug, PartialEq, Eq) ]
pub enum SlotChoice {
Slot0 ,
Slot1 ,
Slot2 ,
Slot3 ,
Slot4 ,
Slot5 ,
Slot6 ,
2022-03-07 05:23:39 +00:00
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 ,
2022-03-07 08:06:45 +00:00
SlotChoice ::Invalid = > ( ROWS * COLS ) as usize ,
2022-03-07 05:23:39 +00:00
}
}
}
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 ,
}
}
2022-03-07 04:12:05 +00:00
}
2022-03-07 05:23:39 +00:00
pub fn get_ai_choice (
difficulty : AIDifficulty ,
player : Turn ,
board : & BoardType ,
) -> Result < SlotChoice , String > {
2022-03-09 07:22:01 +00:00
let mut rng = get_seeded_random ( ) ? ;
2022-03-07 05:23:39 +00:00
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 ) {
2022-03-10 08:01:01 +00:00
utilities . push ( ( i , utility ) ) ;
2022-03-07 05:23:39 +00:00
}
}
2022-03-07 05:35:09 +00:00
if utilities . is_empty ( ) {
return Err ( " All slots are full " . into ( ) ) ;
}
2022-03-07 10:50:43 +00:00
// shuffle utilities for the cases where there are equivalent utilities
if utilities . len ( ) > 1 {
for i in 1 .. utilities . len ( ) {
2022-03-09 07:22:01 +00:00
utilities . swap ( i , rng . rand_range ( 0 .. ( ( i + 1 ) as u32 ) ) as usize ) ;
2022-03-07 10:50:43 +00:00
}
}
2022-03-09 07:22:01 +00:00
let mut pick_some_of_choices = | amount : usize | -> Result < SlotChoice , String > {
2022-03-07 05:23:39 +00:00
let mut maximums : BTreeMap < i64 , usize > = BTreeMap ::new ( ) ;
2022-03-07 10:50:43 +00:00
for ( idx , utility ) in & utilities {
2022-03-07 05:35:09 +00:00
// f64 cannot be used as Key since it doesn't implement Ord.
// Use i64 as a substitute, noting that the map stores in ascending
// order.
let mut utility_value = ( utility * 10000.0 ) as i64 ;
while maximums . contains_key ( & utility_value ) {
2022-03-09 07:22:01 +00:00
utility_value + = rng . rand_range ( 0 .. 7 ) as i64 - 3 ;
2022-03-07 05:23:39 +00:00
}
2022-03-07 10:50:43 +00:00
maximums . insert ( utility_value , * idx ) ;
2022-03-07 05:23:39 +00:00
}
2022-03-07 05:35:09 +00:00
// don't pick from more items than what exists
2022-03-07 05:23:39 +00:00
let mod_amount = if maximums . len ( ) < amount {
maximums . len ( )
} else {
amount
} ;
2022-03-07 05:35:09 +00:00
// don't use random if only 1 item is to be picked
let random_number : usize = if mod_amount > 1 {
2022-03-09 07:22:01 +00:00
rng . rand_u32 ( ) as usize % mod_amount
2022-03-07 05:35:09 +00:00
} else {
0
} ;
2022-03-07 05:23:39 +00:00
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.
2022-03-07 10:50:43 +00:00
// This is done because BTreeMap stores keys in ascending order.
2022-03-07 05:23:39 +00:00
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
2022-03-07 07:59:18 +00:00
let mut max = - 1.0 f64 ;
2022-03-07 05:23:39 +00:00
let mut max_idx : usize = 0 ;
2022-03-07 07:59:18 +00:00
for ( idx , utility ) in utilities {
if utility > max {
max = utility ;
2022-03-07 05:23:39 +00:00
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 ;
}
2022-03-10 07:17:16 +00:00
while idx < ( ( ROWS - 1 ) * COLS ) as usize
& & board [ idx + COLS as usize ] . get ( ) = = BoardState ::Empty
{
2022-03-07 05:23:39 +00:00
idx + = COLS as usize ;
}
2022-03-07 05:45:57 +00:00
// check if placing a token here is a win
if get_block_amount ( player . get_opposite ( ) , idx , 3 , board ) {
2022-03-07 05:23:39 +00:00
return Some ( 1.0 ) ;
}
2022-03-07 05:45:57 +00:00
// check if placing a token here blocks a win
if get_block_amount ( player , idx , 3 , board ) {
return Some ( 0.9 ) ;
}
let mut utility : f64 = 0.5 ;
// check if placing a token here connects 2 pieces
if get_block_amount ( player . get_opposite ( ) , idx , 2 , board ) {
utility * = 1.5 ;
if utility > = 0.8 {
utility = 0.8 ;
}
}
// check if placing a token here blocks 2 pieces
if get_block_amount ( player , idx , 2 , board ) {
utility * = 1.2 ;
if utility > = 0.8 {
utility = 0.8 ;
}
}
// check if placing a token here connects 1 piece
if get_block_amount ( player . get_opposite ( ) , idx , 1 , board ) {
utility * = 1.09 ;
if utility > = 0.8 {
utility = 0.8 ;
}
}
2022-03-07 05:23:39 +00:00
2022-03-10 07:44:01 +00:00
// check if placing a token here allows the opposing player to win
if idx > = COLS as usize {
let cloned_board = board_deep_clone ( board ) ;
cloned_board [ idx ] . replace ( player . into ( ) ) ;
cloned_board [ idx - ( COLS as usize ) ] . replace ( player . get_opposite ( ) . into ( ) ) ;
if let Some ( ( state , _ ) ) = check_win_draw ( & cloned_board ) {
if state = = player . get_opposite ( ) . into ( ) {
utility * = 0.1 ;
}
}
}
2022-03-07 05:45:57 +00:00
Some ( utility )
2022-03-07 05:23:39 +00:00
}
2022-03-07 05:45:57 +00:00
/// Returns true if placing a token at idx will block the opposite player that
/// has "amount" in a line (horizontally, vertically, and diagonally).
fn get_block_amount ( player : Turn , idx : usize , amount : usize , board : & BoardType ) -> bool {
2022-03-07 05:23:39 +00:00
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 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
return true ;
}
} else {
break ;
}
}
// check right
2022-03-10 07:39:26 +00:00
// count = 0; // don't reset count, since horizontal may pass through idx
2022-03-07 05:23:39 +00:00
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 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
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 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
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 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
return true ;
}
} else {
break ;
}
}
2022-03-10 07:39:26 +00:00
// check diagonal right up
// count = 0; // don't reset count as diagonal may pass through idx
2022-03-07 05:23:39 +00:00
temp_idx = idx ;
2022-03-10 07:39:26 +00:00
while temp_idx % ( COLS as usize ) < ( COLS - 1 ) as usize & & temp_idx / ( COLS as usize ) > 0 {
temp_idx = temp_idx + 1 - COLS as usize ;
2022-03-07 05:23:39 +00:00
if board [ temp_idx ] . get ( ) = = opposite . into ( ) {
count + = 1 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
return true ;
}
} else {
break ;
}
}
2022-03-10 07:39:26 +00:00
// check diagonal right down
2022-03-07 05:23:39 +00:00
count = 0 ;
temp_idx = idx ;
2022-03-10 07:39:26 +00:00
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 ;
2022-03-07 05:23:39 +00:00
if board [ temp_idx ] . get ( ) = = opposite . into ( ) {
count + = 1 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
return true ;
}
} else {
break ;
}
}
2022-03-10 07:39:26 +00:00
// check diagonal left up
// count = 0; // don't reset count as diagonal may pass through idx
2022-03-07 05:23:39 +00:00
temp_idx = idx ;
2022-03-10 07:39:26 +00:00
while temp_idx % ( COLS as usize ) > 0 & & temp_idx / ( COLS as usize ) > 0 {
temp_idx = temp_idx - 1 - COLS as usize ;
2022-03-07 05:23:39 +00:00
if board [ temp_idx ] . get ( ) = = opposite . into ( ) {
count + = 1 ;
2022-03-07 05:45:57 +00:00
if count > = amount {
2022-03-07 05:23:39 +00:00
return true ;
}
} else {
break ;
}
}
// exhausted all possible potential wins, therefore does not block a win
false
2022-03-07 04:12:05 +00:00
}