add ability to detect obvious check (chains of 6 moves or less)

This commit is contained in:
Jesse D. McDonald 2020-04-05 04:42:10 -05:00
parent 75b66e6ce3
commit fcb14d489c
3 changed files with 374 additions and 202 deletions

View File

@ -1,63 +1,120 @@
'use strict';
export const IteratorMixin = {
map: function* map(f) {
let y;
while (!(y = this.next()).done) {
yield f(y.value);
}
return y.value;
},
function identity(x) {
return x;
}
filter: function* filter(f) {
let y;
while (!(y = this.next()).done) {
if (f(y.value)) {
yield y.value;
}
}
return y.value;
},
export class Iterator {
constructor(from) {
let next;
take: function* take(limit) {
let remaining = Number(limit) & -1;
let y;
while (remaining > 0 && !(y = this.next()).done) {
yield y.value;
remaining -= 1;
}
},
drop: function* drop(limit) {
let remaining = Number(limit) & -1;
let y;
while (!(y = this.next()).done) {
if (remaining <= 0) {
yield y.value;
if (Symbol.iterator in from.__proto__) {
const iter = from.__proto__[Symbol.iterator].call(from);
next = iter.next.bind(iter);
} else if (typeof from === 'function') {
next = from;
} else {
throw new TypeError('Iterator class needs iterable input');
}
Object.defineProperty(this, 'next', {
value: next,
writable: false,
enumerable: true,
configurable: false,
});
}
[Symbol.iterator]() {
return this;
}
map(f) {
const next = this.next;
return new Iterator(function() {
const y = next();
if (y.done) {
return y;
} else {
return { value: f(y.value), done: false };
}
});
}
filter(f) {
const next = this.next;
return new Iterator(function() {
let y;
while (!(y = next()).done && !f(y.value)) {
continue;
}
return y;
});
}
take(limit) {
let remaining = Number(limit) & -1;
const next = this.next;
return new Iterator(function() {
if (remaining > 0) {
remaining -= 1;
return next();
} else {
return { value: undefined, done: true };
}
});
}
drop(limit) {
let remaining = Number(limit) & -1;
const next = this.next;
return new Iterator(function() {
while (remaining > 0 && !next().done) {
remaining -= 1;
}
return next();
});
}
},
asIndexedPairs: function* asIndexedPairs() {
asIndexedPairs() {
const next = this.next;
let index = 0;
let y;
while (!(y = this.next()).done) {
yield [index++, x];
return new Iterator(function() {
const y = next();
if (y.done) {
return y;
} else {
return { value: [index++, y.value], done: false };
}
return y.value;
},
flatMap: function* flatMap(f) {
let y;
while (!(y = this.next()).done) {
yield* f(y.value);
});
}
return y.value;
},
reduce: function reduce(f, state) {
flatMap(f) {
const next = this.next;
let innerNext;
return new Iterator(function() {
for (;;) {
if (innerNext) {
let y = innerNext();
if (!y.done) {
return y;
}
}
let z = next();
if (z.done) {
innerNext = undefined;
return z;
}
const iter = y.value.__proto__[Symbol.iterator].call(y.value);
innerNext = iter.next.bind(iter);
}
});
}
reduce(f, state) {
if (typeof state === 'undefined') {
const first = this.next();
if (first.done) {
@ -65,52 +122,60 @@ export const IteratorMixin = {
}
state = first.value;
}
let y;
while (!(y = this.next()).done) {
state = f(state, y.value);
}
return state;
},
}
toArray: function toArray() {
toArray() {
return [...this];
},
}
forEach: function* forEach(f) {
forEach(f) {
let y;
while (!(y = this.next()).done) {
f(y.value);
}
},
/* extension: return final value from underlying iterator */
return y.value;
}
some: function some(f) {
some(f) {
/* extension: if f is undefined, assume identity function */
let iter = (typeof f === 'undefined') ? this : IteratorMixin.map.call(this, f);
if (typeof f === 'undefined') {
f = identity;
}
let y;
while (!(y = iter.next()).done) {
if (y.value) {
while (!(y = this.next()).done) {
if (f(y.value)) {
return true;
}
}
return false;
},
}
every: function every(f) {
every(f) {
/* extension: if f is undefined, assume identity function */
let iter = (typeof f === 'undefined') ? this : IteratorMixin.map.call(this, f);
if (typeof f === 'undefined') {
f = identity;
}
let y;
while (!(y = iter.next()).done) {
if (!y.value) {
while (!(y = this.next()).done) {
if (!f(y.value)) {
return false;
}
}
return true;
},
}
find: function find(f) {
find(f) {
/* extension: if f is undefined, return the first 'truthy' value */
if (typeof f === undefined) {
/* extension */
f = function identity(x) { return x; };
f = identity;
}
let y;
while (!(y = this.next()).done) {
@ -118,19 +183,17 @@ export const IteratorMixin = {
return y.value;
}
}
},
}
/* extension */
includes: function includes(x) {
return IteratorMixin.some.call(this, function equalsX(y) { return y === x; });
},
includes(x) {
return this.some(function matches(y) { return y == x; });
}
/* extension */
strictlyIncludes(x) {
return this.some(function matches(y) { return y === x; });
}
};
export const Iterator = {};
for (const fn in IteratorMixin) {
Iterator[fn] = function() {
return IteratorMixin[fn].call(...arguments);
};
}
export default Iterator;

View File

@ -128,7 +128,14 @@ class Board {
}
toString() {
return new Buffer(this._board).toString('hex')
const bufferStr = new Buffer(this._board).toString('hex');
if (this._phantom) {
const phantom = this._phantom;
const phantomStr = ':' + phantom.side[0] + phantom.type + phantom.from;
return bufferStr + phantomStr;
} else {
return bufferStr;
}
}
get phantom() {
@ -302,6 +309,114 @@ const NBSP = '\u00a0'; /* non-breaking space */
const SHY = '\u00ad' /* soft hyphen */
const ZWSP = '\u200b'; /* zero-width space */
function addHistory(game) {
const prior = game._undo;
const move = game.lastMove;
if (game._history === undefined) {
if (!move.phantom && !move.resign && move.side === LIGHT) {
game._turn += 1;
}
return;
}
let result = '';
if (move.phantom) {
result += SHY + '*';
} else if (!move.resign) {
if (game._turn > 0 || move.side === DARK) {
result += ' ';
}
if (move.side === LIGHT) {
game._turn += 1;
result += String(game._turn) + '.' + NBSP;
}
}
if (move.castle) {
result += 'O-O';
} else if (move.queen_castle) {
result += 'O-O-O';
} else if (move.side && move.type && move.from && move.to) {
let piece = '';
if (move.alongside) {
if (move.side === LIGHT) {
piece = move.type.toUpperCase() + move.alongside.toUpperCase();
} else {
piece = move.alongside.toUpperCase() + move.type.toUpperCase();
}
} else {
piece = move.type === PAWN ? '' : move.type.toUpperCase();
}
/* the second condition below is for en passant of a joined piece */
if (!move.phantom || move.from !== prior.lastMove.to) {
const sameKind = prior.board.findPieces(move.side, move.type, move.alongside || EMPTY);
const legalFrom = [];
let sameFile = 0; /* column / letter */
let sameRank = 0; /* row / number */
for (const where of sameKind) {
if (prior.isLegalMove(move.side, where, move.to, true)) {
legalFrom.push(where);
if (where[0] === move.from[0]) {
sameFile += 1;
}
if (where[1] === move.from[1]) {
sameRank += 1;
}
}
}
/* always disambiguate captures by pawns (standard convention) */
if (legalFrom.length !== 1 || (move.type === PAWN && move.took)) {
/* append file, rank, or both to disambiguate */
if (sameFile === 1) {
piece += move.from[0];
} else if (sameRank === 1) {
piece += move.from[1];
} else {
piece += move.from;
}
}
}
const took = move.took ? 'x' : '';
result += piece + took + move.to;
}
if (move.en_passant) {
result += 'e.p.';
}
if (move.promotion) {
result += '(' + move.promotion.toUpperCase() + ')';
}
if (move.took === KING) {
result += '#';
} else if (game.isInCheck()) {
result += '+';
}
let winner = game.winner;
if (winner === LIGHT) {
result += ' 1-0';
} else if (winner === DARK) {
result += ' 0-1';
} else if (game.status !== PLAYING) {
result += ' \u00bd-\u00bd'; /* 1/2-1/2 */
}
game._history += result;
}
class Game {
constructor(original) {
if (original !== undefined) {
@ -314,13 +429,19 @@ class Game {
this._status = original._status;
this._moves = JSON.parse(JSON.stringify(original._moves));
this._redo = JSON.parse(JSON.stringify(original._redo));
this._history = original._history;
this._turn = original._turn;
this._castling = JSON.parse(JSON.stringify(original._castling));
this._undo = original._undo;
} else {
this._board = new Board();
this._player = LIGHT;
this._status = PLAYING;
this._moves = [];
this._redo = [];
this._history = '';
this._turn = 0;
this._undo = null;
/* set to false when the king or rook moves */
this._castling = {};
@ -460,19 +581,88 @@ class Game {
}
isLegalMove(side, from, to, canCapture) {
const legals = this.legalMoves(side, from, canCapture);
return Iterator.some(legals, (where) => where === to);
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;
const other = otherSide(side);
const kings = [...this._board.findPieces(side, KING)];
if (kings.length !== 1) {
throw { message: "there should be exactly one king" };
throw { message: 'there should be exactly one king per side' };
}
const king = kings[0];
/* check is evaluated as if the current player passed without moving */
const sim = new Game(this);
sim.dropHistory();
sim._player = other;
/* start with the opponent's unpaired pieces, the ones that can capture */
let queue = [];
for (const from of sim._board.findPieces(other, true, false)) {
if (sim.isLegalMove(other, from, king, true)) {
/* this piece can directly capture the king */
return 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();
while (queue.length > 0) {
const game = queue[0].game;
const from = queue[0].from;
queue = queue.slice(1);
seen.add(game.toString());
/* 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 (!game.isLegalMove(other, from, pair, true)) {
/* can't reach it */
continue;
}
const game2 = new Game(game);
try {
game2.move(from, pair);
} catch (err) {
/* internal error, but keep looking at the rest of the queue */
console.log('isInCheck:', err);
continue;
}
if (seen.has(game2._board.toString())) {
/* we've already been here via another path */
continue;
}
if (game2.isLegalMove(other, PHANTOM, king, true)) {
/* we have our answer */
return true;
}
if (game2._moves.length < moveLimit) {
queue.push({ game: game2, from: PHANTOM });
}
}
}
/* didn't find anything */
return false;
}
move(from, to, meta) {
@ -525,6 +715,7 @@ class Game {
move.meta = JSON.parse(JSON.stringify(meta));
}
this._undo = new Game(this);
board.move(from, to);
if (type === KING) {
@ -582,12 +773,14 @@ class Game {
this._player = otherSide(this._player);
}
this._moves.push(move);
this._redo = [];
if (took === KING) {
this._status = ENDED;
}
this._moves.push(move);
this._redo = [];
addHistory(this);
}
resign(meta) {
@ -604,9 +797,12 @@ class Game {
move.meta = meta;
}
this._status = ENDED;
this._undo = new Game(this);
this._moves.push(move);
this._redo = [];
this._status = ENDED;
addHistory(this);
}
get lastMove() {
@ -656,15 +852,10 @@ class Game {
const lastMove = this._moves[this._moves.length - 1];
const savedRedo = this._redo;
/* replay all moves except the last in a new game object */
const replay = new this.constructor();
for (let i = 0; i < this._moves.length - 1; ++i) {
replay.replayMove(this._moves[i]);
}
/* copy all the properties from the replayed game to this one */
for (const prop in replay) {
this[prop] = replay[prop];
/* 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];
}
/* restore the original redo history and add the undone move */
@ -685,122 +876,28 @@ class Game {
this._redo = [];
}
countTurns() {
let n = 0;
let player = null;
for (const move of this._moves) {
/* multiple consecutive moves by the same player are a single turn */
if (move.side !== player) {
++n;
get turns() {
return this._turn;
}
player = move.side;
get history() {
return this._history;
}
return n;
dropHistory() {
this._history = undefined;
}
renderHistory() {
let replay = new Game();
let result = '';
let n = 0;
if (this._history === undefined) {
const replay = new Game();
for (const move of this._moves) {
if (move.phantom) {
result += SHY + '*';
} else if (!move.resign) {
if (n > 0 || move.side === DARK) {
result += ' ';
}
if (move.side === LIGHT) {
++n;
result += String(n) + '.' + NBSP;
}
}
if (move.castle) {
result += 'O-O';
} else if (move.queen_castle) {
result += 'O-O-O';
} else if (move.side && move.type && move.from && move.to) {
let piece = '';
if (move.alongside) {
if (move.side === LIGHT) {
piece = move.type.toUpperCase() + move.alongside.toUpperCase();
} else {
piece = move.alongside.toUpperCase() + move.type.toUpperCase();
}
} else {
piece = move.type === PAWN ? '' : move.type.toUpperCase();
}
/* the second condition below is for en passant of a joined piece */
if (!move.phantom || move.from !== replay.lastMove.to) {
const sameKind = replay.board.findPieces(move.side, move.type, move.alongside || EMPTY);
const legalFrom = [];
let sameFile = 0; /* column / letter */
let sameRank = 0; /* row / number */
for (const where of sameKind) {
if (replay.isLegalMove(move.side, where, move.to, true)) {
legalFrom.push(where);
if (where[0] === move.from[0]) {
sameFile += 1;
}
if (where[1] === move.from[1]) {
sameRank += 1;
}
}
}
/* always disambiguate captures by pawns (standard convention) */
if (legalFrom.length !== 1 || (move.type === PAWN && move.took)) {
/* append file, rank, or both to disambiguate */
if (sameFile === 1) {
piece += move.from[0];
} else if (sameRank === 1) {
piece += move.from[1];
} else {
piece += move.from;
}
}
}
const took = move.took ? 'x' : '';
result += piece + took + move.to;
}
if (move.en_passant) {
result += 'e.p.';
}
if (move.promotion) {
result += '(' + move.promotion.toUpperCase() + ')';
}
if (move.took === KING) {
result += '#';
}
replay.replayMove(move);
}
let winner = replay.winner;
if (winner === LIGHT) {
result += ' 1-0';
} else if (winner === DARK) {
result += ' 0-1';
} else if (replay.status !== PLAYING) {
result += ' \u00bd-\u00bd'; /* 1/2-1/2 */
this._history = replay._history;
}
return result;
return this._history;
}
}

View File

@ -144,6 +144,15 @@ $(function (){
const from = piece.data('location');
const legals = currentGame.legalMoves(side, from);
for (const there of legals) {
try {
const preview = new PS.Game(currentGame);
preview.dropHistory();
preview.move(from, there);
if (preview.isInCheck(side)) {
continue;
}
} catch (err) {}
const square = cbSquare(there);
square.addClass('cb-legal')
if (event === 'drag') {
@ -336,13 +345,16 @@ $(function (){
}
msg += (winner === PS.LIGHT ? 'Light' : 'Dark') + ' player won!';
} else if (playing) {
if (game.isInCheck()) {
msg += 'Check! ';
}
msg += (game.player === PS.LIGHT ? 'Light' : 'Dark') + ' player\'s turn.';
} else {
msg += 'Game ended in a draw.';
}
$('#cb_message').text(msg);
$('#cb_history').text(game.renderHistory());
$('#cb_history').text(game.history || '');
$('#cb_nav_first').attr('disabled', !game.canUndo);
$('#cb_nav_prev_turn').attr('disabled', !game.canUndo);
@ -438,7 +450,7 @@ $(function (){
const gameId = $('#cb_board').data('gameId');
const lightName = $('#cb_light_name').val();
const darkName = $('#cb_dark_name').val();
const turns = currentGame.countTurns();
const turns = currentGame.turns;
const winner = currentGame.winner;
const lastMove = currentGame.lastMove || {};
const lastMeta = lastMove.meta || {};