AI perfect chance, rendering with proportions instead of absolute pixel values
This commit is contained in:
parent
ab0d0e5df3
commit
40d47f8042
106
src/comp/mod.rs
106
src/comp/mod.rs
@ -1,12 +1,15 @@
|
||||
//! AI player logic
|
||||
|
||||
use indextree::{Arena, Node, NodeId, NodeEdge};
|
||||
|
||||
use rand::prelude::*;
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
// use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::log;
|
||||
use crate::log_error;
|
||||
|
||||
use crate::board::{Board, BrdIdx};
|
||||
use crate::board::enums::{MoveType, Moveable, Team};
|
||||
@ -63,13 +66,15 @@ pub struct Computer {
|
||||
pub search_depth: usize,
|
||||
pub team: Team,
|
||||
pub last_node_count: usize,
|
||||
pub perfect_chance: f64,
|
||||
}
|
||||
|
||||
impl Computer {
|
||||
pub fn new(search_depth: usize, team: Team) -> Computer {
|
||||
pub fn new(search_depth: usize, team: Team, perfect_chance: f64) -> Computer {
|
||||
Computer {
|
||||
search_depth,
|
||||
team,
|
||||
perfect_chance,
|
||||
last_node_count: 0,
|
||||
}
|
||||
}
|
||||
@ -333,49 +338,88 @@ impl Computer {
|
||||
.expect("No node returned for node id")
|
||||
.get(); // get BoardNode from Node
|
||||
|
||||
// when boards have equal scores, store for shuffling and selection
|
||||
let mut equal_scores = Vec::with_capacity(10);
|
||||
// node ids of available next moves
|
||||
let possible_moves: Vec<NodeId> = root_node.children(&tree).collect();
|
||||
|
||||
if possible_moves.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
#[cfg(feature = "debug_logs")]
|
||||
{
|
||||
log!("Current root score: {}", root_board_node.score);
|
||||
let scores: Vec<NodeId> = root_node
|
||||
.children(&tree)
|
||||
.collect();
|
||||
let scores: Vec<isize> = scores
|
||||
.into_iter()
|
||||
.map(|n| tree.get(n).unwrap().get().score)
|
||||
let scores: Vec<isize> = possible_moves
|
||||
.iter()
|
||||
.map(|n| tree.get(*n).unwrap().get().score)
|
||||
.collect();
|
||||
log!("Next boards scores: {:?}", scores);
|
||||
}
|
||||
|
||||
// search through root node's children for the same score
|
||||
for n in root_node.children(&tree) {
|
||||
let mut rng = rand::thread_rng();
|
||||
// random number to compare against threshold
|
||||
let perfect_num: f64 = rng.gen();
|
||||
|
||||
// get each board
|
||||
let iter_board_node = tree
|
||||
.get(n) // get Node
|
||||
.expect("No node returned for node id")
|
||||
.get(); // get BoardNode from Node
|
||||
// make perfect move
|
||||
if perfect_num < self.perfect_chance {
|
||||
#[cfg(feature = "debug_logs")]
|
||||
log!("Making perfect move");
|
||||
|
||||
if root_board_node.score == iter_board_node.score {
|
||||
equal_scores.push(iter_board_node);
|
||||
// return Some(iter_board_node.board.clone());
|
||||
// get boards of equal score that are perfect for the given player
|
||||
let possible_perfect_moves: Vec<&BoardNode> = possible_moves
|
||||
.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 {
|
||||
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()
|
||||
)
|
||||
Some(Computer::random_choice(&tree, possible_moves, &mut rng))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
|
||||
|
||||
use crate::board::Square;
|
||||
use crate::board::enums::Strength::*;
|
||||
use crate::log;
|
||||
// use crate::log;
|
||||
|
||||
// use Team::*;
|
||||
|
||||
@ -25,7 +25,7 @@ fn available_moves() {
|
||||
// _ . _
|
||||
|
||||
let mut brd = Board::new(3, 2, White);
|
||||
let comp = Computer::new(3, White);
|
||||
let comp = Computer::new(3, White, 0.5);
|
||||
|
||||
// log!("{}", brd);
|
||||
|
||||
@ -50,7 +50,7 @@ fn available_moves_jumps() {
|
||||
// _ . _ .
|
||||
|
||||
let mut brd = Board::new(4, 4, White);
|
||||
let comp = Computer::new(3, White);
|
||||
let comp = Computer::new(3, White, 0.5);
|
||||
|
||||
// log!("{}", brd);
|
||||
|
||||
@ -71,7 +71,7 @@ fn available_moves_jumps() {
|
||||
#[wasm_bindgen_test]
|
||||
fn available_moves_std_brd() {
|
||||
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);
|
||||
|
||||
@ -87,7 +87,7 @@ fn available_moves_std_brd() {
|
||||
#[wasm_bindgen_test]
|
||||
fn expand_node() {
|
||||
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);
|
||||
|
||||
@ -106,7 +106,7 @@ fn expand_node() {
|
||||
#[wasm_bindgen_test]
|
||||
fn expand_layer() {
|
||||
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);
|
||||
|
||||
@ -123,7 +123,7 @@ fn expand_layer() {
|
||||
#[wasm_bindgen_test]
|
||||
fn leaf_nodes() {
|
||||
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 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(2, 1)), 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);
|
||||
|
||||
@ -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(2, 1)), 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);
|
||||
|
||||
|
@ -29,6 +29,7 @@ pub struct Game {
|
||||
painter: Option<Painter>,
|
||||
search_depth: usize,
|
||||
pub last_node_count: usize,
|
||||
pub perfect_chance: f64,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@ -95,6 +96,7 @@ impl Game {
|
||||
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) {
|
||||
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
|
||||
pub fn clear_selected(&mut self) {
|
||||
self.selected_piece = None;
|
||||
@ -180,6 +187,7 @@ impl Game {
|
||||
painter: None,
|
||||
search_depth,
|
||||
last_node_count: 0,
|
||||
perfect_chance: 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,6 +204,7 @@ impl Game {
|
||||
),
|
||||
search_depth,
|
||||
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
|
||||
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());
|
||||
|
||||
self.last_node_count = comp.last_node_count;
|
||||
|
||||
match new_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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// use crate::board::{Piece};
|
||||
use crate::board::enums::Strength::*;
|
||||
use crate::board::enums::Team::*;
|
||||
// use crate::board::enums::Team::*;
|
||||
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
@ -29,6 +29,12 @@ macro_rules! log {
|
||||
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]
|
||||
pub fn init_wasm() {
|
||||
|
35
src/paint.rs
35
src/paint.rs
@ -23,8 +23,8 @@ const BLACK_SQUARE: &str = "#000000";
|
||||
|
||||
/// Default hex colour value for outline of black squares
|
||||
const SQUARE_OUTLINE: &str = "#9c9c9c";
|
||||
/// Line width when outlining black squares
|
||||
const OUTLINE_WIDTH: f64 = 3.0;
|
||||
/// Line width when outlining black squares as proportion of min cell dimension
|
||||
const OUTLINE_WIDTH: f64 = 0.05;
|
||||
/// Whether to outline black squares
|
||||
const DRAW_OUTLINE: bool = true;
|
||||
|
||||
@ -46,10 +46,10 @@ const SELECTED_PIECE_OUTLINE: &str = "#d1cf45";
|
||||
const KING_OUTLINE: &str = "#ffea00";
|
||||
/// Whether to outline pieces
|
||||
const DRAW_PIECE_OUTLINES: bool = true;
|
||||
/// Line width for outlining pieces
|
||||
const PIECE_OUTLINE_WIDTH: f64 = 6.0;
|
||||
/// Margin from square to define piece radius
|
||||
const PIECE_MARGIN: f64 = 14.0;
|
||||
/// Line width for outlining pieces as proportion of piece radius
|
||||
const PIECE_OUTLINE_PROPORTION: f64 = 0.25;
|
||||
/// Proportion of square that piece fills as proportion of min cell dimension
|
||||
const PIECE_PROPORTION: f64 = 0.6;
|
||||
|
||||
/// Used to paint boards onto HTML canvases
|
||||
#[wasm_bindgen]
|
||||
@ -72,7 +72,7 @@ pub struct Painter {
|
||||
king_line: JsValue,
|
||||
|
||||
piece_lines: bool,
|
||||
piece_line_width: f64,
|
||||
piece_line_proportion: f64,
|
||||
|
||||
square_outline: JsValue,
|
||||
outline_width: f64,
|
||||
@ -168,7 +168,7 @@ impl Painter {
|
||||
selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE),
|
||||
king_line: JsValue::from_str(KING_OUTLINE),
|
||||
piece_lines: DRAW_PIECE_OUTLINES,
|
||||
piece_line_width: PIECE_OUTLINE_WIDTH,
|
||||
piece_line_proportion: PIECE_OUTLINE_PROPORTION,
|
||||
|
||||
square_outline: JsValue::from_str(SQUARE_OUTLINE),
|
||||
outline_width: OUTLINE_WIDTH,
|
||||
@ -201,7 +201,7 @@ impl Painter {
|
||||
selected_piece_line: JsValue::from_str(SELECTED_PIECE_OUTLINE),
|
||||
king_line: JsValue::from_str(KING_OUTLINE),
|
||||
piece_lines: DRAW_PIECE_OUTLINES,
|
||||
piece_line_width: PIECE_OUTLINE_WIDTH,
|
||||
piece_line_proportion: PIECE_OUTLINE_PROPORTION,
|
||||
|
||||
square_outline: JsValue::from_str(SQUARE_OUTLINE),
|
||||
outline_width: OUTLINE_WIDTH,
|
||||
@ -254,13 +254,20 @@ impl Painter {
|
||||
|
||||
let cell_height = self.height as usize / board.height;
|
||||
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.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_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
|
||||
for i in 0..board.height {
|
||||
@ -336,7 +343,7 @@ impl Painter {
|
||||
match self.context.arc(
|
||||
center_x,
|
||||
center_y,
|
||||
(cell_width as f64 / 2.0) - PIECE_MARGIN, // radius
|
||||
cell_radius, // radius
|
||||
0.0, // start angle
|
||||
f64::consts::PI * 2.0) // end angle
|
||||
{
|
||||
@ -346,7 +353,7 @@ impl Painter {
|
||||
self.context.fill();
|
||||
|
||||
if self.piece_lines {
|
||||
self.context.set_line_width(self.piece_line_width);
|
||||
self.context.set_line_width(piece_outline);
|
||||
self.context.stroke()
|
||||
}
|
||||
|
||||
@ -366,7 +373,7 @@ impl Painter {
|
||||
match self.context.arc(
|
||||
center_x,
|
||||
center_y,
|
||||
(cell_width as f64 / 2.0) - PIECE_MARGIN, // radius
|
||||
cell_radius, // radius
|
||||
0.0, // start angle
|
||||
f64::consts::PI * 2.0) // end angle
|
||||
{
|
||||
@ -376,7 +383,7 @@ impl Painter {
|
||||
self.context.fill();
|
||||
|
||||
if self.piece_lines {
|
||||
self.context.set_line_width(self.piece_line_width);
|
||||
self.context.set_line_width(piece_outline);
|
||||
self.context.stroke()
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
type="checkbox"
|
||||
value=""
|
||||
@ -129,15 +129,19 @@
|
||||
AI Player
|
||||
</label>
|
||||
</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"
|
||||
id="ai_search_depth"
|
||||
name="ai_search_depth"
|
||||
min="1" max="10" value="4"
|
||||
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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
17
www/index.js
17
www/index.js
@ -13,6 +13,7 @@ var BOARD_HEIGHT = 8;
|
||||
|
||||
var PIECE_ROWS = 3;
|
||||
var SEARCH_DEPTH = 4;
|
||||
var PERFECT_CHANCE = 0.5;
|
||||
|
||||
const STATUS_TIMEOUT = 3000;
|
||||
const WON_TIMEOUT = 3000;
|
||||
@ -137,7 +138,7 @@ function process_canvas_click(cell_coord) {
|
||||
switch(status) {
|
||||
case Moveable.Allowed:
|
||||
|
||||
if (aiCheckBox.checked) {
|
||||
if (aiCheckBox.checked && game.has_won() === undefined) {
|
||||
game.ai_move();
|
||||
nodeCountText.innerText = `searched ${game.last_node_count.toLocaleString("en-GB")} possible moves`;
|
||||
}
|
||||
@ -337,4 +338,16 @@ const 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;
|
Loading…
Reference in New Issue
Block a user