2021-07-10 21:00:30 +01:00
|
|
|
import { Game, Board, BrdIdx, Painter, Team, init_wasm, Moveable, SquareState, Square } from "draught";
|
|
|
|
// import { memory } from "draught/draught_bg.wasm";
|
2021-06-25 18:02:45 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
///////////////////
|
|
|
|
// CONSTS
|
|
|
|
///////////////////
|
2021-06-25 18:02:45 +01:00
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
const CANVAS_WIDTH = 720;
|
|
|
|
const CANVAS_HEIGHT = 720;
|
2021-07-02 13:48:07 +01:00
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
var BOARD_WIDTH = 8;
|
|
|
|
var BOARD_HEIGHT = 8;
|
2021-07-02 13:48:07 +01:00
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
var PIECE_ROWS = 3;
|
|
|
|
var SEARCH_DEPTH = 4;
|
2021-07-14 14:08:54 +01:00
|
|
|
var PERFECT_CHANCE = 0.5;
|
2021-07-02 13:48:07 +01:00
|
|
|
|
2021-07-10 21:00:30 +01:00
|
|
|
const STATUS_TIMEOUT = 3000;
|
2021-07-13 12:07:33 +01:00
|
|
|
const WON_TIMEOUT = 3000;
|
2021-07-10 21:00:30 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
const GameState = {
|
|
|
|
HUMAN_TURN: {
|
|
|
|
THINKING: "human_turn.thinking",
|
|
|
|
FROM_SELECTED: "human_turn.from_selected"
|
|
|
|
},
|
|
|
|
AI_TURN: "ai_turn"
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////
|
|
|
|
// GAME STUFF
|
|
|
|
//////////////////
|
|
|
|
|
|
|
|
init_wasm();
|
|
|
|
|
|
|
|
// let board = new Board(BOARD_WIDTH, BOARD_HEIGHT, Team.Black);
|
|
|
|
|
2021-07-09 22:29:09 +01:00
|
|
|
const statusText = document.getElementById("status-p");
|
2021-07-10 21:00:30 +01:00
|
|
|
const statusAlert = document.getElementById("status-d");
|
|
|
|
const teamText = document.getElementById("team-p");
|
2021-07-12 23:46:07 +01:00
|
|
|
const nodeCountText = document.getElementById("node-count");
|
2021-07-13 12:07:33 +01:00
|
|
|
const winningText = document.getElementById("winning-p");
|
2021-07-10 21:00:30 +01:00
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
const startBtn = document.getElementById("startBtn");
|
|
|
|
startBtn.onclick = start_game;
|
2021-07-10 21:00:30 +01:00
|
|
|
|
2021-07-13 12:07:33 +01:00
|
|
|
let wonTimeout = null;
|
2021-07-10 21:00:30 +01:00
|
|
|
let statusTimeout = null;
|
|
|
|
let setStatus = setStatusAlert;
|
2021-07-09 22:29:09 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
let current_state = GameState.HUMAN_TURN.THINKING;
|
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
let game = null;
|
|
|
|
let painter = null;
|
2021-07-10 19:10:48 +01:00
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
let clicks = [];
|
2021-07-02 16:53:03 +01:00
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
start_game();
|
2021-07-10 21:00:30 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
/////////////////
|
|
|
|
// CANVAS
|
|
|
|
/////////////////
|
|
|
|
|
2021-06-25 18:02:45 +01:00
|
|
|
const canvas = document.getElementById("game-canvas");
|
2021-07-02 13:48:07 +01:00
|
|
|
canvas.addEventListener("click", (event) => {
|
|
|
|
var mousepos = getMousePos(canvas, event);
|
|
|
|
// console.log(mousepos);
|
2021-07-02 16:53:03 +01:00
|
|
|
var cell = new BrdIdx(
|
2021-07-12 23:46:07 +01:00
|
|
|
Math.floor((mousepos.y / canvas.clientHeight) * BOARD_HEIGHT),
|
|
|
|
Math.floor((mousepos.x / canvas.clientWidth) * BOARD_WIDTH),
|
2021-07-02 16:53:03 +01:00
|
|
|
);
|
|
|
|
// console.log(cell);
|
|
|
|
process_canvas_click(cell);
|
2021-07-12 23:46:07 +01:00
|
|
|
});
|
2021-07-02 13:48:07 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
////////////////
|
|
|
|
// FUNCS
|
|
|
|
////////////////
|
|
|
|
|
|
|
|
function start_game() {
|
2021-07-12 23:46:07 +01:00
|
|
|
game = new Game(BOARD_WIDTH, BOARD_HEIGHT, PIECE_ROWS, Team.Black, SEARCH_DEPTH);
|
2021-07-10 23:54:05 +01:00
|
|
|
painter = new Painter(CANVAS_WIDTH, CANVAS_HEIGHT, "game-canvas");
|
2024-02-01 17:51:05 +00:00
|
|
|
// game.set_painter(painter);
|
|
|
|
// game.draw();
|
|
|
|
painter.draw_current(game);
|
2021-07-02 16:53:03 +01:00
|
|
|
|
2021-07-13 12:07:33 +01:00
|
|
|
clearInterval(wonTimeout);
|
2021-07-10 21:00:30 +01:00
|
|
|
updateTeamText();
|
2021-07-13 12:07:33 +01:00
|
|
|
updateWinningText();
|
2021-07-12 23:46:07 +01:00
|
|
|
clicks = [];
|
2021-07-02 16:53:03 +01:00
|
|
|
current_state = GameState.HUMAN_TURN.THINKING;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_canvas_click(cell_coord) {
|
2021-07-10 19:10:48 +01:00
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
switch(current_state) {
|
2021-07-10 23:54:05 +01:00
|
|
|
// first click of a move
|
2021-07-02 16:53:03 +01:00
|
|
|
case GameState.HUMAN_TURN.THINKING:
|
2021-07-10 21:00:30 +01:00
|
|
|
if (game.current_cell_state(cell_coord).state != SquareState.Occupied ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (game.current_cell_state(cell_coord).occupant.team != game.current_turn() ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
// console.log("Your turn, first piece picked");
|
2021-07-02 16:53:03 +01:00
|
|
|
|
2021-07-10 19:10:48 +01:00
|
|
|
clicks.push(cell_coord);
|
2021-07-10 23:54:05 +01:00
|
|
|
current_state = GameState.HUMAN_TURN.FROM_SELECTED;
|
|
|
|
game.set_selected(cell_coord);
|
2024-02-01 17:51:05 +00:00
|
|
|
painter.set_selected(cell_coord);
|
|
|
|
// game.draw();
|
|
|
|
painter.draw_current(game);
|
|
|
|
|
2021-07-02 16:53:03 +01:00
|
|
|
break;
|
2021-07-10 23:54:05 +01:00
|
|
|
|
|
|
|
// second click of a move
|
2021-07-02 16:53:03 +01:00
|
|
|
case GameState.HUMAN_TURN.FROM_SELECTED:
|
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
// second click is different to first, process as move
|
|
|
|
// otherwise, will skip straight to clean up (clear selected and clicks)
|
|
|
|
if (!clicks[0].eq(cell_coord)) {
|
2021-07-10 19:10:48 +01:00
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
if (game.current_cell_state(cell_coord).state != SquareState.Empty ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// console.log("Your turn, first piece already picked, picking second");
|
|
|
|
|
|
|
|
clicks.push(cell_coord);
|
|
|
|
|
|
|
|
if (clicks.length != 2) {
|
|
|
|
setStatus(`Error: wrong number of clicks to process ${clicks.length}`);
|
|
|
|
console.error(`Error: wrong number of clicks to process ${clicks.length}`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2021-07-09 22:29:09 +01:00
|
|
|
|
2021-07-10 19:10:48 +01:00
|
|
|
let status = game.make_move(clicks[0], clicks[1]);
|
|
|
|
|
|
|
|
switch(status) {
|
|
|
|
case Moveable.Allowed:
|
2021-07-12 23:46:07 +01:00
|
|
|
|
2021-07-14 14:08:54 +01:00
|
|
|
if (aiCheckBox.checked && game.has_won() === undefined) {
|
2022-05-05 23:02:04 +01:00
|
|
|
|
|
|
|
let start = performance.now();
|
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
game.ai_move();
|
2022-05-05 23:02:04 +01:00
|
|
|
|
|
|
|
let end = performance.now();
|
|
|
|
|
|
|
|
nodeCountText.innerText = `searched ${game.last_node_count.toLocaleString("en-GB")} possible moves in ${(end - start).toLocaleString()}ms`;
|
2021-07-12 23:46:07 +01:00
|
|
|
}
|
|
|
|
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.IllegalTrajectory:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("You can't move like that!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.JumpingSameTeam:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("You can't jump your own piece!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.NoJumpablePiece:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("There's nothing to jump!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.OccupiedDest:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("There's a piece there!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.OutOfBounds:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("That square's not on the board! (how have you managed that?)");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.UnoccupiedSrc:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("There's no piece to move!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.Unplayable:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("That's not a playable square!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
|
|
|
case Moveable.WrongTeamSrc:
|
2021-07-10 21:00:30 +01:00
|
|
|
setStatus("That's not your piece!");
|
2021-07-10 19:10:48 +01:00
|
|
|
break;
|
2021-07-09 22:29:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-07-10 23:54:05 +01:00
|
|
|
game.clear_selected();
|
2024-02-01 17:51:05 +00:00
|
|
|
painter.clear_selected();
|
|
|
|
// game.draw();
|
|
|
|
painter.draw_current(game);
|
2021-07-10 19:10:48 +01:00
|
|
|
clicks = [];
|
2021-07-02 16:53:03 +01:00
|
|
|
current_state = GameState.HUMAN_TURN.THINKING;
|
|
|
|
|
|
|
|
break;
|
|
|
|
case GameState.AI_TURN:
|
|
|
|
console.log("It's the AI's turn!");
|
|
|
|
break;
|
|
|
|
}
|
2021-07-10 21:00:30 +01:00
|
|
|
|
|
|
|
updateTeamText();
|
2021-07-13 12:07:33 +01:00
|
|
|
updateWinningText();
|
|
|
|
checkWon();
|
2021-07-02 16:53:03 +01:00
|
|
|
}
|
|
|
|
|
2021-07-02 13:48:07 +01:00
|
|
|
function getMousePos(canvas, evt) {
|
|
|
|
var rect = canvas.getBoundingClientRect();
|
|
|
|
return {
|
|
|
|
x: evt.clientX - rect.left,
|
|
|
|
y: evt.clientY - rect.top
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-10 21:00:30 +01:00
|
|
|
function setStatusText(txt, hide = true) {
|
|
|
|
if(statusTimeout != null) {
|
|
|
|
clearInterval(statusTimeout);
|
|
|
|
}
|
|
|
|
|
2021-07-09 22:29:09 +01:00
|
|
|
statusText.innerText = txt;
|
2021-07-10 21:00:30 +01:00
|
|
|
|
|
|
|
if(hide) {
|
|
|
|
statusTimeout = setTimeout(() => {
|
|
|
|
statusText.innerText = "";
|
|
|
|
}, STATUS_TIMEOUT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setStatusAlert(txt, alertType = "danger", hide = true) {
|
|
|
|
if(statusTimeout != null) {
|
|
|
|
clearInterval(statusTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
statusAlert.className = `alert alert-${alertType}`;
|
|
|
|
statusAlert.innerText = txt;
|
|
|
|
statusAlert.hidden = false;
|
|
|
|
|
|
|
|
if(hide) {
|
|
|
|
statusTimeout = setTimeout(() => {
|
|
|
|
statusAlert.hidden = true;
|
|
|
|
}, STATUS_TIMEOUT);
|
|
|
|
}
|
2021-07-09 22:29:09 +01:00
|
|
|
}
|
|
|
|
|
2021-07-10 21:00:30 +01:00
|
|
|
function updateTeamText(){
|
|
|
|
let team = game.current_turn();
|
|
|
|
switch(team) {
|
|
|
|
case Team.White:
|
|
|
|
teamText.innerText = "⚪ White ⚪";
|
|
|
|
break;
|
|
|
|
case Team.Black:
|
|
|
|
teamText.innerText = "🔴 Black 🔴";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-07-12 23:46:07 +01:00
|
|
|
|
2021-07-13 12:07:33 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
////////////////
|
|
|
|
// 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);
|
|
|
|
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);
|
2021-07-13 12:07:33 +01:00
|
|
|
|
|
|
|
if(SEARCH_DEPTH > 4) {
|
|
|
|
setStatus("This increases thinking time exponentially, be careful (probably don't go past 6)", "warning");
|
|
|
|
}
|
2021-07-12 23:46:07 +01:00
|
|
|
}
|
|
|
|
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 = () => {
|
2021-07-13 12:07:33 +01:00
|
|
|
|
2021-07-12 23:46:07 +01:00
|
|
|
}
|
|
|
|
aiCheckBox.onchange = onAICheck;
|
2021-07-14 14:08:54 +01:00
|
|
|
// aiCheckBox.checked = true;
|
|
|
|
|
|
|
|
const aiPerfectChance = document.getElementById("ai_difficulty");
|
|
|
|
/**
|
|
|
|
* Handler for piece rows input box change, start a new game
|
|
|
|
*/
|
|
|
|
const onPerfectChance = () => {
|
|
|
|
|
|
|
|
PERFECT_CHANCE = parseInt(aiPerfectChance.value) / 100;
|
|
|
|
game.set_perfect_chance(PERFECT_CHANCE);
|
|
|
|
}
|
|
|
|
aiPerfectChance.onchange = onPerfectChance;
|
|
|
|
aiPerfectChance.value = 50;
|