fixed AI, added win state
This commit is contained in:
parent
d922c0f345
commit
eb1a240bd7
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "draught"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
authors = ["aj <andrewjpack@gmail.com>"]
|
||||
edition = "2018"
|
||||
repository = "https://github.com/Sarsoo/draught"
|
||||
@ -10,6 +10,8 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
debug_logs = [] # log extra stuff to the web console
|
||||
time_ex = [] # allow time profiling in computer
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.74"
|
||||
|
@ -5,10 +5,12 @@ 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.
|
||||
|
||||
![Screenshot](./docs/screenshot.png)
|
||||
|
||||
## Building
|
||||
|
||||
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};
|
||||
|
||||
/// Move/Jump, for use in Move
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@ -11,6 +12,7 @@ pub enum MoveType {
|
||||
Jump = 1,
|
||||
}
|
||||
|
||||
/// Black/White
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@ -20,6 +22,7 @@ pub enum Team {
|
||||
}
|
||||
|
||||
impl Team {
|
||||
/// Get opposing team
|
||||
pub fn opponent(&self) -> Team{
|
||||
match self {
|
||||
Team::White => Team::Black,
|
||||
@ -37,6 +40,7 @@ impl Display for Team {
|
||||
}
|
||||
}
|
||||
|
||||
/// Man/King
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@ -45,6 +49,7 @@ pub enum Strength {
|
||||
King = 1
|
||||
}
|
||||
|
||||
/// Model board square as Empty/Occupied/Unplayable
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@ -64,6 +69,7 @@ impl Display for SquareState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible outcomes of trying to move
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[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 {
|
||||
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 {
|
||||
let (row_diff, col_diff) = Board::idx_diffs(from, to);
|
||||
|
||||
|
@ -20,6 +20,7 @@ use Team::*;
|
||||
|
||||
#[cfg(test)] pub mod tests;
|
||||
|
||||
/// Represents a move by source/destination indices and the move type
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Move {
|
||||
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)]
|
||||
pub struct BoardNode {
|
||||
pub board: Board,
|
||||
@ -148,7 +150,7 @@ impl Computer {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let mut new_tree = tree.clone();
|
||||
@ -158,17 +160,17 @@ impl Computer {
|
||||
if let NodeEdge::End(node_id) = n {
|
||||
|
||||
// board current being looked at
|
||||
let board_node = tree
|
||||
let board_node = new_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)
|
||||
.children(&new_tree)
|
||||
.into_iter()
|
||||
.map(
|
||||
|n| tree
|
||||
|n| new_tree
|
||||
.get(n) // get Node
|
||||
.expect("No node returned for node id") // unwrap, should always be fine
|
||||
.get() // get BoardNode from Node
|
||||
@ -192,6 +194,7 @@ impl Computer {
|
||||
new_tree
|
||||
}
|
||||
|
||||
/// Get best of given scores for given team
|
||||
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
|
||||
@ -305,6 +308,7 @@ impl Computer {
|
||||
).collect()
|
||||
}
|
||||
|
||||
/// Get a new board based on the given using MiniMax to make decisions
|
||||
pub fn get_move(&mut self, brd: Board) -> Option<Board> {
|
||||
|
||||
let mut tree = Arena::new();
|
||||
@ -321,7 +325,7 @@ impl Computer {
|
||||
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);
|
||||
let tree = Computer::propagate_scores(tree, root_node);
|
||||
|
||||
// get root node to compare
|
||||
let root_board_node = tree
|
||||
@ -329,13 +333,13 @@ impl Computer {
|
||||
.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
|
||||
#[cfg(feature = "debug_logs")]
|
||||
{
|
||||
log!("Current root score: {}", root_board_node.score);
|
||||
let scores: Vec<NodeId> = root_node
|
||||
.children(&tree)
|
||||
.collect();
|
||||
@ -343,8 +347,8 @@ impl Computer {
|
||||
.into_iter()
|
||||
.map(|n| tree.get(n).unwrap().get().score)
|
||||
.collect();
|
||||
log!("SCORES: {:?}", scores);
|
||||
// DEBUG
|
||||
log!("Next boards scores: {:?}", scores);
|
||||
}
|
||||
|
||||
// search through root node's children for the same score
|
||||
for n in root_node.children(&tree) {
|
||||
|
@ -146,9 +146,21 @@ fn best_scores() {
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn propagate_scores() {
|
||||
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||
let mut comp = Computer::new(3, White);
|
||||
fn insert_scores_all_take() {
|
||||
// . _ . _ .
|
||||
// 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);
|
||||
|
||||
@ -157,100 +169,95 @@ fn propagate_scores() {
|
||||
|
||||
comp.expand_layer(&mut tree, vec!(root));
|
||||
|
||||
let moves = comp.propagate_scores(tree, root);
|
||||
// log!("{}", moves.len());
|
||||
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);
|
||||
|
||||
// log!("{}", tree.count());
|
||||
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]
|
||||
// fn tree_2_depth() {
|
||||
// // log!("{}", performance.timing().request_start());
|
||||
#[wasm_bindgen_test]
|
||||
fn insert_scores_one_take() {
|
||||
// . _ . _ .
|
||||
// W . _ . W
|
||||
// . B . _ .
|
||||
// _ . _ . _
|
||||
|
||||
// let iter = 3;
|
||||
// let mut times = Vec::with_capacity(iter);
|
||||
// 4 available moves, all are white taking black
|
||||
|
||||
// for _ in 0..iter {
|
||||
// times.push(time_tree_gen(6));
|
||||
// }
|
||||
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!("{:?}", times);
|
||||
// }
|
||||
// log!("{}", brd);
|
||||
|
||||
// fn time_tree_gen(depth: usize) {
|
||||
// web_sys::console::time_with_label("tree_timer");
|
||||
let mut tree = Arena::new();
|
||||
let root = tree.new_node(BoardNode::brd(brd));
|
||||
|
||||
// let mut comp = Computer::new(depth, White);
|
||||
comp.expand_layer(&mut tree, vec!(root));
|
||||
|
||||
// let mut tree = Arena::new();
|
||||
// let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||
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);
|
||||
|
||||
// comp.gen_tree(&mut tree, brd);
|
||||
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
|
||||
|
||||
// web_sys::console::time_end_with_label("tree_timer");
|
||||
// log!("{}", tree.count());
|
||||
// }
|
||||
// log!("{:?}", children_scores);
|
||||
|
||||
// #[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);
|
||||
assert_eq!(children_scores, vec!(-3, -1));
|
||||
}
|
||||
|
||||
// // log!("{}", brd);
|
||||
// // log!("{}", brd.score());
|
||||
#[cfg(feature = "time_ex")]
|
||||
#[wasm_bindgen_test]
|
||||
fn tree_2_depth() {
|
||||
|
||||
// // let moves = comp.available_turns(&brd);
|
||||
// // log!("{}", moves.len());
|
||||
let iter = 3;
|
||||
let mut times = Vec::with_capacity(iter);
|
||||
|
||||
// let mut tree = Arena::new();
|
||||
// let root_node = comp.gen_tree(&mut tree, brd);
|
||||
for _ in 0..iter {
|
||||
times.push(time_tree_gen(6));
|
||||
}
|
||||
}
|
||||
|
||||
// let lowest_nodes = comp.get_leaf_nodes(&mut tree, root_node);
|
||||
#[cfg(feature = "time_ex")]
|
||||
fn time_tree_gen(depth: usize) {
|
||||
web_sys::console::time_with_label("tree_timer");
|
||||
|
||||
// // log!("{:#?}", lowest_nodes);
|
||||
let mut comp = Computer::new(depth, White);
|
||||
|
||||
// comp.insert_board_scores(&mut tree, lowest_nodes);
|
||||
let mut tree = Arena::new();
|
||||
let brd = Board::init_game(Board::new(8, 8, White), 3);
|
||||
|
||||
// // log!("{:#?}", tree);
|
||||
// // log!("{}", tree.count());
|
||||
// }
|
||||
comp.gen_tree(&mut tree, brd);
|
||||
|
||||
// #[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());
|
||||
// }
|
||||
web_sys::console::time_end_with_label("tree_timer");
|
||||
}
|
||||
|
||||
// #[wasm_bindgen_test]
|
||||
// fn tree_get_move() {
|
||||
|
@ -12,7 +12,7 @@ use crate::board::enums::{SquareState, Moveable, Team};
|
||||
use crate::paint::Painter;
|
||||
use crate::comp::Computer;
|
||||
|
||||
// use Team::*;
|
||||
use Team::*;
|
||||
use SquareState::*;
|
||||
|
||||
use std::fmt::{Display};
|
||||
@ -65,6 +65,31 @@ impl Game {
|
||||
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
|
||||
pub fn current_cell_state(&self, idx: &BrdIdx) -> Square {
|
||||
self.current.cell(self.current.cell_idx(*idx))
|
||||
@ -143,6 +168,7 @@ impl Game {
|
||||
self.current = board;
|
||||
}
|
||||
|
||||
/// Get new game without board renderer
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team, search_depth: usize) -> 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 {
|
||||
Game {
|
||||
current: Board::init_game(
|
||||
@ -172,10 +199,12 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set painter for rendering boards
|
||||
pub fn set_painter(&mut self, value: Painter) {
|
||||
self.painter = Some(value);
|
||||
}
|
||||
|
||||
/// Draw current board using painter if exists
|
||||
pub fn draw(&self) {
|
||||
match &self.painter {
|
||||
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) {
|
||||
|
||||
let mut comp = Computer::new(self.search_depth, self.current.current_turn);
|
||||
|
@ -5,7 +5,6 @@
|
||||
pub mod board;
|
||||
pub mod utils;
|
||||
pub mod game;
|
||||
pub mod player;
|
||||
pub mod paint;
|
||||
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;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||
user-select: none; /* Standard syntax */
|
||||
}
|
||||
|
||||
#game-canvas {
|
||||
width: 1000px;
|
||||
height: 1000px;
|
||||
@ -73,21 +79,21 @@
|
||||
<div class="card-header">
|
||||
<h1>Draught 🚀</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body no-select">
|
||||
<div class="row p-1">
|
||||
<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 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-success">Start</button>
|
||||
<button id="startBtn" class="btn btn-success" title="reset the game and start again">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row p-3">
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-4" title="board width in cells">
|
||||
<input type="number"
|
||||
id="width"
|
||||
name="width"
|
||||
@ -95,7 +101,7 @@
|
||||
class="form-control">
|
||||
<label for="width">width</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-4" title="board height in cells">
|
||||
<input type="number"
|
||||
id="height"
|
||||
name="height"
|
||||
@ -103,7 +109,7 @@
|
||||
class="form-control">
|
||||
<label for="height">height</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-4" title="number of rows to populate with pieces per player">
|
||||
<input type="number"
|
||||
id="play_rows"
|
||||
name="play_rows"
|
||||
@ -113,28 +119,35 @@
|
||||
</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">
|
||||
<div class="col-sm-4" title="should the AI play?">
|
||||
<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">
|
||||
<div class="col-sm-4" title="how many layers deep should the AI search (grows exponentially, be careful)">
|
||||
<input type="number"
|
||||
id="ai_search_depth"
|
||||
name="ai_search_depth"
|
||||
min="1" max="10" value="4"
|
||||
class="form-control">
|
||||
<label for="ai_search_depth">ai difficulty</label>
|
||||
<label for="ai_search_depth">ai difficulty <small class="text-muted">moves ahead</small></label>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-3">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-6" title="current turn">
|
||||
<h1 id="team-p"></h1>
|
||||
</div>
|
||||
<div class="col-sm-6" title="who's winning">
|
||||
<h1 id="winning-p"></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-3">
|
||||
<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;
|
||||
|
||||
const STATUS_TIMEOUT = 3000;
|
||||
const WON_TIMEOUT = 3000;
|
||||
|
||||
const GameState = {
|
||||
HUMAN_TURN: {
|
||||
@ -36,10 +37,12 @@ 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 winningText = document.getElementById("winning-p");
|
||||
|
||||
const startBtn = document.getElementById("startBtn");
|
||||
startBtn.onclick = start_game;
|
||||
|
||||
let wonTimeout = null;
|
||||
let statusTimeout = null;
|
||||
let setStatus = setStatusAlert;
|
||||
|
||||
@ -78,7 +81,9 @@ function start_game() {
|
||||
game.set_painter(painter);
|
||||
game.draw();
|
||||
|
||||
clearInterval(wonTimeout);
|
||||
updateTeamText();
|
||||
updateWinningText();
|
||||
clicks = [];
|
||||
current_state = GameState.HUMAN_TURN.THINKING;
|
||||
}
|
||||
@ -131,12 +136,10 @@ 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;
|
||||
@ -180,6 +183,8 @@ function process_canvas_click(cell_coord) {
|
||||
}
|
||||
|
||||
updateTeamText();
|
||||
updateWinningText();
|
||||
checkWon();
|
||||
}
|
||||
|
||||
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
|
||||
////////////////
|
||||
@ -268,7 +308,6 @@ const pieceRowsBox = document.getElementById("play_rows");
|
||||
const onPieceRows = () => {
|
||||
|
||||
PIECE_ROWS = parseInt(pieceRowsBox.value);
|
||||
console.log(typeof(PIECE_ROWS));
|
||||
start_game();
|
||||
}
|
||||
pieceRowsBox.onchange = onPieceRows;
|
||||
@ -282,6 +321,10 @@ const onAISearchDepth = () => {
|
||||
|
||||
SEARCH_DEPTH = parseInt(aiSearchDepthBox.value);
|
||||
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.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
|
||||
*/
|
||||
const onAICheck = () => {
|
||||
console.log(aiCheckBox.checked);
|
||||
|
||||
}
|
||||
aiCheckBox.onchange = onAICheck;
|
||||
// aiCheckBox.checked = true;
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "draught",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"description": "Rust wasm-based checkers game",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
Loading…
Reference in New Issue
Block a user