add ability to detect obvious check (chains of 6 moves or less)
This commit is contained in:
parent
75b66e6ce3
commit
fcb14d489c
215
js/iterator.js
215
js/iterator.js
|
|
@ -1,63 +1,120 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
export const IteratorMixin = {
|
function identity(x) {
|
||||||
map: function* map(f) {
|
return x;
|
||||||
let y;
|
}
|
||||||
while (!(y = this.next()).done) {
|
|
||||||
yield f(y.value);
|
|
||||||
}
|
|
||||||
return y.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: function* filter(f) {
|
export class Iterator {
|
||||||
let y;
|
constructor(from) {
|
||||||
while (!(y = this.next()).done) {
|
let next;
|
||||||
if (f(y.value)) {
|
|
||||||
yield y.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return y.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
take: function* take(limit) {
|
if (Symbol.iterator in from.__proto__) {
|
||||||
let remaining = Number(limit) & -1;
|
const iter = from.__proto__[Symbol.iterator].call(from);
|
||||||
let y;
|
next = iter.next.bind(iter);
|
||||||
while (remaining > 0 && !(y = this.next()).done) {
|
} else if (typeof from === 'function') {
|
||||||
yield y.value;
|
next = from;
|
||||||
remaining -= 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
drop: function* drop(limit) {
|
|
||||||
let remaining = Number(limit) & -1;
|
|
||||||
let y;
|
|
||||||
while (!(y = this.next()).done) {
|
|
||||||
if (remaining <= 0) {
|
|
||||||
yield y.value;
|
|
||||||
} else {
|
} 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;
|
remaining -= 1;
|
||||||
}
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
asIndexedPairs: function* asIndexedPairs() {
|
asIndexedPairs() {
|
||||||
|
const next = this.next;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let y;
|
return new Iterator(function() {
|
||||||
while (!(y = this.next()).done) {
|
const y = next();
|
||||||
yield [index++, x];
|
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') {
|
if (typeof state === 'undefined') {
|
||||||
const first = this.next();
|
const first = this.next();
|
||||||
if (first.done) {
|
if (first.done) {
|
||||||
|
|
@ -65,52 +122,60 @@ export const IteratorMixin = {
|
||||||
}
|
}
|
||||||
state = first.value;
|
state = first.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
let y;
|
let y;
|
||||||
while (!(y = this.next()).done) {
|
while (!(y = this.next()).done) {
|
||||||
state = f(state, y.value);
|
state = f(state, y.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
},
|
}
|
||||||
|
|
||||||
toArray: function toArray() {
|
toArray() {
|
||||||
return [...this];
|
return [...this];
|
||||||
},
|
}
|
||||||
|
|
||||||
forEach: function* forEach(f) {
|
forEach(f) {
|
||||||
let y;
|
let y;
|
||||||
while (!(y = this.next()).done) {
|
while (!(y = this.next()).done) {
|
||||||
f(y.value);
|
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 */
|
/* 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;
|
let y;
|
||||||
while (!(y = iter.next()).done) {
|
while (!(y = this.next()).done) {
|
||||||
if (y.value) {
|
if (f(y.value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
every: function every(f) {
|
every(f) {
|
||||||
/* extension: if f is undefined, assume identity function */
|
/* 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;
|
let y;
|
||||||
while (!(y = iter.next()).done) {
|
while (!(y = this.next()).done) {
|
||||||
if (!y.value) {
|
if (!f(y.value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
|
|
||||||
find: function find(f) {
|
find(f) {
|
||||||
|
/* extension: if f is undefined, return the first 'truthy' value */
|
||||||
if (typeof f === undefined) {
|
if (typeof f === undefined) {
|
||||||
/* extension */
|
f = identity;
|
||||||
f = function identity(x) { return x; };
|
|
||||||
}
|
}
|
||||||
let y;
|
let y;
|
||||||
while (!(y = this.next()).done) {
|
while (!(y = this.next()).done) {
|
||||||
|
|
@ -118,19 +183,17 @@ export const IteratorMixin = {
|
||||||
return y.value;
|
return y.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
/* extension */
|
/* extension */
|
||||||
includes: function includes(x) {
|
includes(x) {
|
||||||
return IteratorMixin.some.call(this, function equalsX(y) { return y === 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;
|
export default Iterator;
|
||||||
|
|
|
||||||
337
js/pacosako.js
337
js/pacosako.js
|
|
@ -128,7 +128,14 @@ class Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
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() {
|
get phantom() {
|
||||||
|
|
@ -302,6 +309,114 @@ const NBSP = '\u00a0'; /* non-breaking space */
|
||||||
const SHY = '\u00ad' /* soft hyphen */
|
const SHY = '\u00ad' /* soft hyphen */
|
||||||
const ZWSP = '\u200b'; /* zero-width space */
|
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 {
|
class Game {
|
||||||
constructor(original) {
|
constructor(original) {
|
||||||
if (original !== undefined) {
|
if (original !== undefined) {
|
||||||
|
|
@ -314,13 +429,19 @@ class Game {
|
||||||
this._status = original._status;
|
this._status = original._status;
|
||||||
this._moves = JSON.parse(JSON.stringify(original._moves));
|
this._moves = JSON.parse(JSON.stringify(original._moves));
|
||||||
this._redo = JSON.parse(JSON.stringify(original._redo));
|
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._castling = JSON.parse(JSON.stringify(original._castling));
|
||||||
|
this._undo = original._undo;
|
||||||
} else {
|
} else {
|
||||||
this._board = new Board();
|
this._board = new Board();
|
||||||
this._player = LIGHT;
|
this._player = LIGHT;
|
||||||
this._status = PLAYING;
|
this._status = PLAYING;
|
||||||
this._moves = [];
|
this._moves = [];
|
||||||
this._redo = [];
|
this._redo = [];
|
||||||
|
this._history = '';
|
||||||
|
this._turn = 0;
|
||||||
|
this._undo = null;
|
||||||
|
|
||||||
/* set to false when the king or rook moves */
|
/* set to false when the king or rook moves */
|
||||||
this._castling = {};
|
this._castling = {};
|
||||||
|
|
@ -460,19 +581,88 @@ class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
isLegalMove(side, from, to, canCapture) {
|
isLegalMove(side, from, to, canCapture) {
|
||||||
const legals = this.legalMoves(side, from, canCapture);
|
const legals = new Iterator(this.legalMoves(side, from, canCapture));
|
||||||
return Iterator.some(legals, (where) => where === to);
|
return legals.strictlyIncludes(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
isInCheck(_side) {
|
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 side = _side || this._player;
|
||||||
|
const other = otherSide(side);
|
||||||
const kings = [...this._board.findPieces(side, KING)];
|
const kings = [...this._board.findPieces(side, KING)];
|
||||||
|
|
||||||
if (kings.length !== 1) {
|
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];
|
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) {
|
move(from, to, meta) {
|
||||||
|
|
@ -525,6 +715,7 @@ class Game {
|
||||||
move.meta = JSON.parse(JSON.stringify(meta));
|
move.meta = JSON.parse(JSON.stringify(meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._undo = new Game(this);
|
||||||
board.move(from, to);
|
board.move(from, to);
|
||||||
|
|
||||||
if (type === KING) {
|
if (type === KING) {
|
||||||
|
|
@ -582,12 +773,14 @@ class Game {
|
||||||
this._player = otherSide(this._player);
|
this._player = otherSide(this._player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._moves.push(move);
|
||||||
|
this._redo = [];
|
||||||
|
|
||||||
if (took === KING) {
|
if (took === KING) {
|
||||||
this._status = ENDED;
|
this._status = ENDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._moves.push(move);
|
addHistory(this);
|
||||||
this._redo = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resign(meta) {
|
resign(meta) {
|
||||||
|
|
@ -604,9 +797,12 @@ class Game {
|
||||||
move.meta = meta;
|
move.meta = meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._status = ENDED;
|
this._undo = new Game(this);
|
||||||
this._moves.push(move);
|
this._moves.push(move);
|
||||||
this._redo = [];
|
this._redo = [];
|
||||||
|
this._status = ENDED;
|
||||||
|
|
||||||
|
addHistory(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastMove() {
|
get lastMove() {
|
||||||
|
|
@ -656,15 +852,10 @@ class Game {
|
||||||
const lastMove = this._moves[this._moves.length - 1];
|
const lastMove = this._moves[this._moves.length - 1];
|
||||||
const savedRedo = this._redo;
|
const savedRedo = this._redo;
|
||||||
|
|
||||||
/* replay all moves except the last in a new game object */
|
/* copy all the properties from the saved prior game to this one */
|
||||||
const replay = new this.constructor();
|
const savedUndo = this._undo;
|
||||||
for (let i = 0; i < this._moves.length - 1; ++i) {
|
for (const prop in savedUndo) {
|
||||||
replay.replayMove(this._moves[i]);
|
this[prop] = savedUndo[prop];
|
||||||
}
|
|
||||||
|
|
||||||
/* copy all the properties from the replayed game to this one */
|
|
||||||
for (const prop in replay) {
|
|
||||||
this[prop] = replay[prop];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* restore the original redo history and add the undone move */
|
/* restore the original redo history and add the undone move */
|
||||||
|
|
@ -685,122 +876,28 @@ class Game {
|
||||||
this._redo = [];
|
this._redo = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
countTurns() {
|
get turns() {
|
||||||
let n = 0;
|
return this._turn;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player = move.side;
|
get history() {
|
||||||
|
return this._history;
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
dropHistory() {
|
||||||
|
this._history = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHistory() {
|
renderHistory() {
|
||||||
let replay = new Game();
|
if (this._history === undefined) {
|
||||||
let result = '';
|
const replay = new Game();
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
for (const move of this._moves) {
|
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);
|
replay.replayMove(move);
|
||||||
}
|
}
|
||||||
|
this._history = replay._history;
|
||||||
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 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return this._history;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,15 @@ $(function (){
|
||||||
const from = piece.data('location');
|
const from = piece.data('location');
|
||||||
const legals = currentGame.legalMoves(side, from);
|
const legals = currentGame.legalMoves(side, from);
|
||||||
for (const there of legals) {
|
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);
|
const square = cbSquare(there);
|
||||||
square.addClass('cb-legal')
|
square.addClass('cb-legal')
|
||||||
if (event === 'drag') {
|
if (event === 'drag') {
|
||||||
|
|
@ -336,13 +345,16 @@ $(function (){
|
||||||
}
|
}
|
||||||
msg += (winner === PS.LIGHT ? 'Light' : 'Dark') + ' player won!';
|
msg += (winner === PS.LIGHT ? 'Light' : 'Dark') + ' player won!';
|
||||||
} else if (playing) {
|
} else if (playing) {
|
||||||
|
if (game.isInCheck()) {
|
||||||
|
msg += 'Check! ';
|
||||||
|
}
|
||||||
msg += (game.player === PS.LIGHT ? 'Light' : 'Dark') + ' player\'s turn.';
|
msg += (game.player === PS.LIGHT ? 'Light' : 'Dark') + ' player\'s turn.';
|
||||||
} else {
|
} else {
|
||||||
msg += 'Game ended in a draw.';
|
msg += 'Game ended in a draw.';
|
||||||
}
|
}
|
||||||
$('#cb_message').text(msg);
|
$('#cb_message').text(msg);
|
||||||
|
|
||||||
$('#cb_history').text(game.renderHistory());
|
$('#cb_history').text(game.history || '');
|
||||||
|
|
||||||
$('#cb_nav_first').attr('disabled', !game.canUndo);
|
$('#cb_nav_first').attr('disabled', !game.canUndo);
|
||||||
$('#cb_nav_prev_turn').attr('disabled', !game.canUndo);
|
$('#cb_nav_prev_turn').attr('disabled', !game.canUndo);
|
||||||
|
|
@ -438,7 +450,7 @@ $(function (){
|
||||||
const gameId = $('#cb_board').data('gameId');
|
const gameId = $('#cb_board').data('gameId');
|
||||||
const lightName = $('#cb_light_name').val();
|
const lightName = $('#cb_light_name').val();
|
||||||
const darkName = $('#cb_dark_name').val();
|
const darkName = $('#cb_dark_name').val();
|
||||||
const turns = currentGame.countTurns();
|
const turns = currentGame.turns;
|
||||||
const winner = currentGame.winner;
|
const winner = currentGame.winner;
|
||||||
const lastMove = currentGame.lastMove || {};
|
const lastMove = currentGame.lastMove || {};
|
||||||
const lastMeta = lastMove.meta || {};
|
const lastMeta = lastMove.meta || {};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue