From 7c04b9ba094cb71caff49c0cd5d980301f56f6dc Mon Sep 17 00:00:00 2001 From: Jesse McDonald Date: Sun, 5 Apr 2020 17:25:06 -0500 Subject: [PATCH] performance improvements for determination of check --- js/pacosako.js | 151 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 113 insertions(+), 38 deletions(-) diff --git a/js/pacosako.js b/js/pacosako.js index cb70079..dff585e 100644 --- a/js/pacosako.js +++ b/js/pacosako.js @@ -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;