AI perfect chance, rendering with proportions instead of absolute pixel values

This commit is contained in:
andy 2021-07-14 14:08:54 +01:00
parent ab0d0e5df3
commit 40d47f8042
8 changed files with 154 additions and 63 deletions

View File

@ -1,12 +1,15 @@
//! AI player logic //! AI player logic
use indextree::{Arena, Node, NodeId, NodeEdge}; use indextree::{Arena, Node, NodeId, NodeEdge};
use rand::prelude::*;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
extern crate wasm_bindgen; extern crate wasm_bindgen;
// use wasm_bindgen::prelude::*; // use wasm_bindgen::prelude::*;
use crate::log; use crate::log;
use crate::log_error;
use crate::board::{Board, BrdIdx}; use crate::board::{Board, BrdIdx};
use crate::board::enums::{MoveType, Moveable, Team}; use crate::board::enums::{MoveType, Moveable, Team};
@ -63,13 +66,15 @@ pub struct Computer {
pub search_depth: usize, pub search_depth: usize,
pub team: Team, pub team: Team,
pub last_node_count: usize, pub last_node_count: usize,
pub perfect_chance: f64,
} }
impl Computer { impl Computer {
pub fn new(search_depth: usize, team: Team) -> Computer { pub fn new(search_depth: usize, team: Team, perfect_chance: f64) -> Computer {
Computer { Computer {
search_depth, search_depth,
team, team,
perfect_chance,
last_node_count: 0, last_node_count: 0,
} }
} }
@ -333,49 +338,88 @@ impl Computer {
.expect("No node returned for node id") .expect("No node returned for node id")
.get(); // get BoardNode from Node .get(); // get BoardNode from Node
// when boards have equal scores, store for shuffling and selection // node ids of available next moves
let mut equal_scores = Vec::with_capacity(10); let possible_moves: Vec<NodeId> = root_node.children(&tree).collect();
if possible_moves.len() == 0 {
return None;
}
// DEBUG // DEBUG
#[cfg(feature = "debug_logs")] #[cfg(feature = "debug_logs")]
{ {
log!("Current root score: {}", root_board_node.score); log!("Current root score: {}", root_board_node.score);
let scores: Vec<NodeId> = root_node let scores: Vec<isize> = possible_moves
.children(&tree) .iter()
.collect(); .map(|n| tree.get(*n).unwrap().get().score)
let scores: Vec<isize> = scores
.into_iter()
.map(|n| tree.get(n).unwrap().get().score)
.collect(); .collect();
log!("Next boards scores: {:?}", scores); log!("Next boards scores: {:?}", scores);
} }
// search through root node's children for the same score let mut rng = rand::thread_rng();
for n in root_node.children(&tree) { // random number to compare against threshold
let perfect_num: f64 = rng.gen();
// get each board // make perfect move
let iter_board_node = tree if perfect_num < self.perfect_chance {
.get(n) // get Node #[cfg(feature = "debug_logs")]
.expect("No node returned for node id") log!("Making perfect move");
.get(); // get BoardNode from Node
if root_board_node.score == iter_board_node.score { // get boards of equal score that are perfect for the given player
equal_scores.push(iter_board_node); let possible_perfect_moves: Vec<&BoardNode> = possible_moves
// return Some(iter_board_node.board.clone()); .iter()
.map(
// get immutable references to BoardNodes for possible moves
|n| tree
.get(*n) // get Node using NodeID
.expect("Unable to get perfect move data from tree node")
.get() // get *BoardNode from Node
)
.filter(
// filter for only scores of root node which are perfect moves
|b| b.score == root_board_node.score
)
.collect();
// weird error, no child nodes have same score as root node
// this is odd because the root nodes score is either the max or min of it's children
if possible_perfect_moves.len() == 0 {
log_error!("No next moves matched the score of the root node, picking randomly instead");
Some(Computer::random_choice(&tree, possible_moves, &mut rng))
} }
} // only one possible move, use that
else if possible_perfect_moves.len() == 1 {
Some(possible_perfect_moves[0].board.clone())
}
// more than one possible perfect move to make, choose one randomly
else {
Some(
possible_perfect_moves
.choose(&mut rng) // random choice
.unwrap() // unwrap Option
.board
.clone()
)
}
}
// get random move
else {
#[cfg(feature = "debug_logs")]
log!("Making random move");
if equal_scores.len() == 0 { Some(Computer::random_choice(&tree, possible_moves, &mut rng))
None
} else if equal_scores.len() == 1 {
Some(equal_scores[0].board.clone())
} else {
Some(equal_scores
.choose(&mut rand::thread_rng())
.unwrap()
.board
.clone()
)
} }
} }
/// Get a random board from possible node IDs and associated tree
fn random_choice(tree: &Arena<BoardNode>, possible_moves: Vec<NodeId>, rng: &mut ThreadRng) -> Board {
let chosen_move = possible_moves.choose(rng).unwrap();
tree
.get(*chosen_move)
.expect("Unable to get random move data from tree node")
.get()
.board
.clone()
}
} }

View File

@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
use crate::board::Square; use crate::board::Square;
use crate::board::enums::Strength::*; use crate::board::enums::Strength::*;
use crate::log; // use crate::log;
// use Team::*; // use Team::*;
@ -25,7 +25,7 @@ fn available_moves() {
// _ . _ // _ . _
let mut brd = Board::new(3, 2, White); let mut brd = Board::new(3, 2, White);
let comp = Computer::new(3, White); let comp = Computer::new(3, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -50,7 +50,7 @@ fn available_moves_jumps() {
// _ . _ . // _ . _ .
let mut brd = Board::new(4, 4, White); let mut brd = Board::new(4, 4, White);
let comp = Computer::new(3, White); let comp = Computer::new(3, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -71,7 +71,7 @@ fn available_moves_jumps() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn available_moves_std_brd() { fn available_moves_std_brd() {
let brd = Board::init_game(Board::new(8, 8, White), 3); let brd = Board::init_game(Board::new(8, 8, White), 3);
let comp = Computer::new(3, White); let comp = Computer::new(3, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -87,7 +87,7 @@ fn available_moves_std_brd() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn expand_node() { fn expand_node() {
let brd = Board::init_game(Board::new(8, 8, White), 3); let brd = Board::init_game(Board::new(8, 8, White), 3);
let mut comp = Computer::new(3, White); let mut comp = Computer::new(3, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -106,7 +106,7 @@ fn expand_node() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn expand_layer() { fn expand_layer() {
let brd = Board::init_game(Board::new(8, 8, White), 3); let brd = Board::init_game(Board::new(8, 8, White), 3);
let mut comp = Computer::new(3, White); let mut comp = Computer::new(3, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -123,7 +123,7 @@ fn expand_layer() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn leaf_nodes() { fn leaf_nodes() {
let brd = Board::init_game(Board::new(8, 8, White), 3); let brd = Board::init_game(Board::new(8, 8, White), 3);
let mut comp = Computer::new(3, White); let mut comp = Computer::new(3, White, 0.5);
let mut tree = Arena::new(); let mut tree = Arena::new();
let id = tree.new_node(BoardNode::brd(brd)); let id = tree.new_node(BoardNode::brd(brd));
@ -160,7 +160,7 @@ fn insert_scores_all_take() {
brd.set_cell(brd.cell_idx(BrdIdx::from(1, 4)), Square::pc(White, Man)); brd.set_cell(brd.cell_idx(BrdIdx::from(1, 4)), Square::pc(White, Man));
brd.set_cell(brd.cell_idx(BrdIdx::from(2, 1)), Square::pc(Black, Man)); brd.set_cell(brd.cell_idx(BrdIdx::from(2, 1)), Square::pc(Black, Man));
brd.set_cell(brd.cell_idx(BrdIdx::from(2, 3)), Square::pc(Black, Man)); brd.set_cell(brd.cell_idx(BrdIdx::from(2, 3)), Square::pc(Black, Man));
let mut comp = Computer::new(1, White); let mut comp = Computer::new(1, White, 0.5);
// log!("{}", brd); // log!("{}", brd);
@ -203,7 +203,7 @@ fn insert_scores_one_take() {
brd.set_cell(brd.cell_idx(BrdIdx::from(1, 4)), Square::pc(White, Man)); brd.set_cell(brd.cell_idx(BrdIdx::from(1, 4)), Square::pc(White, Man));
brd.set_cell(brd.cell_idx(BrdIdx::from(2, 1)), Square::pc(Black, Man)); brd.set_cell(brd.cell_idx(BrdIdx::from(2, 1)), Square::pc(Black, Man));
// brd.set_cell(brd.cell_idx(BrdIdx::from(2, 3)), Square::pc(Black, Man)); // brd.set_cell(brd.cell_idx(BrdIdx::from(2, 3)), Square::pc(Black, Man));
let mut comp = Computer::new(1, White); let mut comp = Computer::new(1, White, 0.5);
// log!("{}", brd); // log!("{}", brd);

View File

@ -29,6 +29,7 @@ pub struct Game {
painter: Option<Painter>, painter: Option<Painter>,
search_depth: usize, search_depth: usize,
pub last_node_count: usize, pub last_node_count: usize,
pub perfect_chance: f64,
} }
impl Game { impl Game {
@ -95,6 +96,7 @@ impl Game {
self.current.cell(self.current.cell_idx(*idx)) self.current.cell(self.current.cell_idx(*idx))
} }
/// Set tree depth for AI to search to
pub fn set_search_depth(&mut self, search_depth: usize) { pub fn set_search_depth(&mut self, search_depth: usize) {
self.search_depth = search_depth; self.search_depth = search_depth;
} }
@ -114,6 +116,11 @@ impl Game {
} }
} }
/// Set proportion of perfect moves from AI
pub fn set_perfect_chance(&mut self, new_chance: f64) {
self.perfect_chance = new_chance;
}
/// Clear currently selected piece /// Clear currently selected piece
pub fn clear_selected(&mut self) { pub fn clear_selected(&mut self) {
self.selected_piece = None; self.selected_piece = None;
@ -180,6 +187,7 @@ impl Game {
painter: None, painter: None,
search_depth, search_depth,
last_node_count: 0, last_node_count: 0,
perfect_chance: 0.5,
} }
} }
@ -196,6 +204,7 @@ impl Game {
), ),
search_depth, search_depth,
last_node_count: 0, last_node_count: 0,
perfect_chance: 0.5,
} }
} }
@ -215,14 +224,22 @@ impl Game {
/// Create computer, get move from current board and update current board /// Create computer, get move from current board and update current board
pub fn ai_move(&mut self) { pub fn ai_move(&mut self) {
let mut comp = Computer::new(self.search_depth, self.current.current_turn); let mut comp = Computer::new(self.search_depth, self.current.current_turn, self.perfect_chance);
let new_brd = comp.get_move(self.current.clone()); let new_brd = comp.get_move(self.current.clone());
self.last_node_count = comp.last_node_count; self.last_node_count = comp.last_node_count;
match new_brd { match new_brd {
Some(brd) => self.push_new_board(brd), Some(brd) => self.push_new_board(brd),
None => panic!("No AI move returned"), None => {
log!("No possible moves, re-pushing current board");
let mut new_brd = self.current.clone();
new_brd.current_turn = new_brd.current_turn.opponent();
self.push_new_board(new_brd);
},
} }
} }
} }

View File

@ -6,7 +6,7 @@ wasm_bindgen_test_configure!(run_in_browser);
// use crate::board::{Piece}; // use crate::board::{Piece};
use crate::board::enums::Strength::*; use crate::board::enums::Strength::*;
use crate::board::enums::Team::*; // use crate::board::enums::Team::*;
#[wasm_bindgen_test] #[wasm_bindgen_test]

View File

@ -29,6 +29,12 @@ macro_rules! log {
web_sys::console::log_1(&format!( $( $t )* ).into()); web_sys::console::log_1(&format!( $( $t )* ).into());
} }
} }
#[macro_export]
macro_rules! log_error {
( $( $t:tt )* ) => {
web_sys::console::error_1(&format!( $( $t )* ).into());
}
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn init_wasm() { pub fn init_wasm() {

View File

@ -23,8 +23,8 @@ const BLACK_SQUARE: &str = "#000000";
/// Default hex colour value for outline of black squares /// Default hex colour value for outline of black squares
const SQUARE_OUTLINE: &str = "#9c9c9c"; const SQUARE_OUTLINE: &str = "#9c9c9c";
/// Line width when outlining black squares /// Line width when outlining black squares as proportion of min cell dimension
const OUTLINE_WIDTH: f64 = 3.0; const OUTLINE_WIDTH: f64 = 0.05;
/// Whether to outline black squares /// Whether to outline black squares
const DRAW_OUTLINE: bool = true; const DRAW_OUTLINE: bool = true;
@ -46,10 +46,10 @@ const SELECTED_PIECE_OUTLINE: &str = "#d1cf45";
const KING_OUTLINE: &str = "#ffea00"; const KING_OUTLINE: &str = "#ffea00";
/// Whether to outline pieces /// Whether to outline pieces
const DRAW_PIECE_OUTLINES: bool = true; const DRAW_PIECE_OUTLINES: bool = true;
/// Line width for outlining pieces /// Line width for outlining pieces as proportion of piece radius
const PIECE_OUTLINE_WIDTH: f64 = 6.0; const PIECE_OUTLINE_PROPORTION: f64 = 0.25;
/// Margin from square to define piece radius /// Proportion of square that piece fills as proportion of min cell dimension
const PIECE_MARGIN: f64 = 14.0; const PIECE_PROPORTION: f64 = 0.6;
/// Used to paint boards onto HTML canvases /// Used to paint boards onto HTML canvases
#[wasm_bindgen] #[wasm_bindgen]
@ -72,7 +72,7 @@ pub struct Painter {
king_line: JsValue, king_line: JsValue,
piece_lines: bool, piece_lines: bool,
piece_line_width: f64, piece_line_proportion: f64,
square_outline: JsValue, square_outline: JsValue,
outline_width: f64, outline_width: f64,
@ -168,7 +168,7 @@ impl Painter {
selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE), selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE),
king_line: JsValue::from_str(KING_OUTLINE), king_line: JsValue::from_str(KING_OUTLINE),
piece_lines: DRAW_PIECE_OUTLINES, piece_lines: DRAW_PIECE_OUTLINES,
piece_line_width: PIECE_OUTLINE_WIDTH, piece_line_proportion: PIECE_OUTLINE_PROPORTION,
square_outline: JsValue::from_str(SQUARE_OUTLINE), square_outline: JsValue::from_str(SQUARE_OUTLINE),
outline_width: OUTLINE_WIDTH, outline_width: OUTLINE_WIDTH,
@ -201,7 +201,7 @@ impl Painter {
selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE), selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE),
king_line: JsValue::from_str(KING_OUTLINE), king_line: JsValue::from_str(KING_OUTLINE),
piece_lines: DRAW_PIECE_OUTLINES, piece_lines: DRAW_PIECE_OUTLINES,
piece_line_width: PIECE_OUTLINE_WIDTH, piece_line_proportion: PIECE_OUTLINE_PROPORTION,
square_outline: JsValue::from_str(SQUARE_OUTLINE), square_outline: JsValue::from_str(SQUARE_OUTLINE),
outline_width: OUTLINE_WIDTH, outline_width: OUTLINE_WIDTH,
@ -254,13 +254,20 @@ impl Painter {
let cell_height = self.height as usize / board.height; let cell_height = self.height as usize / board.height;
let cell_width = self.width as usize / board.width; let cell_width = self.width as usize / board.width;
let min_dimension = usize::min(cell_width, cell_height) as f64;
let cell_radius = min_dimension * PIECE_PROPORTION / 2.0;
let piece_outline = cell_radius * self.piece_line_proportion;
let square_outline = min_dimension * self.outline_width;
self.context.set_fill_style(&self.white_square); self.context.set_fill_style(&self.white_square);
self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64); self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);
self.context.set_fill_style(&self.black_square); self.context.set_fill_style(&self.black_square);
self.context.set_stroke_style(&self.square_outline); self.context.set_stroke_style(&self.square_outline);
self.context.set_line_width(self.outline_width); self.context.set_line_width(square_outline);
// Draw black squares onto canvas // Draw black squares onto canvas
for i in 0..board.height { for i in 0..board.height {
@ -336,7 +343,7 @@ impl Painter {
match self.context.arc( match self.context.arc(
center_x, center_x,
center_y, center_y,
(cell_width as f64 / 2.0) - PIECE_MARGIN, // radius cell_radius, // radius
0.0, // start angle 0.0, // start angle
f64::consts::PI * 2.0) // end angle f64::consts::PI * 2.0) // end angle
{ {
@ -346,7 +353,7 @@ impl Painter {
self.context.fill(); self.context.fill();
if self.piece_lines { if self.piece_lines {
self.context.set_line_width(self.piece_line_width); self.context.set_line_width(piece_outline);
self.context.stroke() self.context.stroke()
} }
@ -366,7 +373,7 @@ impl Painter {
match self.context.arc( match self.context.arc(
center_x, center_x,
center_y, center_y,
(cell_width as f64 / 2.0) - PIECE_MARGIN, // radius cell_radius, // radius
0.0, // start angle 0.0, // start angle
f64::consts::PI * 2.0) // end angle f64::consts::PI * 2.0) // end angle
{ {
@ -376,7 +383,7 @@ impl Painter {
self.context.fill(); self.context.fill();
if self.piece_lines { if self.piece_lines {
self.context.set_line_width(self.piece_line_width); self.context.set_line_width(piece_outline);
self.context.stroke() self.context.stroke()
} }
} }

View File

@ -119,7 +119,7 @@
</div> </div>
</div> </div>
<div class="row p-3"> <div class="row p-3">
<div class="col-sm-4" title="should the AI play?"> <div class="col-sm-3" title="should the AI play?">
<input class="form-check-input" <input class="form-check-input"
type="checkbox" type="checkbox"
value="" value=""
@ -129,15 +129,19 @@
AI Player AI Player
</label> </label>
</div> </div>
<div class="col-sm-4" title="how many layers deep should the AI search (grows exponentially, be careful)"> <div class="col-sm-3" title="how many layers deep should the AI search (grows exponentially, be careful)">
<input type="number" <input type="number"
id="ai_search_depth" id="ai_search_depth"
name="ai_search_depth" name="ai_search_depth"
min="1" max="10" value="4" min="1" max="10" value="4"
class="form-control"> class="form-control">
<label for="ai_search_depth">ai difficulty <small class="text-muted">moves ahead</small></label> <label for="ai_search_depth">ai clairvoyance <small class="text-muted">moves ahead</small></label>
</div> </div>
<div class="col-sm-4" title="how many nodes were expanded in the search tree"> <div class="col-sm-3" title="what percentage of the AI's moves should be perfect?">
<label for="ai_difficulty" class="form-label">ai difficulty <small class="text-muted">%</small></label>
<input type="range" class="form-range" min="1" max="100" id="ai_difficulty">
</div>
<div class="col-sm-3" title="how many nodes were expanded in the search tree">
<p class="text-muted" id="node-count"></p> <p class="text-muted" id="node-count"></p>
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@ var BOARD_HEIGHT = 8;
var PIECE_ROWS = 3; var PIECE_ROWS = 3;
var SEARCH_DEPTH = 4; var SEARCH_DEPTH = 4;
var PERFECT_CHANCE = 0.5;
const STATUS_TIMEOUT = 3000; const STATUS_TIMEOUT = 3000;
const WON_TIMEOUT = 3000; const WON_TIMEOUT = 3000;
@ -137,7 +138,7 @@ function process_canvas_click(cell_coord) {
switch(status) { switch(status) {
case Moveable.Allowed: case Moveable.Allowed:
if (aiCheckBox.checked) { if (aiCheckBox.checked && game.has_won() === undefined) {
game.ai_move(); game.ai_move();
nodeCountText.innerText = `searched ${game.last_node_count.toLocaleString("en-GB")} possible moves`; nodeCountText.innerText = `searched ${game.last_node_count.toLocaleString("en-GB")} possible moves`;
} }
@ -337,4 +338,16 @@ const onAICheck = () => {
} }
aiCheckBox.onchange = onAICheck; aiCheckBox.onchange = onAICheck;
// aiCheckBox.checked = true; // aiCheckBox.checked = true;
const aiPerfectChance = document.getElementById("ai_difficulty");
/**
* Handler for piece rows input box change, start a new game
*/
const onPerfectChance = () => {
PERFECT_CHANCE = parseInt(aiPerfectChance.value) / 100;
game.set_perfect_chance(PERFECT_CHANCE);
}
aiPerfectChance.onchange = onPerfectChance;
aiPerfectChance.value = 50;