first commit
This commit is contained in:
46
packages/draughts/computer/alpha-beta-search.js
Normal file
46
packages/draughts/computer/alpha-beta-search.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { quiescenceSearch } from './quiescence-search';
|
||||
import { GameStates } from '@draughts/core';
|
||||
|
||||
const getShuffledArray = (arr) => {
|
||||
const newArr = [...arr];
|
||||
for (let i = newArr.length - 1; i > 0; i--) {
|
||||
const rand = Math.floor(Math.random() * (i + 1));
|
||||
[newArr[i], newArr[rand]] = [newArr[rand], newArr[i]];
|
||||
}
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export function alphaBetaMove(board, depth) {
|
||||
let recordE = Number.NEGATIVE_INFINITY;
|
||||
let recordMove = null;
|
||||
|
||||
for (const move of getShuffledArray(board.moves)) {
|
||||
const nextBoard = board.doMove(move);
|
||||
const e = -alphaBetaSearch(
|
||||
nextBoard,
|
||||
depth - 1,
|
||||
Number.NEGATIVE_INFINITY,
|
||||
Number.POSITIVE_INFINITY
|
||||
);
|
||||
if (e >= recordE) {
|
||||
recordE = e;
|
||||
recordMove = move;
|
||||
}
|
||||
}
|
||||
|
||||
return recordMove;
|
||||
}
|
||||
|
||||
export function alphaBetaSearch(board, depth, alpha, beta) {
|
||||
if (depth === 0 || board.state !== GameStates.PLAYING)
|
||||
return quiescenceSearch(board, alpha, beta);
|
||||
|
||||
for (const move of board.moves) {
|
||||
const nextBoard = board.doMove(move);
|
||||
const e = -alphaBetaSearch(nextBoard, depth - 1, -beta, -alpha);
|
||||
if (e >= beta) return beta;
|
||||
alpha = Math.max(e, alpha);
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
101
packages/draughts/computer/evaluate-board.js
Normal file
101
packages/draughts/computer/evaluate-board.js
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
eachCell,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
BOARD_SIZE,
|
||||
Players,
|
||||
GameStates,
|
||||
} from '@draughts/core';
|
||||
|
||||
const winnerMap = {
|
||||
[GameStates.WHITE_WON]: Players.WHITE,
|
||||
[GameStates.BLACK_WON]: Players.BLACK,
|
||||
};
|
||||
|
||||
export function evaluateBoard(board) {
|
||||
if (board.state === GameStates.DRAW) {
|
||||
return 0;
|
||||
}
|
||||
if (board.state !== GameStates.PLAYING) {
|
||||
const winner = winnerMap[board.state];
|
||||
return board.playerToMove === winner
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
if (isEndgame(board)) {
|
||||
return evaluateEndgamePosition(board);
|
||||
}
|
||||
return evaluateRegularPosition(board);
|
||||
}
|
||||
|
||||
function isEndgame(board) {
|
||||
for (const { piece } of eachCell(board.position)) {
|
||||
if (!pieceIsPlayer(piece, Players.NONE) && !pieceIsQueen(piece)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function evaluateEndgamePosition(board) {
|
||||
let playerPieces = 0;
|
||||
let opponentPieces = 0;
|
||||
let distances = 0;
|
||||
for (const { cell, piece } of eachCell(board.position)) {
|
||||
if (pieceIsPlayer(piece, board.playerToMove)) {
|
||||
playerPieces += 1;
|
||||
distances += calculateDistances(board, cell);
|
||||
} else if (!pieceIsPlayer(piece, Players.NONE)) {
|
||||
opponentPieces += 1;
|
||||
}
|
||||
}
|
||||
if (playerPieces >= opponentPieces) {
|
||||
return -distances;
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
|
||||
function evaluateRegularPosition(board) {
|
||||
let e = 0;
|
||||
for (const { cell, piece } of eachCell(board.position)) {
|
||||
const pieceEvaluation = evaluatePiece(cell, piece);
|
||||
if (pieceIsPlayer(piece, board.playerToMove)) {
|
||||
e += pieceEvaluation;
|
||||
} else if (!pieceIsPlayer(piece, Players.NONE)) {
|
||||
e -= pieceEvaluation;
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function evaluatePiece(cell, piece) {
|
||||
let e = 20;
|
||||
if (pieceIsQueen(piece)) {
|
||||
e += 40;
|
||||
} else if (pieceIsPlayer(piece, Players.WHITE)) {
|
||||
e += BOARD_SIZE - 1 - cell.row;
|
||||
} else if (pieceIsPlayer(piece, Players.BLACK)) {
|
||||
e += cell.row;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function calculateDistances(board, baseCell) {
|
||||
let distances = 0;
|
||||
for (const { cell, piece } of eachCell(board.position)) {
|
||||
if (
|
||||
!pieceIsPlayer(piece, board.playerToMove) &&
|
||||
!pieceIsPlayer(piece, Players.NONE)
|
||||
) {
|
||||
distances += euclideanDistance(baseCell, cell);
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
|
||||
function euclideanDistance(a, b) {
|
||||
const rowSquare = Math.pow(a.row - b.row, 2);
|
||||
const colSquare = Math.pow(a.col - b.col, 2);
|
||||
const distanceFloat = Math.sqrt(rowSquare + colSquare);
|
||||
return Math.ceil(distanceFloat);
|
||||
}
|
||||
1
packages/draughts/computer/index.js
Normal file
1
packages/draughts/computer/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { alphaBetaMove } from './alpha-beta-search';
|
||||
543
packages/draughts/computer/negascout-result.js
Normal file
543
packages/draughts/computer/negascout-result.js
Normal file
@ -0,0 +1,543 @@
|
||||
import {
|
||||
eachCell,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
BOARD_SIZE,
|
||||
Players,
|
||||
GameStates,
|
||||
} from '@draughts/core';
|
||||
|
||||
const winnerMap = {
|
||||
[GameStates.WHITE_WON]: Players.WHITE,
|
||||
[GameStates.BLACK_WON]: Players.BLACK,
|
||||
};
|
||||
|
||||
export function negascout_search(board) {
|
||||
if (board.state === GameStates.DRAW) {
|
||||
return 0;
|
||||
}
|
||||
if (board.state !== GameStates.PLAYING) {
|
||||
const winner = winnerMap[board.state];
|
||||
return board.playerToMove === winner
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
if (isEndgame(board)) {
|
||||
return evaluateEndgamePosition(board);
|
||||
}
|
||||
return evaluateRegularPosition(board);
|
||||
}
|
||||
|
||||
function eval_board(Board, pieceType, restrictions) {
|
||||
let score = 0;
|
||||
const min_r = restrictions[0];
|
||||
const min_c = restrictions[1];
|
||||
const max_r = restrictions[2];
|
||||
const max_c = restrictions[3];
|
||||
for (let row = min_r; row < max_r + 1; row++) {
|
||||
for (let column = min_c; column < max_c + 1; column++) {
|
||||
if (Board[row][column] == pieceType) {
|
||||
let block = 0;
|
||||
let piece = 1;
|
||||
// left
|
||||
if (column === 0 || Board[row][column - 1] !== 0) {
|
||||
block++;
|
||||
}
|
||||
// pieceNum
|
||||
for (column++; column < Columns && Board[row][column] === pieceType; column++) {
|
||||
piece++;
|
||||
}
|
||||
// right
|
||||
if (column === Columns || Board[row][column] !== 0) {
|
||||
block++;
|
||||
}
|
||||
score += evaluateblock(block, piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let column = min_c; column < max_c + 1; column++) {
|
||||
for (let row = min_r; row < max_r + 1; row++) {
|
||||
if (Board[row][column] == pieceType) {
|
||||
let block = 0;
|
||||
let piece = 1;
|
||||
// left
|
||||
if (row === 0 || Board[row - 1][column] !== 0) {
|
||||
block++;
|
||||
}
|
||||
// pieceNum
|
||||
for (row++; row < Rows && Board[row][column] === pieceType; row++) {
|
||||
piece++;
|
||||
}
|
||||
// right
|
||||
if (row === Rows || Board[row][column] !== 0) {
|
||||
block++;
|
||||
}
|
||||
score += evaluateblock(block, piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let n = min_r; n < (max_c - min_c + max_r); n += 1) {
|
||||
let r = n;
|
||||
let c = min_c;
|
||||
while (r >= min_r && c <= max_c) {
|
||||
if (r <= max_r) {
|
||||
if (Board[r][c] === pieceType) {
|
||||
let block = 0;
|
||||
let piece = 1;
|
||||
// left
|
||||
if (c === 0 || r === Rows - 1 || Board[r + 1][c - 1] !== 0) {
|
||||
block++;
|
||||
}
|
||||
// pieceNum
|
||||
r--;
|
||||
c++;
|
||||
for (; r >= 0 && Board[r][c] === pieceType; r--) {
|
||||
piece++;
|
||||
c++
|
||||
}
|
||||
// right
|
||||
if (r < 0 || c === Columns || Board[r][c] !== 0) {
|
||||
block++;
|
||||
}
|
||||
score += evaluateblock(block, piece);
|
||||
}
|
||||
}
|
||||
r -= 1;
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let n = min_r - (max_c - min_c); n <= max_r; n++) {
|
||||
let r = n;
|
||||
let c = min_c;
|
||||
while (r <= max_r && c <= max_c) {
|
||||
if (r >= min_r && r <= max_r) {
|
||||
if (Board[r][c] === pieceType) {
|
||||
let block = 0;
|
||||
let piece = 1;
|
||||
// left
|
||||
if (c === 0 || r === 0 || Board[r - 1][c - 1] !== 0) {
|
||||
block++;
|
||||
}
|
||||
// pieceNum
|
||||
r++;
|
||||
c++;
|
||||
for (; r < Rows && Board[r][c] == pieceType; r++) {
|
||||
piece++;
|
||||
c++;
|
||||
}
|
||||
// right
|
||||
if (r === Rows || c === Columns || Board[r][c] !== 0) {
|
||||
block++;
|
||||
}
|
||||
score += evaluateblock(block, piece);
|
||||
}
|
||||
}
|
||||
r += 1;
|
||||
c += 1;
|
||||
}
|
||||
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function evaluateblock(blocks, pieces) {
|
||||
if (blocks === 0) {
|
||||
switch (pieces) {
|
||||
case 1:
|
||||
return LiveOne;
|
||||
case 2:
|
||||
return LiveTwo;
|
||||
case 3:
|
||||
return LiveThree;
|
||||
case 4:
|
||||
return LiveFour;
|
||||
default:
|
||||
return Five;
|
||||
}
|
||||
} else if (blocks === 1) {
|
||||
switch (pieces) {
|
||||
case 1:
|
||||
return DeadOne;
|
||||
case 2:
|
||||
return DeadTwo;
|
||||
case 3:
|
||||
return DeadThree;
|
||||
case 4:
|
||||
return DeadFour;
|
||||
default:
|
||||
return Five;
|
||||
}
|
||||
} else {
|
||||
if (pieces >= 5) {
|
||||
return Five;
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function check_directions(arr) {
|
||||
for (let i = 0; i < arr.length - 4; i++) {
|
||||
if (arr[i] !== 0) {
|
||||
if (arr[i] === arr[i + 1] && arr[i] === arr[i + 2] && arr[i] === arr[i + 3] && arr[i] === arr[i + 4]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function get_directions(Board, x, y) {
|
||||
const Directions = [[],[],[],[]];
|
||||
for (let i = -4; i < 5; i++) {
|
||||
if (x + i >= 0 && x + i <= Rows - 1) {
|
||||
Directions[0].push(Board[x + i][y])
|
||||
if (y + i >= 0 && y + i <= Columns - 1) {
|
||||
Directions[2].push(Board[x + i][y + i])
|
||||
}
|
||||
}
|
||||
if (y + i >= 0 && y + i <= Columns - 1) {
|
||||
Directions[1].push(Board[x][y + i])
|
||||
if (x - i >= 0 && x - i <= Rows - 1) {
|
||||
Directions[3].push(Board[x - i][y + i])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Directions
|
||||
}
|
||||
|
||||
function checkwin(Board, x, y) {
|
||||
const Directions = get_directions(Board, x, y)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (check_directions(Directions[i])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function remoteCell(Board, r, c) {
|
||||
for (let i = r - 2; i <= r + 2; i++) {
|
||||
if (i < 0 || i >= Rows) continue;
|
||||
for (let j = c - 2; j <= c + 2; j++) {
|
||||
if (j < 0 || j >= Columns) continue;
|
||||
if (Board[i][j] !== 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function Get_restrictions(Board) {
|
||||
let min_r = Infinity;
|
||||
let min_c = Infinity;
|
||||
let max_r = -Infinity;
|
||||
let max_c = -Infinity;
|
||||
for (let i = 0; i < Rows; i++) {
|
||||
for (let j = 0; j < Columns; j++) {
|
||||
if (Board[i][j] !== 0) {
|
||||
min_r = Math.min(min_r, i)
|
||||
min_c = Math.min(min_c, j)
|
||||
max_r = Math.max(max_r, i)
|
||||
max_c = Math.max(max_c, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (min_r - 2 < 0) {
|
||||
min_r = 2;
|
||||
}
|
||||
if (min_c - 2 < 0) {
|
||||
min_c = 2;
|
||||
}
|
||||
if (max_r + 2 >= Rows) {
|
||||
max_r = Rows - 3;
|
||||
}
|
||||
if (max_c + 2 >= Columns) {
|
||||
max_c = Columns - 3;
|
||||
}
|
||||
return [min_r, min_c, max_r, max_c]
|
||||
}
|
||||
|
||||
function Change_restrictions(restrictions, i, j) {
|
||||
let min_r = restrictions[0];
|
||||
let min_c = restrictions[1];
|
||||
let max_r = restrictions[2];
|
||||
let max_c = restrictions[3];
|
||||
if (i < min_r) {
|
||||
min_r = i
|
||||
} else if (i > max_r) {
|
||||
max_r = i
|
||||
}
|
||||
if (j < min_c) {
|
||||
min_c = j
|
||||
} else if (j > max_c) {
|
||||
max_c = j
|
||||
}
|
||||
if (min_r - 2 < 0) {
|
||||
min_r = 2;
|
||||
}
|
||||
if (min_c - 2 < 0) {
|
||||
min_c = 2;
|
||||
}
|
||||
if (max_r + 2 >= Rows) {
|
||||
max_r = Rows - 3;
|
||||
}
|
||||
if (max_c + 2 >= Columns) {
|
||||
max_c = Columns - 3;
|
||||
}
|
||||
return [min_r, min_c, max_r, max_c]
|
||||
}
|
||||
|
||||
function compare(a, b) {
|
||||
if (a.score < b.score)
|
||||
return 1;
|
||||
if (a.score > b.score)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function BoardGenerator(restrictions, Board, player) {
|
||||
const availSpots_score = []; //c is j r is i;
|
||||
const min_r = restrictions[0];
|
||||
const min_c = restrictions[1];
|
||||
const max_r = restrictions[2];
|
||||
const max_c = restrictions[3];;
|
||||
for (let i = min_r - 2; i <= max_r + 2; i++) {
|
||||
for (let j = min_c - 2; j <= max_c + 2; j++) {
|
||||
if (Board[i][j] === 0 && !remoteCell(Board, i, j)) {
|
||||
const move = {}
|
||||
move.i = i;
|
||||
move.j = j;
|
||||
move.score = evaluate_move(Board, i, j, player)
|
||||
if (move.score === WIN_DETECTED) {
|
||||
return [move]
|
||||
}
|
||||
availSpots_score.push(move)
|
||||
}
|
||||
}
|
||||
}
|
||||
availSpots_score.sort(compare);
|
||||
// return availSpots_score.slice(0,20)
|
||||
return availSpots_score;
|
||||
}
|
||||
|
||||
function evaluate_direction(direction_arr, player) {
|
||||
let score = 0;
|
||||
for (let i = 0;(i + 4) < direction_arr.length; i++) {
|
||||
let you = 0;
|
||||
let enemy = 0;
|
||||
for (let j = 0; j <= 4; j++) {
|
||||
if (direction_arr[i + j] === player) {
|
||||
you++
|
||||
} else if (direction_arr[i + j] === -player) {
|
||||
enemy++
|
||||
}
|
||||
}
|
||||
score += evalff(get_seq(you, enemy));
|
||||
if ((score >= 800000)) {
|
||||
return WIN_DETECTED;
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
function get_seq(y, e) {
|
||||
if (y + e === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (y !== 0 && e === 0) {
|
||||
return y
|
||||
}
|
||||
if (y === 0 && e !== 0) {
|
||||
return -e
|
||||
}
|
||||
if (y !== 0 && e !== 0) {
|
||||
return 17
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate_move(Board, x, y, player) {
|
||||
let score = 0;
|
||||
const Directions = get_directions(Board, x, y);
|
||||
let temp_score;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
temp_score = evaluate_direction(Directions[i], player);
|
||||
if (temp_score === WIN_DETECTED) {
|
||||
return WIN_DETECTED
|
||||
} else {
|
||||
score += temp_score
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
function evaluate_state(Board, player, hash, restrictions) {
|
||||
const black_score = eval_board(Board, -1, restrictions);
|
||||
const white_score = eval_board(Board, 1, restrictions);
|
||||
let score = 0;
|
||||
if (player == -1) {
|
||||
score = (black_score - white_score);
|
||||
} else {
|
||||
score = (white_score - black_score);
|
||||
}
|
||||
StateCache.set(hash,score);
|
||||
StateCachePuts++;
|
||||
return score;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function random32() {
|
||||
let o = new Uint32Array(1);
|
||||
self.crypto.getRandomValues(o);
|
||||
return o[0];
|
||||
}
|
||||
|
||||
function Table_init() {
|
||||
for (let i = 0; i < Rows; i++) {
|
||||
Table[i] = [];
|
||||
for (let j = 0; j < Columns; j++) {
|
||||
Table[i][j] = []
|
||||
Table[i][j][0] = random32(); //1
|
||||
Table[i][j][1] = random32(); //2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function hash(board) {
|
||||
let h = 0;
|
||||
let p;
|
||||
for (let i = 0; i < Rows; i++) {
|
||||
for (let j = 0; j < Columns; j++) {
|
||||
let Board_value = board[i][j];
|
||||
if (Board_value !== 0) {
|
||||
if (Board_value === -1) {
|
||||
p = 0
|
||||
} else {
|
||||
p = 1
|
||||
}
|
||||
h = h ^ Table[i][j][p];
|
||||
}
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function update_hash(hash, player, row, col) {
|
||||
if (player === -1) {
|
||||
player = 0
|
||||
} else {
|
||||
player = 1
|
||||
}
|
||||
hash = hash ^ Table[row][col][player];
|
||||
return hash
|
||||
}
|
||||
|
||||
|
||||
function negascout(newBoard, player, depth, alpha, beta, hash, restrictions, last_i, last_j) {
|
||||
const alphaOrig = alpha;
|
||||
const CacheNode =Cache.get(hash)
|
||||
if ((CacheNode !== undefined) && (CacheNode.depth >= depth)) {
|
||||
CacheHits++;
|
||||
const score = CacheNode.score;
|
||||
if (CacheNode.Flag === 0) {
|
||||
CacheCutoffs++;
|
||||
return score
|
||||
}
|
||||
if (CacheNode.Flag === -1) {
|
||||
alpha = Math.max(alpha, score);
|
||||
} else if (CacheNode.Flag === 1) {
|
||||
beta = Math.min(beta, score);
|
||||
}
|
||||
if (alpha >= beta) {
|
||||
CacheCutoffs++
|
||||
return score
|
||||
}
|
||||
}
|
||||
fc++
|
||||
|
||||
if (checkwin(newBoard, last_i, last_j)) {
|
||||
return -2000000 + (MaximumDepth - depth)
|
||||
}
|
||||
if (depth === 0) {
|
||||
const StateCacheNode=StateCache.get(hash);
|
||||
if (StateCacheNode !== undefined) {
|
||||
StateCacheHits++
|
||||
return StateCacheNode
|
||||
}
|
||||
return evaluate_state(newBoard, player, hash, restrictions)
|
||||
}
|
||||
|
||||
const availSpots = BoardGenerator(restrictions, newBoard, player);
|
||||
if (availSpots.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let b = beta;
|
||||
let bestscore = -Infinity;
|
||||
const bestMove={};
|
||||
for (let y = 0; y < availSpots.length; y++) {
|
||||
let i = availSpots[y].i;
|
||||
let j = availSpots[y].j;
|
||||
const newHash = update_hash(hash, player, i, j)
|
||||
newBoard[i][j] = player;
|
||||
const restrictions_temp = Change_restrictions(restrictions, i, j)
|
||||
let score = -negascout(newBoard, -player, depth - 1, -b, -alpha, newHash, restrictions_temp, i, j)
|
||||
if (score > alpha && score < beta && y > 0) {
|
||||
score = -negascout(newBoard, -player, depth - 1, -beta, -score, newHash, restrictions_temp, i, j)
|
||||
}
|
||||
if (score > bestscore) {
|
||||
bestscore = score
|
||||
if (depth === MaximumDepth) {
|
||||
bestMove.i=i
|
||||
bestMove.j=j
|
||||
bestMove.score=score;
|
||||
}
|
||||
}
|
||||
newBoard[i][j] = 0;
|
||||
alpha = Math.max(alpha, score)
|
||||
if (alpha >= beta) {
|
||||
break;
|
||||
}
|
||||
b = alpha + 1;
|
||||
}
|
||||
CachePuts++
|
||||
const obj={score: bestscore,depth:depth};
|
||||
if (bestscore <= alphaOrig) {
|
||||
obj.Flag = 1
|
||||
} else if (bestscore >= b) {
|
||||
obj.Flag = -1
|
||||
} else {
|
||||
obj.Flag = 0
|
||||
}
|
||||
Cache.set(hash,obj);
|
||||
if (depth == MaximumDepth) {
|
||||
return bestMove
|
||||
} else {
|
||||
return bestscore
|
||||
}
|
||||
}
|
||||
|
||||
function iterative_negascout(player, Board, depth) {
|
||||
let bestmove;
|
||||
let i = 2;
|
||||
while (i !== depth + 2) {
|
||||
MaximumDepth = i;
|
||||
bestmove = negascout(Board, player, MaximumDepth, -Infinity, Infinity, hash(Board), Get_restrictions(Board), 0, 0)
|
||||
// Set_last_best(bestmove)
|
||||
console.log(MaximumDepth)
|
||||
console.log(bestmove)
|
||||
let t11 = performance.now();
|
||||
console.log((t11 - t00) / 1000)
|
||||
if (bestmove.score > 1999900) {
|
||||
break;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
return bestmove
|
||||
}
|
||||
20
packages/draughts/computer/quiescence-search.js
Normal file
20
packages/draughts/computer/quiescence-search.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { evaluateBoard } from './evaluate-board';
|
||||
|
||||
export function quiescenceSearch(board, alpha, beta) {
|
||||
const baseE = evaluateBoard(board);
|
||||
|
||||
if (baseE >= beta) return beta;
|
||||
alpha = Math.max(baseE, alpha);
|
||||
|
||||
for (const move of board.moves) {
|
||||
if (move.captures.length <= 0) continue;
|
||||
|
||||
const nextBoard = board.doMove(move);
|
||||
const e = -quiescenceSearch(nextBoard, -beta, -alpha);
|
||||
|
||||
if (e >= beta) return beta;
|
||||
alpha = Math.max(e, alpha);
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
178
packages/draughts/core/board.js
Normal file
178
packages/draughts/core/board.js
Normal file
@ -0,0 +1,178 @@
|
||||
import {
|
||||
cellIsEmpty,
|
||||
queenPiece,
|
||||
eachCell,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
shouldQueen,
|
||||
clonePosition,
|
||||
filterToLongestCaptures,
|
||||
cellWithinBounds,
|
||||
} from './utilities';
|
||||
import { Players, Pieces, GameStates } from '@draughts/core';
|
||||
|
||||
export class Board {
|
||||
constructor(position, playerToMove, firstMove = true) {
|
||||
this.position = position;
|
||||
this.playerToMove = playerToMove;
|
||||
this.firstMove = firstMove;
|
||||
|
||||
this.direction = this.playerToMove === Players.WHITE ? -1 : 1;
|
||||
this.moves = this._computeMoves();
|
||||
this.state = this._computeState();
|
||||
}
|
||||
|
||||
_computeState() {
|
||||
if (this.moves.length > 0) {
|
||||
return GameStates.PLAYING;
|
||||
}
|
||||
|
||||
let blackPieces = 0;
|
||||
let whitePieces = 0;
|
||||
|
||||
for (const { piece } of eachCell(this.position)) {
|
||||
if (pieceIsPlayer(piece, Players.WHITE)) {
|
||||
whitePieces = whitePieces + 1;
|
||||
} else if (pieceIsPlayer(piece, Players.BLACK)) {
|
||||
blackPieces = blackPieces + 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.playerToMove = Players.NONE;
|
||||
|
||||
if (blackPieces === 0) {
|
||||
return GameStates.WHITE_WON;
|
||||
}
|
||||
|
||||
if (whitePieces === 0) {
|
||||
return GameStates.BLACK_WON;
|
||||
}
|
||||
|
||||
return GameStates.DRAW;
|
||||
}
|
||||
|
||||
_computeMoves() {
|
||||
const starts = this._getStarts();
|
||||
const captures = this._getCaptures(starts);
|
||||
if (captures.length > 0) return captures;
|
||||
return this._getMoves(starts);
|
||||
}
|
||||
|
||||
_getStarts() {
|
||||
const starts = [];
|
||||
for (const { cell, piece } of eachCell(this.position)) {
|
||||
if (pieceIsPlayer(piece, this.playerToMove)) {
|
||||
starts.push(cell);
|
||||
}
|
||||
}
|
||||
return starts;
|
||||
}
|
||||
|
||||
_getCaptures(starts) {
|
||||
let captures = [];
|
||||
for (const start of starts) {
|
||||
captures = [
|
||||
...captures,
|
||||
...this._getNextCaptures(this.position, {
|
||||
captures: [],
|
||||
path: [start],
|
||||
}),
|
||||
];
|
||||
}
|
||||
return filterToLongestCaptures(captures);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
_getNextCaptures(initialPosition, move) {
|
||||
const captures = [];
|
||||
const start = move.path.at(-1);
|
||||
const piece = initialPosition[start.row][start.col];
|
||||
const possibleDRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||
|
||||
for (const dRow of possibleDRow) {
|
||||
for (const dCol of [-1, 1]) {
|
||||
const middle = {
|
||||
col: start.col + dCol * this.direction,
|
||||
row: start.row + dRow * this.direction,
|
||||
};
|
||||
const end = {
|
||||
col: middle.col + dCol * this.direction,
|
||||
row: middle.row + dRow * this.direction,
|
||||
};
|
||||
|
||||
if (!cellWithinBounds(end) || !cellIsEmpty(initialPosition, end))
|
||||
continue;
|
||||
|
||||
const middlePiece = initialPosition[middle.row][middle.col];
|
||||
if (
|
||||
pieceIsPlayer(middlePiece, Players.NONE) ||
|
||||
pieceIsPlayer(middlePiece, this.playerToMove)
|
||||
)
|
||||
continue;
|
||||
|
||||
const queened = shouldQueen(end, piece);
|
||||
const searchPosition = clonePosition(initialPosition);
|
||||
|
||||
searchPosition[start.row][start.col] = Pieces.NONE;
|
||||
searchPosition[middle.row][middle.col] = Pieces.NONE;
|
||||
searchPosition[end.row][end.col] = queened ? queenPiece(piece) : piece;
|
||||
|
||||
const nextMove = {
|
||||
captures: [...move.captures, middle],
|
||||
path: [...move.path, end],
|
||||
};
|
||||
|
||||
captures.push(nextMove);
|
||||
if (!queened)
|
||||
captures.push(...this._getNextCaptures(searchPosition, nextMove));
|
||||
}
|
||||
}
|
||||
|
||||
return filterToLongestCaptures(captures);
|
||||
}
|
||||
|
||||
_getMoves(starts) {
|
||||
const moves = [];
|
||||
|
||||
for (const start of starts) {
|
||||
const piece = this.position[start.row][start.col];
|
||||
const possibleDeltaRow = pieceIsQueen(piece) ? [-1, 1] : [1];
|
||||
for (const deltaRow of possibleDeltaRow) {
|
||||
for (const deltaCol of [-1, 1]) {
|
||||
const end = {
|
||||
col: start.col + deltaCol * this.direction,
|
||||
row: start.row + deltaRow * this.direction,
|
||||
};
|
||||
if (cellWithinBounds(end) && cellIsEmpty(this.position, end)) {
|
||||
moves.push({ captures: [], path: [start, end] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
|
||||
doMove({ path, captures }) {
|
||||
const start = path.at(0);
|
||||
const startPiece = this.position[start.row][start.col];
|
||||
const end = path.at(-1);
|
||||
|
||||
const endPiece = shouldQueen(end, startPiece)
|
||||
? queenPiece(startPiece)
|
||||
: startPiece;
|
||||
|
||||
const newPosition = clonePosition(this.position);
|
||||
newPosition[path.at(0).row][path.at(0).col] = Pieces.NONE;
|
||||
newPosition[end.row][end.col] = endPiece;
|
||||
|
||||
for (const capture of captures) {
|
||||
newPosition[capture.row][capture.col] = Pieces.NONE;
|
||||
}
|
||||
|
||||
const newPlayerToMove =
|
||||
this.playerToMove === Players.WHITE ? Players.BLACK : Players.WHITE;
|
||||
|
||||
return new Board(newPosition, newPlayerToMove, false);
|
||||
}
|
||||
}
|
||||
20
packages/draughts/core/constants/enums.js
Normal file
20
packages/draughts/core/constants/enums.js
Normal file
@ -0,0 +1,20 @@
|
||||
export const Pieces = {
|
||||
BLACK: 'b',
|
||||
BLACK_QUEEN: 'bq',
|
||||
NONE: '',
|
||||
WHITE: 'w',
|
||||
WHITE_QUEEN: 'wq',
|
||||
};
|
||||
|
||||
export const GameStates = {
|
||||
BLACK_WON: 'b',
|
||||
DRAW: 'd',
|
||||
PLAYING: 'p',
|
||||
WHITE_WON: 'w',
|
||||
};
|
||||
|
||||
export const Players = {
|
||||
BLACK: 'b',
|
||||
NONE: '',
|
||||
WHITE: 'w',
|
||||
};
|
||||
5
packages/draughts/core/constants/index.js
Normal file
5
packages/draughts/core/constants/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
export { Players, Pieces, GameStates } from './enums';
|
||||
|
||||
export { INITIAL_POSITION } from './positions';
|
||||
|
||||
export const BOARD_SIZE = 8;
|
||||
86
packages/draughts/core/constants/positions.js
Normal file
86
packages/draughts/core/constants/positions.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { Pieces } from './enums';
|
||||
|
||||
export const INITIAL_POSITION = [
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
],
|
||||
[
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
Pieces.NONE,
|
||||
Pieces.BLACK,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
|
||||
[
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
|
||||
[
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
],
|
||||
[
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
Pieces.WHITE,
|
||||
Pieces.NONE,
|
||||
],
|
||||
];
|
||||
22
packages/draughts/core/index.js
Normal file
22
packages/draughts/core/index.js
Normal file
@ -0,0 +1,22 @@
|
||||
export {
|
||||
Players,
|
||||
GameStates,
|
||||
Pieces,
|
||||
INITIAL_POSITION,
|
||||
BOARD_SIZE,
|
||||
} from './constants';
|
||||
|
||||
export { Board } from './board';
|
||||
|
||||
export {
|
||||
cellIsEmpty,
|
||||
queenPiece,
|
||||
eachCell,
|
||||
compareCells,
|
||||
pieceIsPlayer,
|
||||
pieceIsQueen,
|
||||
shouldQueen,
|
||||
clonePosition,
|
||||
filterToLongestCaptures,
|
||||
cellWithinBounds,
|
||||
} from './utilities';
|
||||
81
packages/draughts/core/utilities/index.js
Normal file
81
packages/draughts/core/utilities/index.js
Normal file
@ -0,0 +1,81 @@
|
||||
import { BOARD_SIZE, Pieces, Players } from '@draughts/core';
|
||||
|
||||
const isQueenMap = {
|
||||
[Pieces.NONE]: false,
|
||||
[Pieces.BLACK]: false,
|
||||
[Pieces.WHITE]: false,
|
||||
[Pieces.BLACK_QUEEN]: true,
|
||||
[Pieces.WHITE_QUEEN]: true,
|
||||
};
|
||||
|
||||
export const pieceIsQueen = (piece) => isQueenMap[piece];
|
||||
|
||||
const isPlayerMap = {
|
||||
[Players.NONE]: new Set([Pieces.NONE]),
|
||||
[Players.WHITE]: new Set([Pieces.WHITE, Pieces.WHITE_QUEEN]),
|
||||
[Players.BLACK]: new Set([Pieces.BLACK, Pieces.BLACK_QUEEN]),
|
||||
};
|
||||
|
||||
export const pieceIsPlayer = (piece, player) => {
|
||||
return isPlayerMap[player].has(piece);
|
||||
};
|
||||
|
||||
export const cellWithinBounds = ({ row, col }) =>
|
||||
row < BOARD_SIZE && row >= 0 && col < BOARD_SIZE && col >= 0;
|
||||
|
||||
export const cellIsEmpty = (position, { row, col }) =>
|
||||
position[row][col] === Pieces.NONE;
|
||||
|
||||
export function* eachCell(position) {
|
||||
for (const [row, rowArray] of position.entries()) {
|
||||
for (const [col, piece] of rowArray.entries()) {
|
||||
yield { cell: { col, row }, piece };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const crownQueenMap = {
|
||||
[Pieces.BLACK]: Pieces.BLACK_QUEEN,
|
||||
[Pieces.WHITE]: Pieces.WHITE_QUEEN,
|
||||
[Pieces.BLACK_QUEEN]: Pieces.BLACK_QUEEN,
|
||||
[Pieces.WHITE_QUEEN]: Pieces.WHITE_QUEEN,
|
||||
};
|
||||
|
||||
export const queenPiece = (piece) => crownQueenMap[piece];
|
||||
|
||||
export const shouldQueen = (cell, piece) => {
|
||||
if (pieceIsQueen(piece)) return false;
|
||||
return (
|
||||
(pieceIsPlayer(piece, Players.WHITE) && cell.row === 0) ||
|
||||
(pieceIsPlayer(piece, Players.BLACK) && cell.row === BOARD_SIZE - 1)
|
||||
);
|
||||
};
|
||||
|
||||
export const filterToLongestCaptures = (moves) => {
|
||||
let longestCaptures = [];
|
||||
let recordLength = 0;
|
||||
for (const move of moves) {
|
||||
if (move.captures.length > recordLength) {
|
||||
longestCaptures = [move];
|
||||
recordLength = move.captures.length;
|
||||
} else if (move.captures.length === recordLength) {
|
||||
longestCaptures.push(move);
|
||||
}
|
||||
}
|
||||
return longestCaptures;
|
||||
};
|
||||
|
||||
export const clonePosition = (position) => [...position.map((row) => [...row])];
|
||||
|
||||
export const compareCells = (cellOne, cellTwo) => {
|
||||
if (!cellOne || !cellTwo) return false;
|
||||
return cellOne.row === cellTwo.row && cellOne.col === cellTwo.col;
|
||||
};
|
||||
|
||||
const formatPlayerMap = {
|
||||
[Players.NONE]: 'none',
|
||||
[Players.WHITE]: 'white',
|
||||
[Players.BLACK]: 'black',
|
||||
};
|
||||
|
||||
export const formatPlayer = (player) => formatPlayerMap[player];
|
||||
Reference in New Issue
Block a user