working AI, dynamic canvas size, UI input
This commit is contained in:
parent
6d2eb8ab0a
commit
d922c0f345
12
Cargo.toml
12
Cargo.toml
@ -3,7 +3,7 @@ name = "draught"
|
||||
version = "0.1.0"
|
||||
authors = ["aj <andrewjpack@gmail.com>"]
|
||||
edition = "2018"
|
||||
repository = "https://github.com/Sarsoo/checkers"
|
||||
repository = "https://github.com/Sarsoo/draught"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@ -15,6 +15,12 @@ default = ["console_error_panic_hook"]
|
||||
wasm-bindgen = "0.2.74"
|
||||
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
|
||||
# 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
|
||||
@ -47,6 +53,6 @@ wasm-bindgen-test = "0.3.24"
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
# [package.metadata.wasm-pack.profile.release]
|
||||
# wasm-opt = false
|
||||
|
||||
|
@ -677,6 +677,9 @@ impl Board {
|
||||
|
||||
Board::check_kinged(&mut new, to);
|
||||
|
||||
// board has been changed, update player turn
|
||||
new.current_turn = new.current_turn.opponent();
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
@ -707,6 +710,9 @@ impl Board {
|
||||
|
||||
Board::check_kinged(&mut new, to);
|
||||
|
||||
// board has been changed, update player turn
|
||||
new.current_turn = new.current_turn.opponent();
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
|
300
src/comp/mod.rs
300
src/comp/mod.rs
@ -1,17 +1,18 @@
|
||||
//! AI player logic
|
||||
|
||||
use indextree::{Arena, NodeId};
|
||||
use indextree::{Arena, Node, NodeId, NodeEdge};
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
// use wasm_bindgen::prelude::*;
|
||||
|
||||
// use crate::log;
|
||||
use crate::log;
|
||||
|
||||
use crate::board::{Board, BrdIdx};
|
||||
use crate::board::enums::{MoveType, Moveable, Team};
|
||||
use crate::board::iter::{PieceIterator};
|
||||
|
||||
// use Team::*;
|
||||
use Team::*;
|
||||
// use Strength::*;
|
||||
// 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
|
||||
#[derive(Debug)]
|
||||
pub struct Computer {
|
||||
pub root_node_id: NodeId,
|
||||
pub search_depth: usize,
|
||||
pub team: Team,
|
||||
pub last_node_count: usize,
|
||||
}
|
||||
|
||||
impl Computer {
|
||||
pub fn new(initial_board: Board, search_depth: usize, team: Team) -> Computer {
|
||||
let mut tree = Arena::new();
|
||||
let root_node_id = tree.new_node(initial_board);
|
||||
pub fn new(search_depth: usize, team: Team) -> Computer {
|
||||
Computer {
|
||||
root_node_id,
|
||||
search_depth,
|
||||
team
|
||||
team,
|
||||
last_node_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
// to reduce memory re-allocations
|
||||
@ -109,43 +128,250 @@ impl Computer {
|
||||
moves
|
||||
}
|
||||
|
||||
pub fn gen_tree(&mut self, tree: &mut Arena<Board>, board: Board) {
|
||||
|
||||
// possible boards from given
|
||||
let boards = self.get_move_boards(&board);
|
||||
/// Generate tree of boards to given search depth, return root node
|
||||
fn gen_tree(&mut self, tree: &mut Arena<BoardNode>, board: Board) -> NodeId {
|
||||
|
||||
// root node of tree
|
||||
let root = tree.new_node(board);
|
||||
|
||||
// insert possible boards
|
||||
let ids = self.insert_boards(tree, boards);
|
||||
// append ids to root node
|
||||
ids.iter().for_each( |id| root.append(*id, tree) );
|
||||
|
||||
}
|
||||
|
||||
pub fn insert_boards(&mut self, tree: &mut Arena<Board>, boards: Vec<Board>) -> Vec<NodeId> {
|
||||
let root = tree.new_node(BoardNode::brd(board));
|
||||
|
||||
boards
|
||||
.into_iter().map(
|
||||
|b| tree.new_node(b)
|
||||
).collect()
|
||||
let mut nodes = vec!(root);
|
||||
|
||||
for _ in 0..self.search_depth {
|
||||
if nodes.len() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
nodes = self.expand_layer(tree, nodes);
|
||||
}
|
||||
|
||||
root
|
||||
}
|
||||
|
||||
pub fn get_move_boards(&self, board: &Board) -> Vec<Board> {
|
||||
/// Propagate scores up the tree employing MiniMax algorithm
|
||||
fn propagate_scores(&mut self, tree: Arena<BoardNode>, root: NodeId) -> Arena<BoardNode> {
|
||||
|
||||
self.available_turns(board)
|
||||
.into_iter().map(
|
||||
|m| {
|
||||
match m.mv_type {
|
||||
MoveType::Move => board.apply_move(m.from, m.to),
|
||||
MoveType::Jump => board.apply_jump(m.from, m.to),
|
||||
// need to clone tree because we iterate over it and edit it at the same time
|
||||
let mut new_tree = tree.clone();
|
||||
|
||||
for n in root.traverse(&tree) {
|
||||
// only check end variant, checks nodes once
|
||||
if let NodeEdge::End(node_id) = n {
|
||||
|
||||
// board current being looked at
|
||||
let board_node = tree
|
||||
.get(node_id) // get Node
|
||||
.expect("No node returned for node id")
|
||||
.get(); // get BoardNode from Node
|
||||
|
||||
// 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
use super::*;
|
||||
// use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use crate::board::Square;
|
||||
use crate::board::enums::Strength::*;
|
||||
use crate::log;
|
||||
|
||||
use Team::*;
|
||||
// use Team::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
@ -23,18 +24,17 @@ fn available_moves() {
|
||||
// . W .
|
||||
// _ . _
|
||||
|
||||
let brd = Board::new(3, 2, White);
|
||||
let mut brd2 = brd.clone();
|
||||
let comp = Computer::new(brd, 3, White);
|
||||
let mut brd = Board::new(3, 2, White);
|
||||
let comp = Computer::new(3, White);
|
||||
|
||||
// log!("{}", brd2);
|
||||
// log!("{}", brd);
|
||||
|
||||
// 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);
|
||||
|
||||
@ -49,18 +49,17 @@ fn available_moves_jumps() {
|
||||
// . _ . _
|
||||
// _ . _ .
|
||||
|
||||
let brd = Board::new(4, 4, White);
|
||||
let mut brd2 = brd.clone();
|
||||
let comp = Computer::new(brd, 3, White);
|
||||
let mut brd = Board::new(4, 4, White);
|
||||
let comp = Computer::new(3, White);
|
||||
|
||||
// log!("{}", brd2);
|
||||
// log!("{}", brd);
|
||||
|
||||
brd2.set_cell(brd2.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(0, 1), Square::pc(White, 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);
|
||||
|
||||
@ -72,12 +71,11 @@ fn available_moves_jumps() {
|
||||
#[wasm_bindgen_test]
|
||||
fn available_moves_std_brd() {
|
||||
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||
let brd2 = brd.clone();
|
||||
let comp = Computer::new(brd, 3, White);
|
||||
let comp = Computer::new(3, White);
|
||||
|
||||
// log!("{}", brd2);
|
||||
// log!("{}", brd);
|
||||
|
||||
let moves = comp.available_turns(&brd2);
|
||||
let moves = comp.available_turns(&brd);
|
||||
|
||||
// log!("{:?}", moves);
|
||||
|
||||
@ -87,19 +85,204 @@ fn available_moves_std_brd() {
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn tree_insert() {
|
||||
fn expand_node() {
|
||||
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||
let brd2 = brd.clone();
|
||||
let mut comp = Computer::new(brd, 3, White);
|
||||
let mut comp = Computer::new(3, White);
|
||||
|
||||
// log!("{}", brd2);
|
||||
// log!("{}", brd);
|
||||
|
||||
let moves = comp.available_turns(&brd2);
|
||||
log!("{}", moves.len());
|
||||
let mut tree = Arena::new();
|
||||
let id = tree.new_node(BoardNode::brd(brd));
|
||||
|
||||
let mut tree = Arena::new();
|
||||
comp.gen_tree(&mut tree, brd2);
|
||||
let moves = comp.expand_node(&mut tree, id);
|
||||
|
||||
log!("{}", tree.count());
|
||||
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);
|
||||
// }
|
@ -10,6 +10,7 @@ use crate::log;
|
||||
use crate::board::{Square, BrdIdx};
|
||||
use crate::board::enums::{SquareState, Moveable, Team};
|
||||
use crate::paint::Painter;
|
||||
use crate::comp::Computer;
|
||||
|
||||
// use Team::*;
|
||||
use SquareState::*;
|
||||
@ -25,7 +26,9 @@ pub struct Game {
|
||||
current: Board,
|
||||
selected_piece: Option<BrdIdx>,
|
||||
previous_boards: Vec<Board>,
|
||||
painter: Option<Painter>
|
||||
painter: Option<Painter>,
|
||||
search_depth: usize,
|
||||
pub last_node_count: usize,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@ -57,11 +60,20 @@ impl Game {
|
||||
self.current.current_turn
|
||||
}
|
||||
|
||||
/// Current board's score
|
||||
pub fn score(&self) -> isize {
|
||||
self.current.score()
|
||||
}
|
||||
|
||||
/// Get square on current board for given index
|
||||
pub fn current_cell_state(&self, idx: &BrdIdx) -> Square {
|
||||
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
|
||||
/// TODO: Check whether valid square?
|
||||
pub fn set_selected(&mut self, idx: &BrdIdx) {
|
||||
@ -101,9 +113,6 @@ impl Game {
|
||||
self.execute_jump(from, to);
|
||||
}
|
||||
|
||||
// board has been changed, update player turn
|
||||
self.current.current_turn = self.current.current_turn.opponent();
|
||||
|
||||
} else {
|
||||
log!("Unable to make move, {:?}", able);
|
||||
}
|
||||
@ -135,7 +144,7 @@ impl Game {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
current: Board::init_game(
|
||||
Board::new(width, height, first_turn), piece_rows,
|
||||
@ -143,10 +152,12 @@ impl Game {
|
||||
selected_piece: None,
|
||||
previous_boards: Vec::with_capacity(10),
|
||||
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 {
|
||||
current: Board::init_game(
|
||||
Board::new(width, height, first_turn), piece_rows,
|
||||
@ -156,6 +167,8 @@ impl Game {
|
||||
painter: Some(
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -11,7 +11,7 @@ use crate::board::enums::Team::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
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);
|
||||
|
||||
@ -39,7 +39,7 @@ fn make_move() {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
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);
|
||||
|
||||
|
@ -19,9 +19,9 @@ pub use paint::Painter;
|
||||
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
||||
// allocator.
|
||||
// #[cfg(feature = "wee_alloc")]
|
||||
// #[global_allocator]
|
||||
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
/// Wrap the [`web_sys`] access to the browser console in a macro for easy logging
|
||||
#[macro_export]
|
||||
|
@ -47,10 +47,9 @@ 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 = 3.0;
|
||||
|
||||
const PIECE_OUTLINE_WIDTH: f64 = 6.0;
|
||||
/// 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
|
||||
#[wasm_bindgen]
|
||||
|
@ -25,6 +25,46 @@
|
||||
h1 {
|
||||
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>
|
||||
</head>
|
||||
|
||||
@ -42,10 +82,55 @@
|
||||
<div class="row p-3">
|
||||
<div class="col-sm-12">
|
||||
<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 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="col-sm-12">
|
||||
<h1 id="team-p"></h1>
|
||||
|
93
www/index.js
93
www/index.js
@ -5,13 +5,14 @@ import { Game, Board, BrdIdx, Painter, Team, init_wasm, Moveable, SquareState, S
|
||||
// CONSTS
|
||||
///////////////////
|
||||
|
||||
const CANVAS_WIDTH = 480;
|
||||
const CANVAS_HEIGHT = 480;
|
||||
const CANVAS_WIDTH = 720;
|
||||
const CANVAS_HEIGHT = 720;
|
||||
|
||||
const BOARD_WIDTH = 8;
|
||||
const BOARD_HEIGHT = 8;
|
||||
var BOARD_WIDTH = 8;
|
||||
var BOARD_HEIGHT = 8;
|
||||
|
||||
const PIECE_ROWS = 3;
|
||||
var PIECE_ROWS = 3;
|
||||
var SEARCH_DEPTH = 4;
|
||||
|
||||
const STATUS_TIMEOUT = 3000;
|
||||
|
||||
@ -34,6 +35,7 @@ init_wasm();
|
||||
const statusText = document.getElementById("status-p");
|
||||
const statusAlert = document.getElementById("status-d");
|
||||
const teamText = document.getElementById("team-p");
|
||||
const nodeCountText = document.getElementById("node-count");
|
||||
|
||||
const startBtn = document.getElementById("startBtn");
|
||||
startBtn.onclick = start_game;
|
||||
@ -59,24 +61,25 @@ canvas.addEventListener("click", (event) => {
|
||||
var mousepos = getMousePos(canvas, event);
|
||||
// console.log(mousepos);
|
||||
var cell = new BrdIdx(
|
||||
Math.floor((mousepos.y / CANVAS_HEIGHT) * BOARD_HEIGHT),
|
||||
Math.floor((mousepos.x / CANVAS_WIDTH) * BOARD_WIDTH),
|
||||
Math.floor((mousepos.y / canvas.clientHeight) * BOARD_HEIGHT),
|
||||
Math.floor((mousepos.x / canvas.clientWidth) * BOARD_WIDTH),
|
||||
);
|
||||
// console.log(cell);
|
||||
process_canvas_click(cell);
|
||||
})
|
||||
});
|
||||
|
||||
////////////////
|
||||
// FUNCS
|
||||
////////////////
|
||||
|
||||
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");
|
||||
game.set_painter(painter);
|
||||
game.draw();
|
||||
|
||||
updateTeamText();
|
||||
clicks = [];
|
||||
current_state = GameState.HUMAN_TURN.THINKING;
|
||||
}
|
||||
|
||||
@ -128,6 +131,14 @@ function process_canvas_click(cell_coord) {
|
||||
|
||||
switch(status) {
|
||||
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;
|
||||
case Moveable.IllegalTrajectory:
|
||||
setStatus("You can't move like that!");
|
||||
@ -220,3 +231,67 @@ function updateTeamText(){
|
||||
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;
|
Loading…
Reference in New Issue
Block a user