xiangqii
Version:
xiangqi-engine written in pure js
2,125 lines (1,839 loc) • 55.8 kB
JavaScript
/************************************************\
================================================
WUKONG
javascript Xiang Qi engine
by
Code Monkey King
===============================================
\************************************************/
// Source Url: https://github.com/maksimKorzh/wukong-xiangqi
var Engine = function () {
// engine version
const VERSION = "1.0";
// starting position
const START_FEN =
"rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1";
// sides to move
const RED = 0;
const BLACK = 1;
const NO_COLOR = 2;
// piece encoding
const EMPTY = 0;
const RED_PAWN = 1;
const RED_ADVISOR = 2;
const RED_BISHOP = 3;
const RED_KNIGHT = 4;
const RED_CANNON = 5;
const RED_ROOK = 6;
const RED_KING = 7;
const BLACK_PAWN = 8;
const BLACK_ADVISOR = 9;
const BLACK_BISHOP = 10;
const BLACK_KNIGHT = 11;
const BLACK_CANNON = 12;
const BLACK_ROOK = 13;
const BLACK_KING = 14;
const OFFBOARD = 15;
// piece types
const PAWN = 16;
const ADVISOR = 17;
const BISHOP = 18;
const KNIGHT = 19;
const CANNON = 20;
const ROOK = 21;
const KING = 22;
// map type to piece
const PIECE_TYPE = [
0,
PAWN,
ADVISOR,
BISHOP,
KNIGHT,
CANNON,
ROOK,
KING,
PAWN,
ADVISOR,
BISHOP,
KNIGHT,
CANNON,
ROOK,
KING,
];
// map color to piece
const PIECE_COLOR = [
NO_COLOR,
RED,
RED,
RED,
RED,
RED,
RED,
RED,
BLACK,
BLACK,
BLACK,
BLACK,
BLACK,
BLACK,
BLACK,
];
// square encoding
const A9 = 23,
B9 = 24,
C9 = 25,
D9 = 26,
E9 = 27,
F9 = 28,
G9 = 29,
H9 = 30,
I9 = 31;
const A8 = 34,
B8 = 35,
C8 = 36,
D8 = 37,
E8 = 38,
F8 = 39,
G8 = 40,
H8 = 41,
I8 = 42;
const A7 = 45,
B7 = 46,
C7 = 47,
D7 = 48,
E7 = 49,
F7 = 50,
G7 = 51,
H7 = 52,
I7 = 53;
const A6 = 56,
B6 = 57,
C6 = 58,
D6 = 59,
E6 = 60,
F6 = 61,
G6 = 62,
H6 = 63,
I6 = 64;
const A5 = 67,
B5 = 68,
C5 = 69,
D5 = 70,
E5 = 71,
F5 = 72,
G5 = 73,
H5 = 74,
I5 = 75;
const A4 = 78,
B4 = 79,
C4 = 80,
D4 = 81,
E4 = 82,
F4 = 83,
G4 = 84,
H4 = 85,
I4 = 86;
const A3 = 89,
B3 = 90,
C3 = 91,
D3 = 92,
E3 = 93,
F3 = 94,
G3 = 95,
H3 = 96,
I3 = 97;
const A2 = 100,
B2 = 101,
C2 = 102,
D2 = 103,
E2 = 104,
F2 = 105,
G2 = 106,
H2 = 107,
I2 = 108;
const A1 = 111,
B1 = 112,
C1 = 113,
D1 = 114,
E1 = 115,
F1 = 116,
G1 = 117,
H1 = 118,
I1 = 119;
const A0 = 122,
B0 = 123,
C0 = 124,
D0 = 125,
E0 = 126,
F0 = 127,
G0 = 128,
H0 = 129,
I0 = 130;
// array to convert board square indices to coordinates
const COORDINATES = [
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"a9",
"b9",
"c9",
"d9",
"e9",
"f9",
"g9",
"h9",
"i9",
"xx",
"xx",
"a8",
"b8",
"c8",
"d8",
"e8",
"f8",
"g8",
"h8",
"i8",
"xx",
"xx",
"a7",
"b7",
"c7",
"d7",
"e7",
"f7",
"g7",
"h7",
"i7",
"xx",
"xx",
"a6",
"b6",
"c6",
"d6",
"e6",
"f6",
"g6",
"h6",
"i6",
"xx",
"xx",
"a5",
"b5",
"c5",
"d5",
"e5",
"f5",
"g5",
"h5",
"i5",
"xx",
"xx",
"a4",
"b4",
"c4",
"d4",
"e4",
"f4",
"g4",
"h4",
"i4",
"xx",
"xx",
"a3",
"b3",
"c3",
"d3",
"e3",
"f3",
"g3",
"h3",
"i3",
"xx",
"xx",
"a2",
"b2",
"c2",
"d2",
"e2",
"f2",
"g2",
"h2",
"i2",
"xx",
"xx",
"a1",
"b1",
"c1",
"d1",
"e1",
"f1",
"g1",
"h1",
"i1",
"xx",
"xx",
"a0",
"b0",
"c0",
"d0",
"e0",
"f0",
"g0",
"h0",
"i0",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
"xx",
];
/****************************\
============================
BOARD VARIABLES
============================
\****************************/
/*
PABNCRK pabncrk
兵仕相傌炮俥帥 卒士象馬炮車將
Board representation
(11x14 Mailbox)
x x x x x x x x x x x
x x x x x x x x x x x
x r n b a k a b n r x
x . . . . . . . . . x
x . c . . . . . c . x
x p . p . p . p . p x
x . . . . . . . . . x
x . . . . . . . . . x
x P . P . P . P . P x
x . C . . . . . C . x
x . . . . . . . . . x
x R N B A K A B N R x
x x x x x x x x x x x
x x x x x x x x x x x
*/
// xiangqi board
var board = new Array(11 * 14);
// side to move
var side = RED;
// 60 moves draw rule counter
var sixty = 0;
// almost unnique position identifier
hashKey = 0;
// squares occupied by kings
var kingSquare = [0, 0];
// move stack
var moveStack = [];
// plies
var searchPly = 0;
var gamePly = 0;
/****************************\
============================
BOARD METHODS
============================
\****************************/
// reset board array and game state variables
function resetBoard() {
// reset board position
for (let rank = 0; rank < 14; rank++) {
for (let file = 0; file < 11; file++) {
let square = rank * 11 + file;
if (COORDINATES[square] != "xx") board[square] = EMPTY;
else board[square] = OFFBOARD;
}
}
// reset game state variables
side = RED;
sixty = 0;
hashKey = 0;
kingSquare = [0, 0];
moveStack = [];
// reset plies
searchPly = 0;
gamePly = 0;
// reset repetition table
for (let index in repetitionTable) repetitionTable[index] = 0;
}
/****************************\
============================
RANDOM NUMBER GENERATOR
============================
\****************************/
// fixed random seed
var randomState = 1804289383;
// generate 32-bit pseudo legal numbers
function random() {
var number = randomState;
// 32-bit XOR shift
number ^= number << 13;
number ^= number >> 17;
number ^= number << 5;
randomState = number;
return number;
}
/****************************\
============================
ZOBRIST KEYS
============================
\****************************/
// random keys
var pieceKeys = new Array(15 * 154);
var sideKey;
// init random hash keys
function initRandomKeys() {
for (var index = 0; index < pieceKeys.length; index++)
pieceKeys[index] = random();
sideKey = random();
}
// generate hash key
function generateHashKey() {
var finalKey = 0;
// hash board position
for (var square = 0; square < board.length; square++) {
if (board[square] != OFFBOARD) {
let piece = board[square];
if (piece != EMPTY)
finalKey ^= pieceKeys[piece * board.length + square];
}
}
// hash board state variables
if (side == RED) finalKey ^= sideKey;
return finalKey;
}
/****************************\
============================
INPUT & OUTPUT
============================
\****************************/
// encode ascii pieces
const CHAR_TO_PIECE = {
P: RED_PAWN,
A: RED_ADVISOR,
B: RED_BISHOP,
E: RED_BISHOP,
N: RED_KNIGHT,
H: RED_BISHOP,
C: RED_CANNON,
R: RED_ROOK,
K: RED_KING,
p: BLACK_PAWN,
a: BLACK_ADVISOR,
b: BLACK_BISHOP,
e: BLACK_BISHOP,
n: BLACK_KNIGHT,
h: BLACK_KNIGHT,
c: BLACK_CANNON,
r: BLACK_ROOK,
k: BLACK_KING,
};
// ascii character piece representation
const PIECE_TO_CHAR = [
".",
"P",
"A",
"B",
"N",
"C",
"R",
"K",
"p",
"a",
"b",
"n",
"c",
"r",
"k",
];
// set board position from FEN string
function setBoard(fen) {
resetBoard();
var index = 0;
// parse position
for (let rank = 0; rank < 14; rank++) {
for (let file = 0; file < 11; file++) {
let square = rank * 11 + file;
if (board[square] != OFFBOARD) {
// parse pieces
if (
(fen[index].charCodeAt() >= "a".charCodeAt() &&
fen[index].charCodeAt() <= "z".charCodeAt()) ||
(fen[index].charCodeAt() >= "A".charCodeAt() &&
fen[index].charCodeAt() <= "Z".charCodeAt())
) {
if (fen[index] == "K") kingSquare[RED] = square;
else if (fen[index] == "k") kingSquare[BLACK] = square;
board[square] = CHAR_TO_PIECE[fen[index]];
index++;
}
// parse empty squares
if (
fen[index].charCodeAt() >= "0".charCodeAt() &&
fen[index].charCodeAt() <= "9".charCodeAt()
) {
var offset = fen[index] - "0";
if (board[square] == EMPTY) file--;
file += offset;
index++;
}
if (fen[index] == "/") index++;
}
}
}
// parse side to move
index++;
side = fen[index] == "b" ? BLACK : RED;
// parse sixty move rule
sixty = parseInt(fen.split(" ")[fen.split(" ").length - 2]);
// generate hash key
hashKey = generateHashKey();
}
// print board to console
function printBoard() {
let boardString = "";
// print board position
for (let rank = 0; rank < 14; rank++) {
for (let file = 0; file < 11; file++) {
let square = rank * 11 + file;
if (board[square] != OFFBOARD) {
if (file == 1) boardString += 11 - rank + " ";
boardString += PIECE_TO_CHAR[board[square]] + " ";
}
}
if (rank < 13) boardString += "\n";
}
boardString += " a b c d e f g h i\n\n";
boardString += " side: " + (side == RED ? "r" : "b") + "\n";
boardString += " sixty: " + sixty + "\n";
boardString += " hash key: " + hashKey + "\n";
boardString +=
" king squares: [" +
COORDINATES[kingSquare[RED]] +
", " +
COORDINATES[kingSquare[BLACK]] +
"]\n";
console.log(boardString);
}
// validate move
function moveFromString(moveString) {
let moveList = generateMoves(ALL_MOVES);
// parse move string
var sourceSquare =
(11 - (moveString[1].charCodeAt() - "0".charCodeAt())) * 11 +
(moveString[0].charCodeAt() - "a".charCodeAt() + 1);
var targetSquare =
(11 - (moveString[3].charCodeAt() - "0".charCodeAt())) * 11 +
(moveString[2].charCodeAt() - "a".charCodeAt() + 1);
// validate
for (var count = 0; count < moveList.length; count++) {
var move = moveList[count].move;
var promotedPiece = 0;
if (
getSourceSquare(move) == sourceSquare &&
getTargetSquare(move) == targetSquare
) {
// legal move
return move;
}
}
// illegal move
return 0;
}
// load move sequence
function loadMoves(moves) {
moves = moves.split(" ");
for (let index = 0; index < moves.length; index++) {
let move = moves[index];
let moveString = moves[index];
let validMove = moveFromString(move);
if (validMove) {
makeMove(validMove);
if (typeof document != "undefined") {
try {
let pv = "";
let time = 0;
let score = 0;
let depth = 0;
if (userTime) {
time = Date.now() - userTime;
} else {
score = guiScore;
depth = guiDepth;
time = guiTime;
pv = guiPv;
}
moveStack[moveStack.length - 1].score = score;
moveStack[moveStack.length - 1].depth = depth;
moveStack[moveStack.length - 1].time = time;
moveStack[moveStack.length - 1].pv = pv;
} catch (e) {}
}
}
}
searchPly = 0;
}
// get game moves
function getMoves() {
let moves = [];
for (let index = 0; index < moveStack.length; index++)
moves.push(moveToString(moveStack[index].move));
return moves;
}
// print move
function moveToString(move) {
return (
COORDINATES[getSourceSquare(move)] + COORDINATES[getTargetSquare(move)]
);
}
// print pseudo legal move list
function printMoveList(moveList) {
var listMoves = " Move Piece Captured Flag Score\n\n";
for (var index = 0; index < moveList.length; index++) {
let move = moveList[index].move;
listMoves +=
" " +
COORDINATES[getSourceSquare(move)] +
COORDINATES[getTargetSquare(move)];
listMoves +=
" " +
PIECE_TO_CHAR[getSourcePiece(move)] +
" " +
PIECE_TO_CHAR[getTargetPiece(move)] +
" " +
getCaptureFlag(move) +
" " +
moveList[index].score +
"\n";
}
listMoves += "\n Total moves: " + moveList.length;
console.log(listMoves);
}
/****************************\
============================
ATTACKS
============================
\****************************/
// directions
const UP = -11;
const DOWN = 11;
const LEFT = -1;
const RIGHT = 1;
// piece move offsets
const ORTHOGONALS = [LEFT, RIGHT, UP, DOWN];
const DIAGONALS = [UP + LEFT, UP + RIGHT, DOWN + LEFT, DOWN + RIGHT];
// offsets to get attacks by pawns
const PAWN_ATTACK_OFFSETS = [
[DOWN, LEFT, RIGHT],
[UP, LEFT, RIGHT],
];
// offsets to get attacks by knights
const KNIGHT_ATTACK_OFFSETS = [
[UP + UP + LEFT, LEFT + LEFT + UP],
[UP + UP + RIGHT, RIGHT + RIGHT + UP],
[DOWN + DOWN + LEFT, LEFT + LEFT + DOWN],
[RIGHT + RIGHT + DOWN, DOWN + DOWN + RIGHT],
];
// offsets to get attacks by pawns
const PAWN_MOVE_OFFSETS = [
[UP, LEFT, RIGHT],
[DOWN, LEFT, RIGHT],
];
// offsets to get target squares for knights
const KNIGHT_MOVE_OFFSETS = [
[LEFT + LEFT + UP, LEFT + LEFT + DOWN],
[RIGHT + RIGHT + UP, RIGHT + RIGHT + DOWN],
[UP + UP + LEFT, UP + UP + RIGHT],
[DOWN + DOWN + LEFT, DOWN + DOWN + RIGHT],
];
// offsets to get target squares for bishops
const BISHOP_MOVE_OFFSETS = [
(UP + LEFT) * 2,
(UP + RIGHT) * 2,
(DOWN + LEFT) * 2,
(DOWN + RIGHT) * 2,
];
// square attacked by the given side
function isSquareAttacked(square, color) {
// by knights
for (let direction = 0; direction < DIAGONALS.length; direction++) {
let directionTarget = square + DIAGONALS[direction];
if (board[directionTarget] == EMPTY) {
for (let offset = 0; offset < 2; offset++) {
let knightTarget = square + KNIGHT_ATTACK_OFFSETS[direction][offset];
if (board[knightTarget] == (color == RED ? RED_KNIGHT : BLACK_KNIGHT))
return 1;
}
}
}
// by king, rooks & cannons
for (let direction = 0; direction < ORTHOGONALS.length; direction++) {
let directionTarget = square + ORTHOGONALS[direction];
let jumpOver = 0;
while (board[directionTarget] != OFFBOARD) {
if (jumpOver == 0) {
if (
board[directionTarget] == (color == RED ? RED_ROOK : BLACK_ROOK) ||
board[directionTarget] == (color == RED ? RED_KING : BLACK_KING)
)
return 1;
}
if (board[directionTarget] != EMPTY) jumpOver++;
if (
jumpOver == 2 &&
board[directionTarget] == (color == RED ? RED_CANNON : BLACK_CANNON)
)
return 1;
directionTarget += ORTHOGONALS[direction];
}
}
// by pawns
for (
let direction = 0;
direction < PAWN_ATTACK_OFFSETS[color].length;
direction++
) {
let directionTarget = square + PAWN_ATTACK_OFFSETS[color][direction];
if (board[directionTarget] == (color == RED ? RED_PAWN : BLACK_PAWN))
return 1;
}
return 0;
}
/****************************\
============================
MOVE GENERATOR
============================
\****************************/
// zones of xiangqi board
const BOARD_ZONES = [
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 1, 1, 1,
2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
],
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 2, 2, 2, 1, 1, 1, 0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 1, 1, 1, 2, 2,
2, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
],
];
/*
MOVE ENCODING
0000 0000 0000 0000 0000 1111 1111 source square 0xFF
0000 0000 0000 1111 1111 0000 0000 target square 0xFF00
0000 0000 1111 0000 0000 0000 0000 source piece 0xF0000
0000 1111 0000 0000 0000 0000 0000 target piece 0xF00000
0001 0000 0000 0000 0000 0000 0000 capture flag 0x1000000
*/
// store squares & pieces into a single number
function encodeMove(
sourceSquare,
targetSquare,
sourcePiece,
targetPiece,
captureFlag
) {
return (
sourceSquare |
(targetSquare << 8) |
(sourcePiece << 16) |
(targetPiece << 20) |
(captureFlag << 24)
);
}
function getSourceSquare(move) {
return move & 0xff;
}
function getTargetSquare(move) {
return (move >> 8) & 0xff;
}
function getSourcePiece(move) {
return (move >> 16) & 0xf;
}
function getTargetPiece(move) {
return (move >> 20) & 0xf;
}
function getCaptureFlag(move) {
return (move >> 24) & 0x1;
}
// push move into move list
function pushMove(
moveList,
sourceSquare,
targetSquare,
sourcePiece,
targetPiece,
onlyCaptures
) {
if (targetPiece == EMPTY || (PIECE_COLOR[targetPiece] == side) ^ 1) {
let move = 0;
if (targetPiece)
move = encodeMove(
sourceSquare,
targetSquare,
sourcePiece,
targetPiece,
1
);
else {
if (onlyCaptures == 0)
move = encodeMove(
sourceSquare,
targetSquare,
sourcePiece,
targetPiece,
0
);
}
let moveScore = 0;
if (getCaptureFlag(move)) {
moveScore =
MVV_LVA[
board[getSourceSquare(move)] * 15 + board[getTargetSquare(move)]
];
moveScore += 10000;
} else {
if (killerMoves[searchPly] == move) moveScore = 9000;
else if (killerMoves[MAX_PLY + searchPly] == move) moveScore = 8000;
else
moveScore =
historyMoves[
board[getSourceSquare(move)] * 154 + getTargetSquare(move)
];
}
if (move) {
moveList.push({
move: move,
score: moveScore,
});
}
}
}
// generate pseudo legal moves
function generateMoves(onlyCaptures) {
let moveList = [];
for (let sourceSquare = 0; sourceSquare < board.length; sourceSquare++) {
if (board[sourceSquare] != OFFBOARD) {
let piece = board[sourceSquare];
let pieceType = PIECE_TYPE[piece];
let pieceColor = PIECE_COLOR[piece];
if (pieceColor == side) {
// pawns
if (pieceType == PAWN) {
for (
let direction = 0;
direction < PAWN_MOVE_OFFSETS[side].length;
direction++
) {
let targetSquare =
sourceSquare + PAWN_MOVE_OFFSETS[side][direction];
let targetPiece = board[targetSquare];
if (targetPiece != OFFBOARD)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
if (BOARD_ZONES[side][sourceSquare]) break;
}
}
// kings & advisors
if (pieceType == KING || pieceType == ADVISOR) {
for (
let direction = 0;
direction < ORTHOGONALS.length;
direction++
) {
let offsets = pieceType == KING ? ORTHOGONALS : DIAGONALS;
let targetSquare = sourceSquare + offsets[direction];
let targetPiece = board[targetSquare];
if (BOARD_ZONES[side][targetSquare] == 2)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
}
}
// bishops
if (pieceType == BISHOP) {
for (
let direction = 0;
direction < BISHOP_MOVE_OFFSETS.length;
direction++
) {
let targetSquare = sourceSquare + BISHOP_MOVE_OFFSETS[direction];
let jumpOver = sourceSquare + DIAGONALS[direction];
let targetPiece = board[targetSquare];
if (BOARD_ZONES[side][targetSquare] && board[jumpOver] == EMPTY)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
}
}
// knights
if (pieceType == KNIGHT) {
for (
let direction = 0;
direction < ORTHOGONALS.length;
direction++
) {
let targetDirection = sourceSquare + ORTHOGONALS[direction];
if (board[targetDirection] == EMPTY) {
for (let offset = 0; offset < 2; offset++) {
let targetSquare =
sourceSquare + KNIGHT_MOVE_OFFSETS[direction][offset];
let targetPiece = board[targetSquare];
if (targetPiece != OFFBOARD)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
}
}
}
}
// rooks & cannons
if (pieceType == ROOK || pieceType == CANNON) {
for (
let direction = 0;
direction < ORTHOGONALS.length;
direction++
) {
let targetSquare = sourceSquare + ORTHOGONALS[direction];
let jumpOver = 0;
while (board[targetSquare] != OFFBOARD) {
let targetPiece = board[targetSquare];
if (jumpOver == 0) {
// all rook moves
if (
pieceType == ROOK &&
(PIECE_COLOR[targetPiece] == side) ^ 1
)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
// quiet cannon moves
else if (pieceType == CANNON && targetPiece == EMPTY)
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
}
if (targetPiece) jumpOver++;
if (
targetPiece &&
pieceType == CANNON &&
(PIECE_COLOR[targetPiece] == side) ^ 1 &&
jumpOver == 2
) {
// capture cannon moves
pushMove(
moveList,
sourceSquare,
targetSquare,
board[sourceSquare],
targetPiece,
onlyCaptures
);
break;
}
targetSquare += ORTHOGONALS[direction];
}
}
}
}
}
}
return moveList;
}
// generate only legal moves
function generateLegalMoves() {
let legalMoves = [];
let moveList = generateMoves(ALL_MOVES);
for (let count = 0; count < moveList.length; count++) {
if (makeMove(moveList[count].move) == 0) continue;
legalMoves.push(moveList[count]);
takeBack();
}
return legalMoves;
}
/****************************\
============================
MAKE MOVE / TAKE BACK
============================
\****************************/
// make move
function makeMove(move) {
// update plies
searchPly++;
gamePly++;
// update repetition table
repetitionTable[gamePly] = hashKey;
// moveStack board state variables
moveStack.push({
move: move,
hashKey: hashKey,
sixty: sixty,
});
// parse move
let sourceSquare = getSourceSquare(move);
let targetSquare = getTargetSquare(move);
let sourcePiece = getSourcePiece(move);
let targetPiece = getTargetPiece(move);
let captureFlag = getCaptureFlag(move);
// move piece
board[targetSquare] = sourcePiece;
board[sourceSquare] = EMPTY;
// hash piece
hashKey ^= pieceKeys[sourcePiece * board.length + sourceSquare];
hashKey ^= pieceKeys[sourcePiece * board.length + targetSquare];
if (captureFlag) {
sixty = 0;
hashKey ^= pieceKeys[targetPiece * board.length + targetSquare];
} else sixty++;
// update king square
if (board[targetSquare] == RED_KING || board[targetSquare] == BLACK_KING)
kingSquare[side] = targetSquare;
// switch side to move
side ^= 1;
hashKey ^= sideKey;
// return illegal move if king is left in check
if (isSquareAttacked(kingSquare[side ^ 1], side)) {
takeBack();
return 0;
} else return 1;
}
// take back
function takeBack() {
// update plies
searchPly--;
gamePly--;
// parse move
let moveIndex = moveStack.length - 1;
let move = moveStack[moveIndex].move;
let sourceSquare = getSourceSquare(move);
let targetSquare = getTargetSquare(move);
let sourcePiece = getSourcePiece(move);
let targetPiece = getTargetPiece(move);
// move piece
board[sourceSquare] = sourcePiece;
board[targetSquare] = EMPTY;
// restore captured piece
if (getCaptureFlag(move)) {
board[targetSquare] = targetPiece;
}
// update king square
if (board[sourceSquare] == RED_KING || board[sourceSquare] == BLACK_KING)
kingSquare[side ^ 1] = sourceSquare;
// switch side to move
side ^= 1;
sixty = moveStack[moveIndex].sixty;
hashKey = moveStack[moveIndex].hashKey;
moveStack.pop();
}
// make null move
function makeNullMove() {
// backup current board state
moveStack.push({
move: 0,
side: side,
sixty: sixty,
hashKey: hashKey,
});
sixty = 0;
side ^= 1;
hashKey ^= sideKey;
}
// take null move
function takeNullMove() {
// restore board state
side = moveStack[moveStack.length - 1].side;
sixty = moveStack[moveStack.length - 1].sixty;
hashKey = moveStack[moveStack.length - 1].hashKey;
moveStack.pop();
}
/****************************\
============================
PERFT
============================
\****************************/
/*
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1
depth nodes checks captures
1 44 0 2
2 1920 6 72
3 79666 384 3159
4 3290240 19380 115365
5 133312995 953251 4917734
6 5392831844
r1ba1a3/4kn3/2n1b4/pNp1p1p1p/4c4/6P2/P1P2R2P/1CcC5/9/2BAKAB2 w - - 0 1
depth nodes checks captures
1 38 1 1
2 1128 12 10
3 43929 1190 2105
4 1339047 21299 31409
5 53112976 1496697 3262495
*/
// perft driver
function perftDriver(depth) {
if (depth == 0) {
nodes++;
return;
}
let moveList = generateMoves(ALL_MOVES);
for (var count = 0; count < moveList.length; count++) {
if (!makeMove(moveList[count].move)) continue;
perftDriver(depth - 1);
takeBack();
}
}
// perft test
function perftTest(depth) {
nodes = 0;
console.log(" Performance test:\n");
resultString = "";
let startTime = Date.now();
let moveList = generateMoves(ALL_MOVES);
for (var count = 0; count < moveList.length; count++) {
if (makeMove(moveList[count].move) == 0) continue;
let cumNodes = nodes;
perftDriver(depth - 1);
takeBack();
let oldNodes = nodes - cumNodes;
console.log(
" move" +
" " +
(count + 1) +
(count < 9 ? ": " : ": ") +
COORDINATES[getSourceSquare(moveList[count].move)] +
COORDINATES[getTargetSquare(moveList[count].move)] +
" nodes: " +
oldNodes
);
}
resultString += "\n Depth: " + depth;
resultString += "\n Nodes: " + nodes;
resultString += "\n Time: " + (Date.now() - startTime) + " ms\n";
console.log(resultString);
}
/****************************\
============================
EVALUATION
============================
\****************************/
/*
I took evaluation parameters from Mark Dirish's
javascript xiangqi engine: https://github.com/markdirish/xiangqi
Credits to initial sources (from Mark's sources):
material weights: by Yen et al. 2004, "Computer Chinese Chess" ICGA Journal
PST weights: by Li, Cuanqi 2008, "Using AdaBoost to Implement Chinese
Chess Evaluation Functions", UCLA thesis
*/
// evaluate types P A B N C R K
const EVALUATE_TYPES = [1, 0, 0, 1, 1, 1, 0];
// material weights
const MATERIAL_WEIGHTS = [
// P A B N C R K
0, 30, 120, 120, 270, 285, 600, 6000,
// p a b n c r k
-30, -120, -120, -270, -285, -600, -6000,
];
// piece square tables
const PST = [
// pawns
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
6, 9, 12, 9, 6, 3, 0, 0, 0, 18, 36, 56, 80, 120, 80, 56, 36, 18, 0, 0, 14,
26, 42, 60, 80, 60, 42, 26, 14, 0, 0, 10, 20, 30, 34, 40, 34, 30, 20, 10,
0, 0, 6, 12, 18, 18, 20, 18, 18, 12, 6, 0, 0, 2, 0, 8, 0, 8, 0, 8, 0, 2,
0, 0, 0, 0, -2, 0, 4, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
[], // skip advisors
[], // skip bishops
// knights
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 8,
16, 12, 4, 12, 16, 8, 4, 0, 0, 4, 10, 28, 16, 8, 16, 28, 10, 4, 0, 0, 12,
14, 16, 20, 18, 20, 16, 14, 12, 0, 0, 8, 24, 18, 24, 20, 24, 18, 24, 8, 0,
0, 6, 16, 14, 18, 16, 18, 14, 16, 6, 0, 0, 4, 12, 16, 14, 12, 14, 16, 12,
4, 0, 0, 2, 6, 8, 6, 10, 6, 8, 6, 2, 0, 0, 4, 2, 8, 8, 4, 8, 8, 2, 4, 0,
0, 0, 2, 4, 4, -2, 4, 4, 2, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
// cannon
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 4,
0, -10, -12, -10, 0, 4, 6, 0, 0, 2, 2, 0, -4, -14, -4, 0, 2, 2, 0, 0, 2,
2, 0, -10, -8, -10, 0, 2, 2, 0, 0, 0, 0, -2, 4, 10, 4, -2, 0, 0, 0, 0, 0,
0, 0, 2, 8, 2, 0, 0, 0, 0, 0, -2, 0, 4, 2, 6, 2, 4, 0, -2, 0, 0, 0, 0, 0,
2, 4, 2, 0, 0, 0, 0, 0, 4, 0, 8, 6, 10, 6, 8, 0, 4, 0, 0, 0, 2, 4, 6, 6,
6, 4, 2, 0, 0, 0, 0, 0, 2, 6, 6, 6, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
// rooks
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14,
14, 12, 18, 16, 18, 12, 14, 14, 0, 0, 16, 20, 18, 24, 26, 24, 18, 20, 16,
0, 0, 12, 12, 12, 18, 18, 18, 12, 12, 12, 0, 0, 12, 18, 16, 22, 22, 22,
16, 18, 12, 0, 0, 12, 14, 12, 18, 18, 18, 12, 14, 12, 0, 0, 12, 16, 14,
20, 20, 20, 14, 16, 12, 0, 0, 6, 10, 8, 14, 14, 14, 8, 10, 6, 0, 0, 4, 8,
6, 14, 12, 14, 6, 8, 4, 0, 0, 8, 4, 8, 16, 8, 16, 8, 4, 8, 0, 0, -2, 10,
6, 14, 12, 14, 6, 10, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
],
[], // skip kings
];
// mirror square for black
const MIRROR_SQUARE = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
A0,
B0,
C0,
D0,
E0,
F0,
G0,
H0,
I0,
0,
0,
A1,
B1,
C1,
D1,
E1,
F1,
G1,
H1,
I1,
0,
0,
A2,
B2,
C2,
D2,
E2,
F2,
G2,
H2,
I2,
0,
0,
A3,
B3,
C3,
D3,
E3,
F3,
G3,
H3,
I3,
0,
0,
A4,
B4,
C4,
D4,
E4,
F4,
G4,
H4,
I4,
0,
0,
A5,
B5,
C5,
D5,
E5,
F5,
G5,
H5,
I5,
0,
0,
A6,
B6,
C6,
D6,
E6,
F6,
G6,
H6,
I6,
0,
0,
A7,
B7,
C7,
D7,
E7,
F7,
G7,
H7,
I7,
0,
0,
A8,
B8,
C8,
D8,
E8,
F8,
G8,
H8,
I8,
0,
0,
A9,
B9,
C9,
D9,
E9,
F9,
G9,
H9,
I9,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
];
// static evaluation
function evaluate() {
let score = 0;
for (let square = 0; square < board.length; square++) {
if (board[square] != OFFBOARD) {
if (board[square]) {
let piece = board[square];
let pstIndex = PIECE_TYPE[piece] - 16;
let pieceColor = PIECE_COLOR[piece];
// material score
score += MATERIAL_WEIGHTS[piece];
// positional score
if (EVALUATE_TYPES[pstIndex]) {
if (pieceColor == RED) score += PST[pstIndex][square];
else score -= PST[pstIndex][MIRROR_SQUARE[square]];
}
}
}
}
return side == RED ? score : -score;
}
/****************************\
============================
TRANSPOSITION TABLE
============================
\****************************/
// 16Mb default hash table size
var hashEntries = 838860;
// no hash entry found constant
const NO_HASH = 100000;
// transposition table hash flags
const HASH_EXACT = 0;
const HASH_ALPHA = 1;
const HASH_BETA = 2;
// define TT instance
var hashTable = [];
// set hash size
function setHashSize(Mb) {
hashTable = [];
// adjust MB if going beyond the aloowed bounds
if (Mb < 4) Mb = 4;
if (Mb > 128) Mb = 128;
hashEntries = parseInt((Mb * 0x100000) / 20);
initHashTable();
console.log("Set hash table size to", Mb, "Mb");
console.log("Hash table initialized with", hashEntries, "entries");
}
// clear TT (hash table)
function initHashTable() {
// loop over TT elements
for (var index = 0; index < hashEntries; index++) {
// reset TT inner fields
hashTable[index] = {
hashKey: 0,
depth: 0,
flag: 0,
score: 0,
bestMove: 0,
};
}
}
// read hash entry data
function readHashEntry(alpha, beta, bestMove, depth) {
// init hash entry
var hashEntry = hashTable[(hashKey & 0x7fffffff) % hashEntries];
// match hash key
if (hashEntry.hashKey == hashKey) {
if (hashEntry.depth >= depth) {
// init score
var score = hashEntry.score;
// adjust mating scores
if (score < -MATE_SCORE) score += searchPly;
if (score > MATE_SCORE) score -= searchPly;
// match hash flag
if (hashEntry.flag == HASH_EXACT) return score;
if (hashEntry.flag == HASH_ALPHA && score <= alpha) return alpha;
if (hashEntry.flag == HASH_BETA && score >= beta) return beta;
}
// store best move
bestMove.value = hashEntry.bestMove;
}
// if hash entry doesn't exist
return NO_HASH;
}
// write hash entry data
function writeHashEntry(score, bestMove, depth, hashFlag) {
// init hash entry
var hashEntry = hashTable[(hashKey & 0x7fffffff) % hashEntries];
// adjust mating scores
if (score < -MATE_SCORE) score -= searchPly;
if (score > MATE_SCORE) score += searchPly;
// write hash entry data
hashEntry.hashKey = hashKey;
hashEntry.score = score;
hashEntry.flag = hashFlag;
hashEntry.depth = depth;
hashEntry.bestMove = bestMove;
}
/****************************\
============================
SEARCH
============================
\****************************/
// visited nodes count
var nodes = 0;
// most valuable victim least valuable attacker, e.g. Pxr == 606, Rxp =
const MVV_LVA = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 106,
206, 306, 406, 506, 606, 706, 0, 0, 0, 0, 0, 0, 0, 0, 105, 205, 305, 405,
505, 605, 705, 0, 0, 0, 0, 0, 0, 0, 0, 104, 204, 304, 404, 504, 604, 704, 0,
0, 0, 0, 0, 0, 0, 0, 103, 203, 303, 403, 503, 603, 703, 0, 0, 0, 0, 0, 0, 0,
0, 102, 202, 302, 402, 502, 602, 702, 0, 0, 0, 0, 0, 0, 0, 0, 101, 201, 301,
401, 501, 601, 701, 0, 0, 0, 0, 0, 0, 0, 0, 100, 200, 300, 400, 500, 600,
700,
0, 106, 206, 306, 406, 506, 606, 706, 0, 0, 0, 0, 0, 0, 0, 0, 105, 205, 305,
405, 505, 605, 705, 0, 0, 0, 0, 0, 0, 0, 0, 104, 204, 304, 404, 504, 604,
704, 0, 0, 0, 0, 0, 0, 0, 0, 103, 203, 303, 403, 503, 603, 705, 0, 0, 0, 0,
0, 0, 0, 0, 102, 202, 302, 402, 502, 602, 704, 0, 0, 0, 0, 0, 0, 0, 0, 101,
201, 301, 401, 501, 601, 703, 0, 0, 0, 0, 0, 0, 0, 0, 100, 200, 300, 400,
500, 600, 700, 0, 0, 0, 0, 0, 0, 0,
];
// search constants
const MAX_PLY = 64;
const INFINITY = 50000;
const MATE_VALUE = 49000;
const MATE_SCORE = 48000;
const DO_NULL = 1;
const NO_NULL = 0;
const ALL_MOVES = 0;
const ONLY_CAPTURES = 1;
// search variables
var followPv;
// PV table
var pvTable = new Array(MAX_PLY * MAX_PLY);
var pvLength = new Array(MAX_PLY);
// killer moves
var killerMoves = new Array(2 * MAX_PLY);
// history moves
var historyMoves = new Array(15 * 154);
// repetition table
var repetitionTable = new Array(1000);
// time control handling
var timing = {
timeSet: 0,
stopTime: 0,
stopped: 0,
time: -1,
};
// set time control
function setTimeControl(timeControl) {
timing = timeControl;
}
// reset time control
function resetTimeControl() {
timing = {
timeSet: 0,
stopTime: 0,
stopped: 0,
time: -1,
};
}
function clearSearch() {
// reset nodes counter
nodes = 0;
timing.stopped = 0;
searchPly = 0;
for (let index = 0; index < pvTable.length; index++) pvTable[index] = 0;
for (let index = 0; index < pvLength.length; index++) pvLength[index] = 0;
for (let index = 0; index < killerMoves.length; index++)
killerMoves[index] = 0;
for (let index = 0; index < historyMoves.length; index++)
historyMoves[index] = 0;
}
// handle time control
function checkTime() {
if (timing.timeSet == 1 && Date.now() > timing.stopTime) timing.stopped = 1;
}
// position repetition detection
function isRepetition() {
for (let index = 0; index < gamePly; index++)
if (repetitionTable[index] == hashKey) return 1;
return 0;
}
// move ordering
function sortMoves(currentCount, moveList) {
for (
let nextCount = currentCount + 1;
nextCount < moveList.length;
nextCount++
) {
if (moveList[currentCount].score < moveList[nextCount].score) {
let tempMove = moveList[currentCount];
moveList[currentCount] = moveList[nextCount];
moveList[nextCount] = tempMove;
}
}
}
// sort PV move
function sortPvMove(moveList, bestMove) {
// sort hash table move
for (let count = 0; count < moveList.length; count++) {
if (moveList[count].move == bestMove.value) {
moveList[count].score = 30000;
return;
}
}
// sort PV move
if (searchPly && followPv) {
followPv = 0;
for (let count = 0; count < moveList.length; count++) {
if (moveList[count].move == pvTable[searchPly]) {
followPv = 1;
moveList[count].score = 20000;
break;
}
}
}
}
// store PV move
function storePvMove(move) {
pvTable[searchPly * 64 + searchPly] = move;
for (
var nextPly = searchPly + 1;
nextPly < pvLength[searchPly + 1];
nextPly++
)
pvTable[searchPly * 64 + nextPly] =
pvTable[(searchPly + 1) * 64 + nextPly];
pvLength[searchPly] = pvLength[searchPly + 1];
}
// quiescence search
function quiescence(alpha, beta) {
pvLength[searchPly] = searchPly;
nodes++;
if ((nodes & 2047) == 0) {
checkTime();
if (timing.stopped == 1) return 0;
}
if (searchPly > MAX_PLY - 1) return evaluate();
let evaluation = evaluate();
if (evaluation >= beta) return beta;
if (evaluation > alpha) alpha = evaluation;
var moveList = generateMoves(ONLY_CAPTURES);
// sort PV move
sortPvMove(moveList, { value: 0 });
// loop over moves
for (var count = 0; count < moveList.length; count++) {
sortMoves(count, moveList);
let move = moveList[count].move;
if (makeMove(move) == 0) continue;
var score = -quiescence(-beta, -alpha);
takeBack();
if (timing.stopped == 1) return 0;
if (score > alpha) {
storePvMove(move);
alpha = score;
if (score >= beta) return beta;
}
}
return alpha;
}
// negamax search
function negamax(alpha, beta, depth, nullMove) {
pvLength[searchPly] = searchPly;
// best move for TT
var bestMove = { value: 0 };
var hashFlag = HASH_ALPHA;
let score = 0;
let pvNode = beta - alpha > 1;
let futilityPruning = 0;
// read hash entry
if (
searchPly &&
(score = readHashEntry(alpha, beta, bestMove, depth)) != NO_HASH &&
pvNode == 0
)
return score;
// check time left
if ((nodes & 2047) == 0) {
checkTime();
if (timing.stopped == 1) return 0;
}
if (sixty >= 120) return 0;
if (searchPly && isRepetition()) return -MATERIAL_WEIGHTS[RED_CANNON];
if (depth == 0) {
nodes++;
return quiescence(alpha, beta);
}
// mate distance pruning
if (alpha < -MATE_VALUE) alpha = -MATE_VALUE;
if (beta > MATE_VALUE - 1) beta = MATE_VALUE - 1;
if (alpha >= beta) return alpha;
let legalMoves = 0;
let inCheck = isSquareAttacked(kingSquare[side], side ^ 1);
// check extension
if (inCheck) depth++;
if (inCheck == 0 && pvNode == 0) {
// static evaluation for pruning purposes
let staticEval = evaluate();
// evalution pruning
if (depth < 3 && Math.abs(beta - 1) > -MATE_VALUE + 100) {
let evalMargin = MATERIAL_WEIGHTS[RED_PAWN] * depth;
if (staticEval - evalMargin >= beta) return staticEval - evalMargin;
}
if (nullMove) {
// null move pruning
if (searchPly && depth > 2 && staticEval >= beta) {
makeNullMove();
score = -negamax(-beta, -beta + 1, depth - 1 - 2, NO_NULL);
takeNullMove();
if (timing.stopped == 1) return 0;
if (score >= beta) return beta;
}
// razoring
score = staticEval + MATERIAL_WEIGHTS[RED_PAWN];
let newScore;
if (score < beta) {
if (depth == 1) {
newScore = quiescence(alpha, beta);
return newScore > score ? newScore : score;
}
}
score += MATERIAL_WEIGHTS[RED_PAWN];
if (score < beta && depth < 4) {
newScore = quiescence(alpha, beta);
if (newScore < beta) return newScore > score ? newScore : score;
}
}
// futility pruning condition
let futilityMargin = [
0,
MATERIAL_WEIGHTS[RED_PAWN],
MATERIAL_WEIGHTS[RED_KNIGHT],
MATERIAL_WEIGHTS[RED_CANNON],
];
if (
depth < 4 &&
Math.abs(alpha) < MATE_SCORE &&
staticEval + futilityMargin[depth] <= alpha
)
futilityPruning = 1;
}
let movesSearched = 0;
let moveList = generateMoves(ALL_MOVES);
// sort PV move
sortPvMove(moveList, bestMove);
// loop over moves
for (let count = 0; count < moveList.length; count++) {
sortMoves(count, moveList);
let move = moveList[count].move;
if (makeMove(move) == 0) continue;
legalMoves++;
// futility pruning
if (
futilityPruning &&
movesSearched &&
getCaptureFlag(move) == 0 &&
isSquareAttacked(kingSquare[side], side ^ 1) == 0
) {
takeBack();
continue;
}
if (movesSearched == 0)
score = -negamax(-beta, -alpha, depth - 1, DO_NULL);
else {
// LMR
if (
pvNode == 0 &&
movesSearched > 3 &&
depth > 2 &&
inCheck == 0 &&
(getSourceSquare(move) != getSourceSquare(killerMoves[searchPly]) ||
getTargetSquare(move) != getTargetSquare(killerMoves[searchPly])) &&
(getSourceSquare(move) !=
getSourceSquare(killerMoves[MAX_PLY + searchPly]) ||
getTargetSquare(move) !=
getTargetSquare(killerMoves[MAX_PLY + searchPly])) &&
getCaptureFlag(move) == 0
) {
score = -negamax(-alpha - 1, -alpha, depth - 2, DO_NULL);
} else score = alpha + 1;
// PVS
if (score > alpha) {
score = -negamax(-alpha - 1, -alpha, depth - 1, DO_NULL);
if (score > alpha && score < beta)
score = -negamax(-beta, -alpha, depth - 1, DO_NULL);
}
}
takeBack();
movesSearched++;
if (timing.stopped == 1) return 0;
if (score > alpha) {
hashFlag = HASH_EXACT;
bestMove.value = move;
alpha = score;
storePvMove(move);
// store history moves
if (getCaptureFlag(move) == 0)
historyMoves[
board[getSourceSquare(move)] * board.length + getTargetSquare(move)
] += depth;
if (score >= beta) {
// store hash entry with the score equal to beta
writeHashEntry(beta, bestMove.value, depth, HASH_BETA);
// store killer moves
if (getCaptureFlag(move) == 0) {
killerMoves[MAX_PLY + searchPly] = killerMoves[searchPly];
killerMoves[searchPly] = move;
}
return beta;
}
}
}
// checkmate or stalemate is a win
if (legalMoves == 0) {
return -MATE_VALUE + searchPly;
}
// store hash entry with the score equal to alpha
writeHashEntry(alpha, bestMove.value, depth, hashFlag);
return alpha;
}
// search position for the best move
function searchPosition(depth) {
let start = Date.now();
let score =