fixed AI, added win state

This commit is contained in:
andy 2021-07-13 12:07:33 +01:00
parent d922c0f345
commit eb1a240bd7
13 changed files with 226 additions and 126 deletions

View File

@ -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"

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -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)]

View File

@ -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);

View File

@ -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) {

View File

@ -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() {

View File

@ -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);

View File

@ -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;

View File

@ -1,8 +0,0 @@
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Clone)]
pub struct Player {
score: usize,
}

View File

@ -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">

View File

@ -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;

View File

@ -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": {