working AI, dynamic canvas size, UI input

This commit is contained in:
andy 2021-07-12 23:46:07 +01:00
parent 6d2eb8ab0a
commit d922c0f345
10 changed files with 699 additions and 93 deletions

View File

@ -3,7 +3,7 @@ name = "draught"
version = "0.1.0" version = "0.1.0"
authors = ["aj <andrewjpack@gmail.com>"] authors = ["aj <andrewjpack@gmail.com>"]
edition = "2018" edition = "2018"
repository = "https://github.com/Sarsoo/checkers" repository = "https://github.com/Sarsoo/draught"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
@ -15,6 +15,12 @@ default = ["console_error_panic_hook"]
wasm-bindgen = "0.2.74" wasm-bindgen = "0.2.74"
indextree = "4.3.1" indextree = "4.3.1"
rand = {version = "0.7", features = ["wasm-bindgen"]}
# rand = {version = "0.8"}
# getrandom = { version = "0.2", features = ["js"] }
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
@ -47,6 +53,6 @@ wasm-bindgen-test = "0.3.24"
# Tell `rustc` to optimize for small code size. # Tell `rustc` to optimize for small code size.
opt-level = "s" opt-level = "s"
[package.metadata.wasm-pack.profile.release] # [package.metadata.wasm-pack.profile.release]
wasm-opt = false # wasm-opt = false

View File

@ -677,6 +677,9 @@ impl Board {
Board::check_kinged(&mut new, to); Board::check_kinged(&mut new, to);
// board has been changed, update player turn
new.current_turn = new.current_turn.opponent();
new new
} }
@ -707,6 +710,9 @@ impl Board {
Board::check_kinged(&mut new, to); Board::check_kinged(&mut new, to);
// board has been changed, update player turn
new.current_turn = new.current_turn.opponent();
new new
} }

View File

@ -1,17 +1,18 @@
//! AI player logic //! AI player logic
use indextree::{Arena, NodeId}; use indextree::{Arena, Node, NodeId, NodeEdge};
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::board::{Board, BrdIdx}; use crate::board::{Board, BrdIdx};
use crate::board::enums::{MoveType, Moveable, Team}; use crate::board::enums::{MoveType, Moveable, Team};
use crate::board::iter::{PieceIterator}; use crate::board::iter::{PieceIterator};
// use Team::*; use Team::*;
// use Strength::*; // use Strength::*;
// use SquareState::*; // use SquareState::*;
@ -34,27 +35,45 @@ impl Move {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoardNode {
pub board: Board,
pub score: isize,
}
impl BoardNode {
pub fn new(board: Board, score: isize) -> BoardNode {
BoardNode {
board, score
}
}
pub fn brd(board: Board) -> BoardNode {
BoardNode {
board, score: 0
}
}
}
/// Root-level structure for managing the game as a collection of board states /// Root-level structure for managing the game as a collection of board states
#[derive(Debug)] #[derive(Debug)]
pub struct Computer { pub struct Computer {
pub root_node_id: NodeId,
pub search_depth: usize, pub search_depth: usize,
pub team: Team, pub team: Team,
pub last_node_count: usize,
} }
impl Computer { impl Computer {
pub fn new(initial_board: Board, search_depth: usize, team: Team) -> Computer { pub fn new(search_depth: usize, team: Team) -> Computer {
let mut tree = Arena::new();
let root_node_id = tree.new_node(initial_board);
Computer { Computer {
root_node_id,
search_depth, search_depth,
team team,
last_node_count: 0,
} }
} }
/// Get vector of available moves for a given board /// Get vector of available moves for a given board
pub fn available_turns(&self, board: &Board) -> Vec<Move> { fn available_turns(&self, board: &Board) -> Vec<Move> {
// allocate capacity for 2 moves per piece, likely too much but will be shrunk // allocate capacity for 2 moves per piece, likely too much but will be shrunk
// to reduce memory re-allocations // to reduce memory re-allocations
@ -109,43 +128,250 @@ impl Computer {
moves moves
} }
pub fn gen_tree(&mut self, tree: &mut Arena<Board>, board: Board) { /// Generate tree of boards to given search depth, return root node
fn gen_tree(&mut self, tree: &mut Arena<BoardNode>, board: Board) -> NodeId {
// possible boards from given
let boards = self.get_move_boards(&board);
// root node of tree // root node of tree
let root = tree.new_node(board); let root = tree.new_node(BoardNode::brd(board));
// insert possible boards let mut nodes = vec!(root);
let ids = self.insert_boards(tree, boards);
// append ids to root node
ids.iter().for_each( |id| root.append(*id, tree) );
for _ in 0..self.search_depth {
if nodes.len() == 0 {
break;
}
nodes = self.expand_layer(tree, nodes);
}
root
} }
pub fn insert_boards(&mut self, tree: &mut Arena<Board>, boards: Vec<Board>) -> Vec<NodeId> { /// Propagate scores up the tree employing MiniMax algorithm
fn propagate_scores(&mut self, tree: Arena<BoardNode>, root: NodeId) -> Arena<BoardNode> {
boards // need to clone tree because we iterate over it and edit it at the same time
.into_iter().map( let mut new_tree = tree.clone();
|b| tree.new_node(b)
).collect()
}
pub fn get_move_boards(&self, board: &Board) -> Vec<Board> { for n in root.traverse(&tree) {
// only check end variant, checks nodes once
if let NodeEdge::End(node_id) = n {
self.available_turns(board) // board current being looked at
.into_iter().map( let board_node = tree
|m| { .get(node_id) // get Node
match m.mv_type { .expect("No node returned for node id")
MoveType::Move => board.apply_move(m.from, m.to), .get(); // get BoardNode from Node
MoveType::Jump => board.apply_jump(m.from, m.to),
// get scores of each nodes children
let children_scores: Vec<isize> = node_id // current node
.children(&tree)
.into_iter()
.map(
|n| tree
.get(n) // get Node
.expect("No node returned for node id") // unwrap, should always be fine
.get() // get BoardNode from Node
.score // get score from BoardNode
)
.collect(); // finalise
// only nodes which have children, ignores leaves
if children_scores.len() != 0 {
// calculate score to propagate up tree
new_tree
.get_mut(node_id) // get mutable Node to change score on
.expect("No node returned for node id") // should always be fine
.get_mut() // get mutable BoardNode from Node
.score = Computer::best_score(&board_node.board, children_scores);
} }
} }
).collect() }
new_tree
} }
pub fn get_move(&self) { fn best_score(board: &Board, children_scores: Vec<isize>) -> isize {
match board.current_turn { // MiniMax algorithm here
// whether maximised or minimsed is based on current player
White => children_scores
.into_iter()
.min()
.expect("Couldn't unwrap min score value"),
Black => children_scores
.into_iter()
.max()
.expect("Couldn't unwrap max score value"),
}
}
/// For given NodeIDs, insert score of board into tree
/// Used for leaf nodes ready for propagating up tree
fn insert_board_scores(&mut self, tree: &mut Arena<BoardNode>, nodes: Vec<NodeId>) {
for i in nodes.into_iter() {
match tree.get_mut(i) {
Some(node) => {
let board_node = node.get_mut();
board_node.score = board_node.board.score();
},
None => {
panic!("Failed to get node when inserting board scores");
}
}
}
}
/// Traverse tree from given root ID and get NodeIDs of leaf nodes (those without children)
fn get_leaf_nodes(&mut self, tree: &mut Arena<BoardNode>, root: NodeId) -> Vec<NodeId> {
let mut leaves = Vec::with_capacity(self.search_depth * 30);
for n in root.traverse(tree) {
// only check start variant, checks nodes once
if let NodeEdge::Start(node_id) = n {
let children: Vec<NodeId> = node_id.children(tree).collect();
if children.len() == 0 {
leaves.push(node_id);
}
}
}
leaves
}
/// Expand all given nodes and return newly inserted layer of nodes
fn expand_layer(&mut self, tree: &mut Arena<BoardNode>, nodes: Vec<NodeId>) -> Vec<NodeId> {
nodes
.into_iter().map(
|n| self.expand_node(tree, n)
).flatten()
.collect()
}
/// Insert all possible next boards as children of given board node
fn expand_node(&mut self, tree: &mut Arena<BoardNode>, node: NodeId) -> Vec<NodeId> {
let node_board: Option<&Node<BoardNode>> = tree.get(node);
match node_board {
Some(node_ref) => {
let board: &BoardNode = &*node_ref.get();
// possible boards from given
let boards = self.get_move_boards(board);
// insert possible boards
let ids = self.insert_boards(tree, boards);
// append ids to root node
ids.iter().for_each( |id| node.append(*id, tree) );
return ids;
},
None => {
panic!("No board retrieved from tree, node: {:?}", node);
},
}
}
/// Insert given boards into tree
fn insert_boards(&mut self, tree: &mut Arena<BoardNode>, boards: Vec<BoardNode>) -> Vec<NodeId> {
boards
.into_iter().map(
|b| tree.new_node(b)
).collect()
}
/// Get all possible next boards from given board
fn get_move_boards(&self, board: &BoardNode) -> Vec<BoardNode> {
self.available_turns(&board.board)
.into_iter().map(
|m| {
match m.mv_type {
MoveType::Move => BoardNode::brd(
board.board.apply_move(m.from, m.to)
),
MoveType::Jump => BoardNode::brd(
board.board.apply_jump(m.from, m.to)
),
}
}
).collect()
}
pub fn get_move(&mut self, brd: Board) -> Option<Board> {
let mut tree = Arena::new();
// generate a tree to given depth for the given board
let root_node = self.gen_tree(&mut tree, brd);
self.last_node_count = tree.count() - 1;
// get the leaf nodes to insert the board scores with
let lowest_nodes = self.get_leaf_nodes(&mut tree, root_node);
// insert the board scores for the leaf nodes
self.insert_board_scores(&mut tree, lowest_nodes);
// propagate the scores up the tree, the root node has the best score
let tree = self.propagate_scores(tree, root_node);
// get root node to compare
let root_board_node = tree
.get(root_node) // get Node
.expect("No node returned for node id")
.get(); // get BoardNode from Node
log!("{}", root_board_node.score);
// log!("{:#?}", tree);
// when boards have equal scores, store for shuffling and selection
let mut equal_scores = Vec::with_capacity(10);
// DEBUG
let scores: Vec<NodeId> = root_node
.children(&tree)
.collect();
let scores: Vec<isize> = scores
.into_iter()
.map(|n| tree.get(n).unwrap().get().score)
.collect();
log!("SCORES: {:?}", scores);
// DEBUG
// search through root node's children for the same score
for n in root_node.children(&tree) {
// get each board
let iter_board_node = tree
.get(n) // get Node
.expect("No node returned for node id")
.get(); // get BoardNode from Node
if root_board_node.score == iter_board_node.score {
equal_scores.push(iter_board_node);
// return Some(iter_board_node.board.clone());
}
}
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()
)
}
} }
} }

View File

@ -1,11 +1,12 @@
use super::*; use super::*;
// use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*; 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::*;
wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test_configure!(run_in_browser);
@ -23,18 +24,17 @@ fn available_moves() {
// . W . // . W .
// _ . _ // _ . _
let brd = Board::new(3, 2, White); let mut brd = Board::new(3, 2, White);
let mut brd2 = brd.clone(); let comp = Computer::new(3, White);
let comp = Computer::new(brd, 3, White);
// log!("{}", brd2); // log!("{}", brd);
// can move left and right from central square // can move left and right from central square
brd2.set_cell(brd2.cell_index(0, 1), Square::pc(White, Man)); brd.set_cell(brd.cell_index(0, 1), Square::pc(White, Man));
// log!("{}", brd2); // log!("{}", brd);
let moves = comp.available_turns(&brd2); let moves = comp.available_turns(&brd);
// log!("{:?}", moves); // log!("{:?}", moves);
@ -49,18 +49,17 @@ fn available_moves_jumps() {
// . _ . _ // . _ . _
// _ . _ . // _ . _ .
let brd = Board::new(4, 4, White); let mut brd = Board::new(4, 4, White);
let mut brd2 = brd.clone(); let comp = Computer::new(3, White);
let comp = Computer::new(brd, 3, White);
// log!("{}", brd2); // log!("{}", brd);
brd2.set_cell(brd2.cell_index(0, 1), Square::pc(White, Man)); brd.set_cell(brd.cell_index(0, 1), Square::pc(White, Man));
brd2.set_cell(brd2.cell_index(1, 2), Square::pc(Black, Man)); brd.set_cell(brd.cell_index(1, 2), Square::pc(Black, Man));
// log!("{}", brd2); // log!("{}", brd);
let moves = comp.available_turns(&brd2); let moves = comp.available_turns(&brd);
// log!("{:?}", moves); // log!("{:?}", moves);
@ -72,12 +71,11 @@ 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 brd2 = brd.clone(); let comp = Computer::new(3, White);
let comp = Computer::new(brd, 3, White);
// log!("{}", brd2); // log!("{}", brd);
let moves = comp.available_turns(&brd2); let moves = comp.available_turns(&brd);
// log!("{:?}", moves); // log!("{:?}", moves);
@ -87,19 +85,204 @@ fn available_moves_std_brd() {
} }
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn tree_insert() { 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 brd2 = brd.clone(); let mut comp = Computer::new(3, White);
let mut comp = Computer::new(brd, 3, White);
// log!("{}", brd2); // log!("{}", brd);
let moves = comp.available_turns(&brd2);
log!("{}", moves.len());
let mut tree = Arena::new(); let mut tree = Arena::new();
comp.gen_tree(&mut tree, brd2); let id = tree.new_node(BoardNode::brd(brd));
log!("{}", tree.count()); let moves = comp.expand_node(&mut tree, id);
assert_eq!(moves.len(), 7);
assert_eq!(tree.count(), 8);
let children: Vec<NodeId> = id.children(&mut tree).collect();
assert_eq!(children.len(), 7);
} }
#[wasm_bindgen_test]
fn expand_layer() {
let brd = Board::init_game(Board::new(8, 8, White), 3);
let mut comp = Computer::new(3, White);
// log!("{}", brd);
let mut tree = Arena::new();
let id = tree.new_node(BoardNode::brd(brd));
let moves = comp.expand_layer(&mut tree, vec!(id));
let moves = comp.expand_layer(&mut tree, moves);
assert_eq!(moves.len(), 49);
assert_eq!(tree.count(), 49 + 7 + 1);
}
#[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 tree = Arena::new();
let id = tree.new_node(BoardNode::brd(brd));
let moves = comp.expand_node(&mut tree, id);
let children: Vec<NodeId> = id.children(&mut tree).collect();
let moves2 = comp.expand_node(&mut tree, children[0]);
assert_eq!(comp.get_leaf_nodes(&mut tree, id).len(), moves.len() + moves2.len() - 1);
}
#[wasm_bindgen_test]
fn best_scores() {
let brd = Board::new(1, 1, White);
assert_eq!(Computer::best_score(&brd, vec!(-5, -1, 2, 3, 4)), -5);
let brd = Board::new(1, 1, Black);
assert_eq!(Computer::best_score(&brd, vec!(-1, 2, 3, 4)), 4);
}
#[wasm_bindgen_test]
fn propagate_scores() {
let brd = Board::init_game(Board::new(8, 8, White), 3);
let mut comp = Computer::new(3, White);
// log!("{}", brd);
let mut tree = Arena::new();
let root = tree.new_node(BoardNode::brd(brd));
comp.expand_layer(&mut tree, vec!(root));
let moves = comp.propagate_scores(tree, root);
// log!("{}", moves.len());
// log!("{}", tree.count());
}
// #[wasm_bindgen_test]
// fn tree_2_depth() {
// // log!("{}", performance.timing().request_start());
// let iter = 3;
// let mut times = Vec::with_capacity(iter);
// for _ in 0..iter {
// times.push(time_tree_gen(6));
// }
// log!("{:?}", times);
// }
// fn time_tree_gen(depth: usize) {
// web_sys::console::time_with_label("tree_timer");
// let mut comp = Computer::new(depth, White);
// let mut tree = Arena::new();
// let brd = Board::init_game(Board::new(8, 8, White), 3);
// comp.gen_tree(&mut tree, brd);
// web_sys::console::time_end_with_label("tree_timer");
// log!("{}", tree.count());
// }
// #[wasm_bindgen_test]
// fn tree_last_nodes() {
// let mut brd = Board::new(2, 2, White);
// brd.set_cell(1, Square::pc(White, King));
// let mut comp = Computer::new(3, White);
// // log!("{}", brd);
// // log!("{}", brd.score());
// // let moves = comp.available_turns(&brd);
// // log!("{}", moves.len());
// let mut tree = Arena::new();
// let root_node = comp.gen_tree(&mut tree, brd);
// let lowest_nodes = comp.get_leaf_nodes(&mut tree, root_node);
// // log!("{:#?}", lowest_nodes);
// comp.insert_board_scores(&mut tree, lowest_nodes);
// // log!("{:#?}", tree);
// // log!("{}", tree.count());
// }
// #[wasm_bindgen_test]
// fn tree_score_propagation() {
// let mut brd = Board::new(4, 4, White);
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 2)), Square::pc(White, Man));
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 0)), Square::pc(White, Man));
// brd.set_cell(brd.cell_idx(BrdIdx::from(2, 1)), Square::pc(Black, Man));
// let mut comp = Computer::new(1, White);
// // log!("{}", brd);
// // log!("{}", brd.score());
// // let moves = comp.available_turns(&brd);
// // log!("{}", moves.len());
// let mut tree = Arena::new();
// let root_node = comp.gen_tree(&mut tree, brd);
// // log!("{}", root_node);
// let lowest_nodes = comp.get_leaf_nodes(&mut tree, root_node);
// // log!("{:#?}", lowest_nodes);
// comp.insert_board_scores(&mut tree, lowest_nodes);
// let tree = comp.propagate_scores(tree, root_node);
// let scores: Vec<NodeId> = root_node
// .children(&tree)
// .collect();
// let scores: Vec<isize> = scores.into_iter().map(|n| tree.get(n).unwrap().get().score).collect();
// // log!("SCORES: {:?}", scores);
// // log!("{:#?}", tree);
// // log!("{}", tree.count());
// }
// #[wasm_bindgen_test]
// fn tree_get_move() {
// let mut brd = Board::new(5, 4, White);
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 2)), Square::pc(White, Man));
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 0)), 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, 3)), Square::pc(Black, Man));
// let mut comp = Computer::new(2, White);
// // log!("{}", brd);
// // log!("{:?}", comp.get_move(brd).unwrap());
// }
// #[wasm_bindgen_test]
// fn tree_get_move() {
// let mut brd = Board::new(5, 5, White);
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 2)), Square::pc(White, Man));
// brd.set_cell(brd.cell_idx(BrdIdx::from(1, 0)), 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(4, 1)), Square::pc(Black, Man));
// let mut comp = Computer::new(2, White);
// log!("{}", brd);
// let next = comp.get_move(brd).unwrap();
// log!("{}", next);
// let mut comp = Computer::new(2, White);
// let next = comp.get_move(next);
// // log!("{}", next);
// }

View File

@ -10,6 +10,7 @@ use crate::log;
use crate::board::{Square, BrdIdx}; use crate::board::{Square, BrdIdx};
use crate::board::enums::{SquareState, Moveable, Team}; use crate::board::enums::{SquareState, Moveable, Team};
use crate::paint::Painter; use crate::paint::Painter;
use crate::comp::Computer;
// use Team::*; // use Team::*;
use SquareState::*; use SquareState::*;
@ -25,7 +26,9 @@ pub struct Game {
current: Board, current: Board,
selected_piece: Option<BrdIdx>, selected_piece: Option<BrdIdx>,
previous_boards: Vec<Board>, previous_boards: Vec<Board>,
painter: Option<Painter> painter: Option<Painter>,
search_depth: usize,
pub last_node_count: usize,
} }
impl Game { impl Game {
@ -57,11 +60,20 @@ impl Game {
self.current.current_turn self.current.current_turn
} }
/// Current board's score
pub fn score(&self) -> isize {
self.current.score()
}
/// Get square on current board for given index /// Get square on current board for given index
pub fn current_cell_state(&self, idx: &BrdIdx) -> Square { pub fn current_cell_state(&self, idx: &BrdIdx) -> Square {
self.current.cell(self.current.cell_idx(*idx)) self.current.cell(self.current.cell_idx(*idx))
} }
pub fn set_search_depth(&mut self, search_depth: usize) {
self.search_depth = search_depth;
}
/// Set given index as selected piece /// Set given index as selected piece
/// TODO: Check whether valid square? /// TODO: Check whether valid square?
pub fn set_selected(&mut self, idx: &BrdIdx) { pub fn set_selected(&mut self, idx: &BrdIdx) {
@ -101,9 +113,6 @@ impl Game {
self.execute_jump(from, to); self.execute_jump(from, to);
} }
// board has been changed, update player turn
self.current.current_turn = self.current.current_turn.opponent();
} else { } else {
log!("Unable to make move, {:?}", able); log!("Unable to make move, {:?}", able);
} }
@ -135,7 +144,7 @@ impl Game {
} }
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team) -> Game { pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team, search_depth: usize) -> Game {
Game { Game {
current: Board::init_game( current: Board::init_game(
Board::new(width, height, first_turn), piece_rows, Board::new(width, height, first_turn), piece_rows,
@ -143,10 +152,12 @@ impl Game {
selected_piece: None, selected_piece: None,
previous_boards: Vec::with_capacity(10), previous_boards: Vec::with_capacity(10),
painter: None, painter: None,
search_depth,
last_node_count: 0,
} }
} }
pub fn new_with_canvas(width: usize, height: usize, piece_rows: usize, first_turn: Team, canvas_id: &str, canvas_width: u32, canvas_height: u32) -> Game { pub fn new_with_canvas(width: usize, height: usize, piece_rows: usize, first_turn: Team, search_depth: usize, canvas_id: &str, canvas_width: u32, canvas_height: u32) -> Game {
Game { Game {
current: Board::init_game( current: Board::init_game(
Board::new(width, height, first_turn), piece_rows, Board::new(width, height, first_turn), piece_rows,
@ -156,6 +167,8 @@ impl Game {
painter: Some( painter: Some(
Painter::new(canvas_width, canvas_height, canvas_id) Painter::new(canvas_width, canvas_height, canvas_id)
), ),
search_depth,
last_node_count: 0,
} }
} }
@ -169,6 +182,19 @@ impl Game {
None => log!("No painter to draw board with") None => log!("No painter to draw board with")
} }
} }
pub fn ai_move(&mut self) {
let mut comp = Computer::new(self.search_depth, self.current.current_turn);
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"),
}
}
} }
impl Display for Game { impl Display for Game {

View File

@ -11,7 +11,7 @@ use crate::board::enums::Team::*;
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn make_move() { fn make_move() {
let mut game = Game::new(8, 8, 3, Black); let mut game = Game::new(8, 8, 3, Black, 3);
// log!("{}", game); // log!("{}", game);
// log!("{:?}", game); // log!("{:?}", game);
@ -39,7 +39,7 @@ fn make_move() {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn make_jump() { fn make_jump() {
let mut game = Game::new(8, 8, 3, Black); let mut game = Game::new(8, 8, 3, Black, 3);
// log!("{}", game); // log!("{}", game);
// log!("{:?}", game); // log!("{:?}", game);

View File

@ -19,9 +19,9 @@ pub use paint::Painter;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator. // allocator.
// #[cfg(feature = "wee_alloc")] #[cfg(feature = "wee_alloc")]
// #[global_allocator] #[global_allocator]
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
/// Wrap the [`web_sys`] access to the browser console in a macro for easy logging /// Wrap the [`web_sys`] access to the browser console in a macro for easy logging
#[macro_export] #[macro_export]

View File

@ -47,10 +47,9 @@ 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
const PIECE_OUTLINE_WIDTH: f64 = 3.0; const PIECE_OUTLINE_WIDTH: f64 = 6.0;
/// Margin from square to define piece radius /// Margin from square to define piece radius
const PIECE_MARGIN: f64 = 10.0; const PIECE_MARGIN: f64 = 14.0;
/// Used to paint boards onto HTML canvases /// Used to paint boards onto HTML canvases
#[wasm_bindgen] #[wasm_bindgen]

View File

@ -25,6 +25,46 @@
h1 { h1 {
font-family: monospace; font-family: monospace;
} }
#game-canvas {
width: 1000px;
height: 1000px;
}
@media only screen and (max-width: 1000px) {
#game-canvas {
width: 700px;
height: 700px;
}
}
@media only screen and (max-width: 800px) {
#game-canvas {
width: 600px;
height: 600px;
}
}
@media only screen and (max-width: 600px) {
#game-canvas {
width: 400px;
height: 400px;
}
}
@media only screen and (max-width: 400px) {
#game-canvas {
width: 300px;
height: 300px;
}
}
@media only screen and (max-width: 300px) {
#game-canvas {
width: 200px;
height: 200px;
}
}
</style> </style>
</head> </head>
@ -42,10 +82,55 @@
<div class="row p-3"> <div class="row p-3">
<div class="col-sm-12"> <div class="col-sm-12">
<a href="doc/draught" class="btn btn-secondary" target="_blank">Docs</a> <a href="doc/draught" class="btn btn-secondary" target="_blank">Docs</a>
<button id="startBtn" class="btn btn-primary">Start</button> <button id="startBtn" class="btn btn-success">Start</button>
</div> </div>
</div> </div>
<div class="row p-3">
<div class="col-sm-4">
<input type="number"
id="width"
name="width"
min="3" max="40" value="8"
class="form-control">
<label for="width">width</label>
</div>
<div class="col-sm-4">
<input type="number"
id="height"
name="height"
min="3" max="40" value="8"
class="form-control">
<label for="height">height</label>
</div>
<div class="col-sm-4">
<input type="number"
id="play_rows"
name="play_rows"
min="1" max="3" value="3"
class="form-control">
<label for="play_rows">playable rows</label>
</div>
</div>
<div class="row p-3">
<div class="col-sm-4">
<input class="form-check-input" type="checkbox" value="" id="ai-checkbox" checked="checked">
<label class="form-check-label" for="ai-checkbox">
AI Player
</label>
</div>
<div class="col-sm-4">
<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</label>
</div>
<div class="col-sm-4">
<p class="text-muted" id="node-count"></p>
</div>
</div>
<div class="row p-3"> <div class="row p-3">
<div class="col-sm-12"> <div class="col-sm-12">
<h1 id="team-p"></h1> <h1 id="team-p"></h1>

View File

@ -5,13 +5,14 @@ import { Game, Board, BrdIdx, Painter, Team, init_wasm, Moveable, SquareState, S
// CONSTS // CONSTS
/////////////////// ///////////////////
const CANVAS_WIDTH = 480; const CANVAS_WIDTH = 720;
const CANVAS_HEIGHT = 480; const CANVAS_HEIGHT = 720;
const BOARD_WIDTH = 8; var BOARD_WIDTH = 8;
const BOARD_HEIGHT = 8; var BOARD_HEIGHT = 8;
const PIECE_ROWS = 3; var PIECE_ROWS = 3;
var SEARCH_DEPTH = 4;
const STATUS_TIMEOUT = 3000; const STATUS_TIMEOUT = 3000;
@ -34,6 +35,7 @@ init_wasm();
const statusText = document.getElementById("status-p"); const statusText = document.getElementById("status-p");
const statusAlert = document.getElementById("status-d"); const statusAlert = document.getElementById("status-d");
const teamText = document.getElementById("team-p"); const teamText = document.getElementById("team-p");
const nodeCountText = document.getElementById("node-count");
const startBtn = document.getElementById("startBtn"); const startBtn = document.getElementById("startBtn");
startBtn.onclick = start_game; startBtn.onclick = start_game;
@ -59,24 +61,25 @@ canvas.addEventListener("click", (event) => {
var mousepos = getMousePos(canvas, event); var mousepos = getMousePos(canvas, event);
// console.log(mousepos); // console.log(mousepos);
var cell = new BrdIdx( var cell = new BrdIdx(
Math.floor((mousepos.y / CANVAS_HEIGHT) * BOARD_HEIGHT), Math.floor((mousepos.y / canvas.clientHeight) * BOARD_HEIGHT),
Math.floor((mousepos.x / CANVAS_WIDTH) * BOARD_WIDTH), Math.floor((mousepos.x / canvas.clientWidth) * BOARD_WIDTH),
); );
// console.log(cell); // console.log(cell);
process_canvas_click(cell); process_canvas_click(cell);
}) });
//////////////// ////////////////
// FUNCS // FUNCS
//////////////// ////////////////
function start_game() { function start_game() {
game = new Game(BOARD_WIDTH, BOARD_HEIGHT, PIECE_ROWS, Team.Black); game = new Game(BOARD_WIDTH, BOARD_HEIGHT, PIECE_ROWS, Team.Black, SEARCH_DEPTH);
painter = new Painter(CANVAS_WIDTH, CANVAS_HEIGHT, "game-canvas"); painter = new Painter(CANVAS_WIDTH, CANVAS_HEIGHT, "game-canvas");
game.set_painter(painter); game.set_painter(painter);
game.draw(); game.draw();
updateTeamText(); updateTeamText();
clicks = [];
current_state = GameState.HUMAN_TURN.THINKING; current_state = GameState.HUMAN_TURN.THINKING;
} }
@ -128,6 +131,14 @@ function process_canvas_click(cell_coord) {
switch(status) { switch(status) {
case Moveable.Allowed: case Moveable.Allowed:
console.log(`Score after your turn: ${game.score()}`);
if (aiCheckBox.checked) {
game.ai_move();
nodeCountText.innerText = `searched ${game.last_node_count.toLocaleString("en-GB")} possible moves`;
console.log(`Score after the AI's turn: ${game.score()}`);
}
break; break;
case Moveable.IllegalTrajectory: case Moveable.IllegalTrajectory:
setStatus("You can't move like that!"); setStatus("You can't move like that!");
@ -220,3 +231,67 @@ function updateTeamText(){
break; break;
} }
} }
////////////////
// UI
////////////////
const widthBox = document.getElementById("width");
/**
* Handler for width input box change, start a new game
*/
const onWidth = () => {
BOARD_WIDTH = parseInt(widthBox.value);
start_game();
}
widthBox.onchange = onWidth;
widthBox.value = 8;
const heightBox = document.getElementById("height");
/**
* Handler for height input box change, start a new game
*/
const onHeight = () => {
BOARD_HEIGHT = parseInt(heightBox.value);
pieceRowsBox.max = (BOARD_HEIGHT / 2) - 1;
start_game();
}
heightBox.onchange = onHeight;
heightBox.value = 8;
const pieceRowsBox = document.getElementById("play_rows");
/**
* Handler for piece rows input box change, start a new game
*/
const onPieceRows = () => {
PIECE_ROWS = parseInt(pieceRowsBox.value);
console.log(typeof(PIECE_ROWS));
start_game();
}
pieceRowsBox.onchange = onPieceRows;
pieceRowsBox.value = 3;
const aiSearchDepthBox = document.getElementById("ai_search_depth");
/**
* Handler for AI search depth input box change, start a new game
*/
const onAISearchDepth = () => {
SEARCH_DEPTH = parseInt(aiSearchDepthBox.value);
game.set_search_depth(SEARCH_DEPTH);
}
aiSearchDepthBox.onchange = onAISearchDepth;
aiSearchDepthBox.value = 4;
const aiCheckBox = document.getElementById("ai-checkbox");
/**
* Handler for height input box change, get a new universe of given size
*/
const onAICheck = () => {
console.log(aiCheckBox.checked);
}
aiCheckBox.onchange = onAICheck;
// aiCheckBox.checked = true;