fixed AI, added win state
This commit is contained in:
parent
d922c0f345
commit
eb1a240bd7
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "draught"
|
name = "draught"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
authors = ["aj <andrewjpack@gmail.com>"]
|
authors = ["aj <andrewjpack@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
repository = "https://github.com/Sarsoo/draught"
|
repository = "https://github.com/Sarsoo/draught"
|
||||||
@ -10,6 +10,8 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
debug_logs = [] # log extra stuff to the web console
|
||||||
|
time_ex = [] # allow time profiling in computer
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2.74"
|
wasm-bindgen = "0.2.74"
|
||||||
|
@ -5,10 +5,12 @@ Draught
|
|||||||
|
|
||||||
## [Try it Out!](https://sarsoo.github.io/draught/)
|
## [Try it Out!](https://sarsoo.github.io/draught/)
|
||||||
|
|
||||||
WASM-based checkers game. Looking to implement a minimax based AI.
|
WebAssembly-based checkers game with a minimax-based AI player.
|
||||||
|
|
||||||
Rust WASM module for game logic with a JS frontend for rendering and processing user input.
|
Rust WASM module for game logic with a JS frontend for rendering and processing user input.
|
||||||
|
|
||||||
|
![Screenshot](./docs/screenshot.png)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
1. Setup a Rust + wasm-pack environment and a Node environment
|
1. Setup a Rust + wasm-pack environment and a Node environment
|
||||||
|
BIN
docs/screenshot.png
Normal file
BIN
docs/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -3,6 +3,7 @@ use wasm_bindgen::prelude::*;
|
|||||||
|
|
||||||
use std::fmt::{Display};
|
use std::fmt::{Display};
|
||||||
|
|
||||||
|
/// Move/Jump, for use in Move
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -11,6 +12,7 @@ pub enum MoveType {
|
|||||||
Jump = 1,
|
Jump = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Black/White
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -20,6 +22,7 @@ pub enum Team {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Team {
|
impl Team {
|
||||||
|
/// Get opposing team
|
||||||
pub fn opponent(&self) -> Team{
|
pub fn opponent(&self) -> Team{
|
||||||
match self {
|
match self {
|
||||||
Team::White => Team::Black,
|
Team::White => Team::Black,
|
||||||
@ -37,6 +40,7 @@ impl Display for Team {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Man/King
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -45,6 +49,7 @@ pub enum Strength {
|
|||||||
King = 1
|
King = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Model board square as Empty/Occupied/Unplayable
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -64,6 +69,7 @@ impl Display for SquareState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Possible outcomes of trying to move
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
@ -491,6 +491,7 @@ impl Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that given move trajectory is valid for a man piece
|
||||||
pub fn validate_man_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable {
|
pub fn validate_man_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable {
|
||||||
let (row_diff, col_diff) = Board::idx_diffs(from, to);
|
let (row_diff, col_diff) = Board::idx_diffs(from, to);
|
||||||
|
|
||||||
@ -539,6 +540,7 @@ impl Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that given move trajectory is valid for a king piece
|
||||||
pub fn validate_king_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable {
|
pub fn validate_king_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable {
|
||||||
let (row_diff, col_diff) = Board::idx_diffs(from, to);
|
let (row_diff, col_diff) = Board::idx_diffs(from, to);
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ use Team::*;
|
|||||||
|
|
||||||
#[cfg(test)] pub mod tests;
|
#[cfg(test)] pub mod tests;
|
||||||
|
|
||||||
|
/// Represents a move by source/destination indices and the move type
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Move {
|
pub struct Move {
|
||||||
from: BrdIdx,
|
from: BrdIdx,
|
||||||
@ -35,6 +36,7 @@ impl Move {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For storing boards in the AI tree, stores board with score for comparisons
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct BoardNode {
|
pub struct BoardNode {
|
||||||
pub board: Board,
|
pub board: Board,
|
||||||
@ -148,7 +150,7 @@ impl Computer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Propagate scores up the tree employing MiniMax algorithm
|
/// Propagate scores up the tree employing MiniMax algorithm
|
||||||
fn propagate_scores(&mut self, tree: Arena<BoardNode>, root: NodeId) -> Arena<BoardNode> {
|
fn propagate_scores(tree: Arena<BoardNode>, root: NodeId) -> Arena<BoardNode> {
|
||||||
|
|
||||||
// need to clone tree because we iterate over it and edit it at the same time
|
// need to clone tree because we iterate over it and edit it at the same time
|
||||||
let mut new_tree = tree.clone();
|
let mut new_tree = tree.clone();
|
||||||
@ -158,17 +160,17 @@ impl Computer {
|
|||||||
if let NodeEdge::End(node_id) = n {
|
if let NodeEdge::End(node_id) = n {
|
||||||
|
|
||||||
// board current being looked at
|
// board current being looked at
|
||||||
let board_node = tree
|
let board_node = new_tree
|
||||||
.get(node_id) // get Node
|
.get(node_id) // get Node
|
||||||
.expect("No node returned for node id")
|
.expect("No node returned for node id")
|
||||||
.get(); // get BoardNode from Node
|
.get(); // get BoardNode from Node
|
||||||
|
|
||||||
// get scores of each nodes children
|
// get scores of each nodes children
|
||||||
let children_scores: Vec<isize> = node_id // current node
|
let children_scores: Vec<isize> = node_id // current node
|
||||||
.children(&tree)
|
.children(&new_tree)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|n| tree
|
|n| new_tree
|
||||||
.get(n) // get Node
|
.get(n) // get Node
|
||||||
.expect("No node returned for node id") // unwrap, should always be fine
|
.expect("No node returned for node id") // unwrap, should always be fine
|
||||||
.get() // get BoardNode from Node
|
.get() // get BoardNode from Node
|
||||||
@ -192,6 +194,7 @@ impl Computer {
|
|||||||
new_tree
|
new_tree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get best of given scores for given team
|
||||||
fn best_score(board: &Board, children_scores: Vec<isize>) -> isize {
|
fn best_score(board: &Board, children_scores: Vec<isize>) -> isize {
|
||||||
match board.current_turn { // MiniMax algorithm here
|
match board.current_turn { // MiniMax algorithm here
|
||||||
// whether maximised or minimsed is based on current player
|
// whether maximised or minimsed is based on current player
|
||||||
@ -305,6 +308,7 @@ impl Computer {
|
|||||||
).collect()
|
).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a new board based on the given using MiniMax to make decisions
|
||||||
pub fn get_move(&mut self, brd: Board) -> Option<Board> {
|
pub fn get_move(&mut self, brd: Board) -> Option<Board> {
|
||||||
|
|
||||||
let mut tree = Arena::new();
|
let mut tree = Arena::new();
|
||||||
@ -321,7 +325,7 @@ impl Computer {
|
|||||||
self.insert_board_scores(&mut tree, lowest_nodes);
|
self.insert_board_scores(&mut tree, lowest_nodes);
|
||||||
|
|
||||||
// propagate the scores up the tree, the root node has the best score
|
// propagate the scores up the tree, the root node has the best score
|
||||||
let tree = self.propagate_scores(tree, root_node);
|
let tree = Computer::propagate_scores(tree, root_node);
|
||||||
|
|
||||||
// get root node to compare
|
// get root node to compare
|
||||||
let root_board_node = tree
|
let root_board_node = tree
|
||||||
@ -329,22 +333,22 @@ 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
|
||||||
|
|
||||||
log!("{}", root_board_node.score);
|
|
||||||
// log!("{:#?}", tree);
|
|
||||||
|
|
||||||
// when boards have equal scores, store for shuffling and selection
|
// when boards have equal scores, store for shuffling and selection
|
||||||
let mut equal_scores = Vec::with_capacity(10);
|
let mut equal_scores = Vec::with_capacity(10);
|
||||||
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
let scores: Vec<NodeId> = root_node
|
#[cfg(feature = "debug_logs")]
|
||||||
.children(&tree)
|
{
|
||||||
.collect();
|
log!("Current root score: {}", root_board_node.score);
|
||||||
let scores: Vec<isize> = scores
|
let scores: Vec<NodeId> = root_node
|
||||||
.into_iter()
|
.children(&tree)
|
||||||
.map(|n| tree.get(n).unwrap().get().score)
|
.collect();
|
||||||
.collect();
|
let scores: Vec<isize> = scores
|
||||||
log!("SCORES: {:?}", scores);
|
.into_iter()
|
||||||
// DEBUG
|
.map(|n| tree.get(n).unwrap().get().score)
|
||||||
|
.collect();
|
||||||
|
log!("Next boards scores: {:?}", scores);
|
||||||
|
}
|
||||||
|
|
||||||
// search through root node's children for the same score
|
// search through root node's children for the same score
|
||||||
for n in root_node.children(&tree) {
|
for n in root_node.children(&tree) {
|
||||||
|
@ -146,9 +146,21 @@ fn best_scores() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn propagate_scores() {
|
fn insert_scores_all_take() {
|
||||||
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
// . _ . _ .
|
||||||
let mut comp = Computer::new(3, White);
|
// W . W . W
|
||||||
|
// . B . B .
|
||||||
|
// _ . _ . _
|
||||||
|
|
||||||
|
// 4 available moves, all are white taking black
|
||||||
|
|
||||||
|
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(1, White);
|
||||||
|
|
||||||
// log!("{}", brd);
|
// log!("{}", brd);
|
||||||
|
|
||||||
@ -156,101 +168,96 @@ fn propagate_scores() {
|
|||||||
let root = tree.new_node(BoardNode::brd(brd));
|
let root = tree.new_node(BoardNode::brd(brd));
|
||||||
|
|
||||||
comp.expand_layer(&mut tree, vec!(root));
|
comp.expand_layer(&mut tree, vec!(root));
|
||||||
|
|
||||||
let moves = comp.propagate_scores(tree, root);
|
|
||||||
// log!("{}", moves.len());
|
|
||||||
|
|
||||||
// log!("{}", tree.count());
|
let lowest_nodes = comp.get_leaf_nodes(&mut tree, root);
|
||||||
|
// insert the board scores for the leaf nodes
|
||||||
|
comp.insert_board_scores(&mut tree, lowest_nodes);
|
||||||
|
|
||||||
|
let children_scores: Vec<isize> = root // 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
|
||||||
|
|
||||||
|
assert_eq!(children_scores, vec!(-3, -3, -3, -3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
// fn tree_2_depth() {
|
fn insert_scores_one_take() {
|
||||||
// // log!("{}", performance.timing().request_start());
|
// . _ . _ .
|
||||||
|
// W . _ . W
|
||||||
|
// . B . _ .
|
||||||
|
// _ . _ . _
|
||||||
|
|
||||||
|
// 4 available moves, all are white taking black
|
||||||
|
|
||||||
// let iter = 3;
|
let mut brd = Board::new(5, 4, White);
|
||||||
// let mut times = Vec::with_capacity(iter);
|
// 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(1, White);
|
||||||
|
|
||||||
// for _ in 0..iter {
|
// log!("{}", brd);
|
||||||
// times.push(time_tree_gen(6));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// log!("{:?}", times);
|
let mut tree = Arena::new();
|
||||||
// }
|
let root = tree.new_node(BoardNode::brd(brd));
|
||||||
|
|
||||||
// fn time_tree_gen(depth: usize) {
|
comp.expand_layer(&mut tree, vec!(root));
|
||||||
// web_sys::console::time_with_label("tree_timer");
|
|
||||||
|
|
||||||
// let mut comp = Computer::new(depth, White);
|
let lowest_nodes = comp.get_leaf_nodes(&mut tree, root);
|
||||||
|
// insert the board scores for the leaf nodes
|
||||||
|
comp.insert_board_scores(&mut tree, lowest_nodes);
|
||||||
|
|
||||||
|
let children_scores: Vec<isize> = root // 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
|
||||||
|
|
||||||
// let mut tree = Arena::new();
|
// log!("{:?}", children_scores);
|
||||||
// let brd = Board::init_game(Board::new(8, 8, White), 3);
|
|
||||||
|
|
||||||
// comp.gen_tree(&mut tree, brd);
|
assert_eq!(children_scores, vec!(-3, -1));
|
||||||
|
}
|
||||||
|
|
||||||
// web_sys::console::time_end_with_label("tree_timer");
|
#[cfg(feature = "time_ex")]
|
||||||
// log!("{}", tree.count());
|
#[wasm_bindgen_test]
|
||||||
// }
|
fn tree_2_depth() {
|
||||||
|
|
||||||
// #[wasm_bindgen_test]
|
let iter = 3;
|
||||||
// fn tree_last_nodes() {
|
let mut times = Vec::with_capacity(iter);
|
||||||
// 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);
|
for _ in 0..iter {
|
||||||
// // log!("{}", brd.score());
|
times.push(time_tree_gen(6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // let moves = comp.available_turns(&brd);
|
#[cfg(feature = "time_ex")]
|
||||||
// // log!("{}", moves.len());
|
fn time_tree_gen(depth: usize) {
|
||||||
|
web_sys::console::time_with_label("tree_timer");
|
||||||
|
|
||||||
// let mut tree = Arena::new();
|
let mut comp = Computer::new(depth, White);
|
||||||
// let root_node = comp.gen_tree(&mut tree, brd);
|
|
||||||
|
|
||||||
// let lowest_nodes = comp.get_leaf_nodes(&mut tree, root_node);
|
let mut tree = Arena::new();
|
||||||
|
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||||
|
|
||||||
// // log!("{:#?}", lowest_nodes);
|
comp.gen_tree(&mut tree, brd);
|
||||||
|
|
||||||
// comp.insert_board_scores(&mut tree, lowest_nodes);
|
web_sys::console::time_end_with_label("tree_timer");
|
||||||
|
}
|
||||||
// // 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]
|
// #[wasm_bindgen_test]
|
||||||
// fn tree_get_move() {
|
// fn tree_get_move() {
|
||||||
|
@ -12,7 +12,7 @@ use crate::board::enums::{SquareState, Moveable, Team};
|
|||||||
use crate::paint::Painter;
|
use crate::paint::Painter;
|
||||||
use crate::comp::Computer;
|
use crate::comp::Computer;
|
||||||
|
|
||||||
// use Team::*;
|
use Team::*;
|
||||||
use SquareState::*;
|
use SquareState::*;
|
||||||
|
|
||||||
use std::fmt::{Display};
|
use std::fmt::{Display};
|
||||||
@ -65,6 +65,31 @@ impl Game {
|
|||||||
self.current.score()
|
self.current.score()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get currently winning player
|
||||||
|
pub fn winning(&self) -> Option<Team> {
|
||||||
|
let current_score = self.score();
|
||||||
|
|
||||||
|
if current_score < 0 {
|
||||||
|
Some(White)
|
||||||
|
} else if current_score == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a player has won
|
||||||
|
pub fn has_won(&self) -> Option<Team> {
|
||||||
|
|
||||||
|
if self.current.num_player(White) == 0 {
|
||||||
|
Some(Black)
|
||||||
|
} else if self.current.num_player(Black) == 0 {
|
||||||
|
Some(White)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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))
|
||||||
@ -143,6 +168,7 @@ impl Game {
|
|||||||
self.current = board;
|
self.current = board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get new game without board renderer
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team, search_depth: usize) -> Game {
|
pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team, search_depth: usize) -> Game {
|
||||||
Game {
|
Game {
|
||||||
@ -157,6 +183,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a new game with canvas ID and dimensions
|
||||||
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 {
|
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(
|
||||||
@ -172,10 +199,12 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set painter for rendering boards
|
||||||
pub fn set_painter(&mut self, value: Painter) {
|
pub fn set_painter(&mut self, value: Painter) {
|
||||||
self.painter = Some(value);
|
self.painter = Some(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw current board using painter if exists
|
||||||
pub fn draw(&self) {
|
pub fn draw(&self) {
|
||||||
match &self.painter {
|
match &self.painter {
|
||||||
Some(p) => p.draw(&self.current),
|
Some(p) => p.draw(&self.current),
|
||||||
@ -183,6 +212,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod player;
|
|
||||||
pub mod paint;
|
pub mod paint;
|
||||||
pub mod comp;
|
pub mod comp;
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
extern crate wasm_bindgen;
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Player {
|
|
||||||
score: usize,
|
|
||||||
}
|
|
@ -26,6 +26,12 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-select {
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||||
|
user-select: none; /* Standard syntax */
|
||||||
|
}
|
||||||
|
|
||||||
#game-canvas {
|
#game-canvas {
|
||||||
width: 1000px;
|
width: 1000px;
|
||||||
height: 1000px;
|
height: 1000px;
|
||||||
@ -73,21 +79,21 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h1>Draught 🚀</h1>
|
<h1>Draught 🚀</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body no-select">
|
||||||
<div class="row p-1">
|
<div class="row p-1">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<p class="text-muted">Working on an implementation of checkers in Rust WASM with a thin Js frontend, mainly as an exercise to learn Rust and to have a larger project in the language to fiddle with. The idea is to use the <a href="https://en.wikipedia.org/wiki/Minimax">minimax</a> algorithm to create an AI player that can operate with reasonable performance as a result of Rust's compiled performance.</p>
|
<p class="text-muted">An implementation of checkers in Rust WASM with a thin Js frontend, mainly as an exercise to learn Rust and to have a larger project in the language to fiddle with. Using the <a href="https://en.wikipedia.org/wiki/Minimax">minimax</a> algorithm for an AI player that can operate with reasonable performance as a result of Rust's compiled performance.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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-success">Start</button>
|
<button id="startBtn" class="btn btn-success" title="reset the game and start again">Start</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row p-3">
|
<div class="row p-3">
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" title="board width in cells">
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="width"
|
id="width"
|
||||||
name="width"
|
name="width"
|
||||||
@ -95,7 +101,7 @@
|
|||||||
class="form-control">
|
class="form-control">
|
||||||
<label for="width">width</label>
|
<label for="width">width</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" title="board height in cells">
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="height"
|
id="height"
|
||||||
name="height"
|
name="height"
|
||||||
@ -103,7 +109,7 @@
|
|||||||
class="form-control">
|
class="form-control">
|
||||||
<label for="height">height</label>
|
<label for="height">height</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" title="number of rows to populate with pieces per player">
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="play_rows"
|
id="play_rows"
|
||||||
name="play_rows"
|
name="play_rows"
|
||||||
@ -113,28 +119,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row p-3">
|
<div class="row p-3">
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" title="should the AI play?">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="ai-checkbox" checked="checked">
|
<input class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="ai-checkbox"
|
||||||
|
checked="checked">
|
||||||
<label class="form-check-label" for="ai-checkbox">
|
<label class="form-check-label" for="ai-checkbox">
|
||||||
AI Player
|
AI Player
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" 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</label>
|
<label for="ai_search_depth">ai difficulty <small class="text-muted">moves ahead</small></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4" 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>
|
||||||
<div class="row p-3">
|
<div class="row p-3">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-6" title="current turn">
|
||||||
<h1 id="team-p"></h1>
|
<h1 id="team-p"></h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-6" title="who's winning">
|
||||||
|
<h1 id="winning-p"></h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row p-3">
|
<div class="row p-3">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
51
www/index.js
51
www/index.js
@ -15,6 +15,7 @@ var PIECE_ROWS = 3;
|
|||||||
var SEARCH_DEPTH = 4;
|
var SEARCH_DEPTH = 4;
|
||||||
|
|
||||||
const STATUS_TIMEOUT = 3000;
|
const STATUS_TIMEOUT = 3000;
|
||||||
|
const WON_TIMEOUT = 3000;
|
||||||
|
|
||||||
const GameState = {
|
const GameState = {
|
||||||
HUMAN_TURN: {
|
HUMAN_TURN: {
|
||||||
@ -36,10 +37,12 @@ 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 nodeCountText = document.getElementById("node-count");
|
||||||
|
const winningText = document.getElementById("winning-p");
|
||||||
|
|
||||||
const startBtn = document.getElementById("startBtn");
|
const startBtn = document.getElementById("startBtn");
|
||||||
startBtn.onclick = start_game;
|
startBtn.onclick = start_game;
|
||||||
|
|
||||||
|
let wonTimeout = null;
|
||||||
let statusTimeout = null;
|
let statusTimeout = null;
|
||||||
let setStatus = setStatusAlert;
|
let setStatus = setStatusAlert;
|
||||||
|
|
||||||
@ -78,7 +81,9 @@ function start_game() {
|
|||||||
game.set_painter(painter);
|
game.set_painter(painter);
|
||||||
game.draw();
|
game.draw();
|
||||||
|
|
||||||
|
clearInterval(wonTimeout);
|
||||||
updateTeamText();
|
updateTeamText();
|
||||||
|
updateWinningText();
|
||||||
clicks = [];
|
clicks = [];
|
||||||
current_state = GameState.HUMAN_TURN.THINKING;
|
current_state = GameState.HUMAN_TURN.THINKING;
|
||||||
}
|
}
|
||||||
@ -131,12 +136,10 @@ 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) {
|
if (aiCheckBox.checked) {
|
||||||
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`;
|
||||||
console.log(`Score after the AI's turn: ${game.score()}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -180,6 +183,8 @@ function process_canvas_click(cell_coord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTeamText();
|
updateTeamText();
|
||||||
|
updateWinningText();
|
||||||
|
checkWon();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMousePos(canvas, evt) {
|
function getMousePos(canvas, evt) {
|
||||||
@ -232,6 +237,41 @@ function updateTeamText(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateWinningText(){
|
||||||
|
|
||||||
|
switch(game.winning()) {
|
||||||
|
case undefined:
|
||||||
|
winningText.innerText = "";
|
||||||
|
break;
|
||||||
|
case Team.White:
|
||||||
|
winningText.innerText = "👑 White 👑";
|
||||||
|
break;
|
||||||
|
case Team.Black:
|
||||||
|
winningText.innerText = "👑 Black 👑";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkWon() {
|
||||||
|
|
||||||
|
switch(game.has_won()) {
|
||||||
|
case undefined:
|
||||||
|
break;
|
||||||
|
case Team.White:
|
||||||
|
setStatus("You Lost!");
|
||||||
|
wonTimeout = setInterval(() => {
|
||||||
|
start_game();
|
||||||
|
}, WON_TIMEOUT);
|
||||||
|
break;
|
||||||
|
case Team.Black:
|
||||||
|
setStatus("You Won!", "success");
|
||||||
|
wonTimeout = setInterval(() => {
|
||||||
|
start_game();
|
||||||
|
}, WON_TIMEOUT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// UI
|
// UI
|
||||||
////////////////
|
////////////////
|
||||||
@ -268,7 +308,6 @@ const pieceRowsBox = document.getElementById("play_rows");
|
|||||||
const onPieceRows = () => {
|
const onPieceRows = () => {
|
||||||
|
|
||||||
PIECE_ROWS = parseInt(pieceRowsBox.value);
|
PIECE_ROWS = parseInt(pieceRowsBox.value);
|
||||||
console.log(typeof(PIECE_ROWS));
|
|
||||||
start_game();
|
start_game();
|
||||||
}
|
}
|
||||||
pieceRowsBox.onchange = onPieceRows;
|
pieceRowsBox.onchange = onPieceRows;
|
||||||
@ -282,6 +321,10 @@ const onAISearchDepth = () => {
|
|||||||
|
|
||||||
SEARCH_DEPTH = parseInt(aiSearchDepthBox.value);
|
SEARCH_DEPTH = parseInt(aiSearchDepthBox.value);
|
||||||
game.set_search_depth(SEARCH_DEPTH);
|
game.set_search_depth(SEARCH_DEPTH);
|
||||||
|
|
||||||
|
if(SEARCH_DEPTH > 4) {
|
||||||
|
setStatus("This increases thinking time exponentially, be careful (probably don't go past 6)", "warning");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
aiSearchDepthBox.onchange = onAISearchDepth;
|
aiSearchDepthBox.onchange = onAISearchDepth;
|
||||||
aiSearchDepthBox.value = 4;
|
aiSearchDepthBox.value = 4;
|
||||||
@ -291,7 +334,7 @@ const aiCheckBox = document.getElementById("ai-checkbox");
|
|||||||
* Handler for height input box change, get a new universe of given size
|
* Handler for height input box change, get a new universe of given size
|
||||||
*/
|
*/
|
||||||
const onAICheck = () => {
|
const onAICheck = () => {
|
||||||
console.log(aiCheckBox.checked);
|
|
||||||
}
|
}
|
||||||
aiCheckBox.onchange = onAICheck;
|
aiCheckBox.onchange = onAICheck;
|
||||||
// aiCheckBox.checked = true;
|
// aiCheckBox.checked = true;
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "draught",
|
"name": "draught",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"description": "Rust wasm-based checkers game",
|
"description": "Rust wasm-based checkers game",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Loading…
Reference in New Issue
Block a user