performance improvements for determination of check

This commit is contained in:
Jesse D. McDonald 2020-04-05 17:25:06 -05:00
parent accbc316ff
commit 7c04b9ba09
1 changed files with 113 additions and 38 deletions

View File

@ -111,7 +111,7 @@ class Board {
}
this._board = new Uint8Array(original._board);
this._phantom = JSON.parse(JSON.stringify(original._phantom));
this._phantom = original._phantom && Object.assign({}, original._phantom);
} else {
this._board = new Uint8Array(64);
this._board.fill(0);
@ -139,12 +139,7 @@ class Board {
}
get phantom() {
let phantom = this._phantom;
if (phantom) {
return { side: phantom.side, type: phantom.type, from: phantom.from };
} else {
return null;
}
return this._phantom && Object.assign({}, this._phantom);
}
getPiece(side, square) {
@ -427,12 +422,17 @@ class Game {
this._board = new Board(original._board);
this._player = original._player;
this._status = original._status;
this._moves = JSON.parse(JSON.stringify(original._moves));
this._redo = JSON.parse(JSON.stringify(original._redo));
this._moves = original._moves.slice();
this._redo = original._redo.slice();
this._history = original._history;
this._turn = original._turn;
this._castling = JSON.parse(JSON.stringify(original._castling));
this._undo = original._undo;
this._checkCache = Object.assign({}, original._checkCache);
this._castling = {
[LIGHT]: Object.assign({}, original._castling[LIGHT]),
[DARK]: Object.assign({}, original._castling[DARK]),
};
} else {
this._board = new Board();
this._player = LIGHT;
@ -442,11 +442,13 @@ class Game {
this._history = '';
this._turn = 0;
this._undo = null;
this._checkCache = {};
/* set to false when the king or rook moves */
this._castling = {};
this._castling[LIGHT] = { king: true, queen: true };
this._castling[DARK] = { king: true, queen: true };
this._castling = {
[LIGHT]: { king: true, queen: true },
[DARK]: { king: true, queen: true },
};
}
}
@ -504,18 +506,15 @@ class Game {
if (from === (side === DARK ? 'e8' : 'e1')) {
const row = from[1];
/* memoize isInCheck(); may not need it and don't want to look twice */
/* with any other piece this would be recursive, but kings can't capture */
let inCheck = () => {
const check = this.isInCheck(side);
inCheck = () => check;
return check;
};
/*
* Note: With any other piece the call to isInCheck() would be recursive.
* Kings can't capture, so their moves aren't considered for check.
*/
if (this._castling[side].king &&
board.isEmpty('f' + row) &&
board.isEmpty('g' + row)) {
if (!inCheck()) {
if (!this.isInCheck(side)) {
yield 'g' + row;
}
}
@ -524,7 +523,7 @@ class Game {
board.isEmpty('d' + row) &&
board.isEmpty('c' + row) &&
board.isEmpty('b' + row)) {
if (!inCheck()) {
if (!this.isInCheck(side)) {
yield 'c' + row;
}
}
@ -593,17 +592,89 @@ class Game {
}
isLegalMove(side, from, to, canCapture) {
const board = this._board;
const type = board.getPiece(side, from);
/* this is valid because board.getPiece(side, PHANTOM) !== EMPTY */
const fromSquare = (from === PHANTOM) ? board.phantom.from : from;
if (this._status !== PLAYING || type === EMPTY || to === fromSquare) {
return false;
}
if (canCapture === undefined) {
canCapture = this.canCapture(side, from);
}
/* non-capturing pieces can only move to empty squares */
/* capturing pieces can only move to squares with opponent's pieces */
if (!board.isEmpty(to)) {
if (!canCapture || (board.getPiece(otherSide(side), to) === EMPTY)) {
return false;
}
}
/* pre-screen for basic movement constraints */
/* if a piece has few moves (king, pawn, knight) then just enumerate them */
/* if movement is more extensive (bishop, rook, queen) then we can do better */
if (type === BISHOP || type === ROOK || type === QUEEN) {
const ortho = (type === ROOK || type === QUEEN);
const diag = (type === BISHOP || type === QUEEN);
const fromIndex = squareIndex(fromSquare);
const toIndex = squareIndex(to);
const rowsDiff = (toIndex >>> 3) - (fromIndex >>> 3);
const columnsDiff = (toIndex & 0b111) - (fromIndex & 0b111);
if (rowsDiff === columnsDiff || rowsDiff === -columnsDiff) {
if (type === ROOK) {
return false;
}
} else if (rowsDiff === 0 || columnsDiff === 0) {
if (type === BISHOP) {
return false;
}
} else {
return false;
}
const rowsUp = Math.sign(rowsDiff);
const columnsRight = Math.sign(columnsDiff);
/* verify that the squares in between `fromSquare` and `to` are empty */
let at = offsetSquare(fromSquare, columnsRight, rowsUp);
while (at !== to) {
if (!board.isEmpty(at)) {
return false;
}
at = offsetSquare(at, columnsRight, rowsUp);
}
/* this is a legal move, no need to enumerate */
return true;
}
const legals = new Iterator(this.legalMoves(side, from, canCapture));
return legals.strictlyIncludes(to);
}
isInCheck(_side) {
if (this._status !== PLAYING || this._board.phantom) {
/* can't be in check mid-move, or if the game is already over */
return false;
const side = _side || this._player;
if (this._checkCache[side] !== undefined) {
return this._checkCache[side];
}
function recordCheck(game, x) {
game._checkCache[side] = x;
return x;
}
if (this._status !== PLAYING || this._board.phantom) {
/* can't be in check mid-move, or if the game is already over */
return recordCheck(this, false);
}
const side = _side || this._player;
const other = otherSide(side);
const kings = [...this._board.findPieces(side, KING)];
@ -626,16 +697,15 @@ class Game {
if (sim._board.getPiece(other, from) !== KING) {
if (sim.isLegalMove(other, from, king, true)) {
/* this piece can directly capture the king */
return true;
return recordCheck(this, true);
}
queue.push({ game: sim, from });
}
}
/* arbitrary limit, but a human player would probably miss a 7-move chain too */
const moveLimit = this._moves.length + 6;
const seen = new Set();
let counter = 0;
while (queue.length > 0) {
const game = queue[0].game;
@ -647,6 +717,14 @@ class Game {
/* look for another piece that can reach the king or continue the chain */
const pairs = [...game.board.findPieces(other, true, true)];
for (const pair of pairs) {
if (counter >= 300) {
/* this is taking too long */
const newMoves = game._moves.length - this._moves.length;
console.log(`stopped looking for check after ${counter} moves (max length was ${newMoves})`);
return recordCheck(this, false);
}
++counter;
if (!game.isLegalMove(other, from, pair, true)) {
/* can't reach it */
continue;
@ -668,17 +746,15 @@ class Game {
if (game2.isLegalMove(other, PHANTOM, king, true)) {
/* we have our answer */
return true;
return recordCheck(this, true);
}
if (game2._moves.length < moveLimit) {
queue.push({ game: game2, from: PHANTOM });
}
queue.push({ game: game2, from: PHANTOM });
}
}
/* didn't find anything */
return false;
return recordCheck(this, false);
}
move(from, to, meta) {
@ -789,6 +865,7 @@ class Game {
this._player = otherSide(this._player);
}
this._checkCache = {};
this._moves.push(move);
this._redo = [];
@ -814,6 +891,7 @@ class Game {
}
this._undo = new Game(this);
this._checkCache = {};
this._moves.push(move);
this._redo = [];
this._status = ENDED;
@ -869,10 +947,7 @@ class Game {
const savedRedo = this._redo;
/* copy all the properties from the saved prior game to this one */
const savedUndo = this._undo;
for (const prop in savedUndo) {
this[prop] = savedUndo[prop];
}
Object.assign(this, this._undo);
/* restore the original redo history and add the undone move */
this._redo = savedRedo;