performance improvements for determination of check
This commit is contained in:
parent
accbc316ff
commit
7c04b9ba09
149
js/pacosako.js
149
js/pacosako.js
|
|
@ -111,7 +111,7 @@ class Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._board = new Uint8Array(original._board);
|
this._board = new Uint8Array(original._board);
|
||||||
this._phantom = JSON.parse(JSON.stringify(original._phantom));
|
this._phantom = original._phantom && Object.assign({}, original._phantom);
|
||||||
} else {
|
} else {
|
||||||
this._board = new Uint8Array(64);
|
this._board = new Uint8Array(64);
|
||||||
this._board.fill(0);
|
this._board.fill(0);
|
||||||
|
|
@ -139,12 +139,7 @@ class Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
get phantom() {
|
get phantom() {
|
||||||
let phantom = this._phantom;
|
return this._phantom && Object.assign({}, this._phantom);
|
||||||
if (phantom) {
|
|
||||||
return { side: phantom.side, type: phantom.type, from: phantom.from };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPiece(side, square) {
|
getPiece(side, square) {
|
||||||
|
|
@ -427,12 +422,17 @@ class Game {
|
||||||
this._board = new Board(original._board);
|
this._board = new Board(original._board);
|
||||||
this._player = original._player;
|
this._player = original._player;
|
||||||
this._status = original._status;
|
this._status = original._status;
|
||||||
this._moves = JSON.parse(JSON.stringify(original._moves));
|
this._moves = original._moves.slice();
|
||||||
this._redo = JSON.parse(JSON.stringify(original._redo));
|
this._redo = original._redo.slice();
|
||||||
this._history = original._history;
|
this._history = original._history;
|
||||||
this._turn = original._turn;
|
this._turn = original._turn;
|
||||||
this._castling = JSON.parse(JSON.stringify(original._castling));
|
|
||||||
this._undo = original._undo;
|
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 {
|
} else {
|
||||||
this._board = new Board();
|
this._board = new Board();
|
||||||
this._player = LIGHT;
|
this._player = LIGHT;
|
||||||
|
|
@ -442,11 +442,13 @@ class Game {
|
||||||
this._history = '';
|
this._history = '';
|
||||||
this._turn = 0;
|
this._turn = 0;
|
||||||
this._undo = null;
|
this._undo = null;
|
||||||
|
this._checkCache = {};
|
||||||
|
|
||||||
/* set to false when the king or rook moves */
|
/* set to false when the king or rook moves */
|
||||||
this._castling = {};
|
this._castling = {
|
||||||
this._castling[LIGHT] = { king: true, queen: true };
|
[LIGHT]: { king: true, queen: true },
|
||||||
this._castling[DARK] = { king: true, queen: true };
|
[DARK]: { king: true, queen: true },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -504,18 +506,15 @@ class Game {
|
||||||
if (from === (side === DARK ? 'e8' : 'e1')) {
|
if (from === (side === DARK ? 'e8' : 'e1')) {
|
||||||
const row = from[1];
|
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 */
|
* Note: With any other piece the call to isInCheck() would be recursive.
|
||||||
let inCheck = () => {
|
* Kings can't capture, so their moves aren't considered for check.
|
||||||
const check = this.isInCheck(side);
|
*/
|
||||||
inCheck = () => check;
|
|
||||||
return check;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._castling[side].king &&
|
if (this._castling[side].king &&
|
||||||
board.isEmpty('f' + row) &&
|
board.isEmpty('f' + row) &&
|
||||||
board.isEmpty('g' + row)) {
|
board.isEmpty('g' + row)) {
|
||||||
if (!inCheck()) {
|
if (!this.isInCheck(side)) {
|
||||||
yield 'g' + row;
|
yield 'g' + row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -524,7 +523,7 @@ class Game {
|
||||||
board.isEmpty('d' + row) &&
|
board.isEmpty('d' + row) &&
|
||||||
board.isEmpty('c' + row) &&
|
board.isEmpty('c' + row) &&
|
||||||
board.isEmpty('b' + row)) {
|
board.isEmpty('b' + row)) {
|
||||||
if (!inCheck()) {
|
if (!this.isInCheck(side)) {
|
||||||
yield 'c' + row;
|
yield 'c' + row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -593,17 +592,89 @@ class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
isLegalMove(side, from, to, canCapture) {
|
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));
|
const legals = new Iterator(this.legalMoves(side, from, canCapture));
|
||||||
return legals.strictlyIncludes(to);
|
return legals.strictlyIncludes(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
isInCheck(_side) {
|
isInCheck(_side) {
|
||||||
if (this._status !== PLAYING || this._board.phantom) {
|
const side = _side || this._player;
|
||||||
/* can't be in check mid-move, or if the game is already over */
|
|
||||||
return false;
|
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 other = otherSide(side);
|
||||||
const kings = [...this._board.findPieces(side, KING)];
|
const kings = [...this._board.findPieces(side, KING)];
|
||||||
|
|
||||||
|
|
@ -626,16 +697,15 @@ class Game {
|
||||||
if (sim._board.getPiece(other, from) !== KING) {
|
if (sim._board.getPiece(other, from) !== KING) {
|
||||||
if (sim.isLegalMove(other, from, king, true)) {
|
if (sim.isLegalMove(other, from, king, true)) {
|
||||||
/* this piece can directly capture the king */
|
/* this piece can directly capture the king */
|
||||||
return true;
|
return recordCheck(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.push({ game: sim, from });
|
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();
|
const seen = new Set();
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const game = queue[0].game;
|
const game = queue[0].game;
|
||||||
|
|
@ -647,6 +717,14 @@ class Game {
|
||||||
/* look for another piece that can reach the king or continue the chain */
|
/* look for another piece that can reach the king or continue the chain */
|
||||||
const pairs = [...game.board.findPieces(other, true, true)];
|
const pairs = [...game.board.findPieces(other, true, true)];
|
||||||
for (const pair of pairs) {
|
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)) {
|
if (!game.isLegalMove(other, from, pair, true)) {
|
||||||
/* can't reach it */
|
/* can't reach it */
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -668,17 +746,15 @@ class Game {
|
||||||
|
|
||||||
if (game2.isLegalMove(other, PHANTOM, king, true)) {
|
if (game2.isLegalMove(other, PHANTOM, king, true)) {
|
||||||
/* we have our answer */
|
/* 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 */
|
/* didn't find anything */
|
||||||
return false;
|
return recordCheck(this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
move(from, to, meta) {
|
move(from, to, meta) {
|
||||||
|
|
@ -789,6 +865,7 @@ class Game {
|
||||||
this._player = otherSide(this._player);
|
this._player = otherSide(this._player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._checkCache = {};
|
||||||
this._moves.push(move);
|
this._moves.push(move);
|
||||||
this._redo = [];
|
this._redo = [];
|
||||||
|
|
||||||
|
|
@ -814,6 +891,7 @@ class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._undo = new Game(this);
|
this._undo = new Game(this);
|
||||||
|
this._checkCache = {};
|
||||||
this._moves.push(move);
|
this._moves.push(move);
|
||||||
this._redo = [];
|
this._redo = [];
|
||||||
this._status = ENDED;
|
this._status = ENDED;
|
||||||
|
|
@ -869,10 +947,7 @@ class Game {
|
||||||
const savedRedo = this._redo;
|
const savedRedo = this._redo;
|
||||||
|
|
||||||
/* copy all the properties from the saved prior game to this one */
|
/* copy all the properties from the saved prior game to this one */
|
||||||
const savedUndo = this._undo;
|
Object.assign(this, this._undo);
|
||||||
for (const prop in savedUndo) {
|
|
||||||
this[prop] = savedUndo[prop];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* restore the original redo history and add the undone move */
|
/* restore the original redo history and add the undone move */
|
||||||
this._redo = savedRedo;
|
this._redo = savedRedo;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue