move (de)serialization into the Game class & add rule versioning

This commit is contained in:
Jesse D. McDonald 2020-05-08 20:40:48 -05:00
parent 503a26eda3
commit b1caa69920
2 changed files with 87 additions and 57 deletions

View File

@ -31,26 +31,38 @@ const INITIAL_EDGE_ROW = ROOK + KNIGHT + BISHOP + QUEEN + KING + BISHOP + KNIGHT
const ROWS = '12345678'; const ROWS = '12345678';
const COLUMNS = 'abcdefgh'; const COLUMNS = 'abcdefgh';
/*
* Rule Version
*
* Increment this whenever a change in the rules would make some previously
* illegal moves illegal. The implementation in the Game class needs to check
* this._version and apply the appropriate rules based on which rules were in
* effect when the game was started.
*
* Version 1: Initial version. (Default if version field is missing.)
*/
const CURRENT_VERSION = 1;
function otherSide(side) { function otherSide(side) {
if (side === LIGHT) { if (side === LIGHT) {
return DARK; return DARK;
} else if (side === DARK) { } else if (side === DARK) {
return LIGHT; return LIGHT;
} else { } else {
throw { message: 'invalid side', side: side }; throw new Error(`invalid side: ${side}`);
} }
} }
function squareIndex(square) { function squareIndex(square) {
if (typeof square !== 'string' || square.length !== 2) { if (typeof square !== 'string' || square.length !== 2) {
throw { message: 'invalid square', square: square }; throw new Error(`invalid square: ${square}`);
} }
const column = COLUMNS.indexOf(square[0].toLowerCase()); const column = COLUMNS.indexOf(square[0].toLowerCase());
const row = ROWS.indexOf(square[1]); const row = ROWS.indexOf(square[1]);
if (column < 0 || row < 0) { if (column < 0 || row < 0) {
throw { message: 'invalid square', square: square }; throw new Error(`invalid square: ${square}`);
} }
return (row * 8) + column; return (row * 8) + column;
@ -72,7 +84,7 @@ function encodePiece(side, type) {
let result = ALL_PIECES.indexOf(type); let result = ALL_PIECES.indexOf(type);
if (result < 0) { if (result < 0) {
throw { message: 'invalid piece', piece: type }; throw new Error(`invalid piece: ${type}`);
} }
if (side === LIGHT) { if (side === LIGHT) {
@ -80,7 +92,7 @@ function encodePiece(side, type) {
} else if (side === DARK) { } else if (side === DARK) {
return result; return result;
} else { } else {
throw { message: 'invalid side', side: side }; throw new Error(`invalid side: ${side}`);
} }
} }
@ -92,7 +104,7 @@ function decodePiece(side, value) {
} else if (side === DARK) { } else if (side === DARK) {
sideShift = 0; sideShift = 0;
} else { } else {
throw { message: 'invalid side', side: side }; throw new Error(`invalid side: ${side}`);
} }
let pieceValue = (value >>> sideShift) & 0b1111; let pieceValue = (value >>> sideShift) & 0b1111;
@ -100,7 +112,7 @@ function decodePiece(side, value) {
if (pieceValue < ALL_PIECES.length) { if (pieceValue < ALL_PIECES.length) {
return ALL_PIECES[pieceValue]; return ALL_PIECES[pieceValue];
} else { } else {
throw { message: 'invalid encoded piece', value: pieceValue }; throw new Error(`invalid encoded piece: ${pieceValue}`);
} }
} }
@ -108,7 +120,7 @@ class Board {
constructor(original) { constructor(original) {
if (original !== null && original !== undefined) { if (original !== null && original !== undefined) {
if (!(original instanceof this.constructor)) { if (!(original instanceof this.constructor)) {
throw { message: 'can only clone from another Board instance' }; throw new TypeError(`can only clone from another Board instance`);
} }
this._board = new Uint8Array(original._board); this._board = new Uint8Array(original._board);
@ -175,7 +187,7 @@ class Board {
if (type !== EMPTY) { if (type !== EMPTY) {
if (this._phantom) { if (this._phantom) {
throw { message: 'phantom square is already occupied' }; throw new Error(`phantom square is already occupied`);
} }
this.putPiece(side, square, EMPTY); this.putPiece(side, square, EMPTY);
@ -190,17 +202,17 @@ class Board {
move(from, to) { move(from, to) {
if (this._phantom && from !== PHANTOM) { if (this._phantom && from !== PHANTOM) {
throw { message: 'must complete prior move before moving other pieces' }; throw new Error(`must complete prior move before moving other pieces`);
} }
let lightPiece = this.getPiece(LIGHT, from); let lightPiece = this.getPiece(LIGHT, from);
let darkPiece = this.getPiece(DARK, from); let darkPiece = this.getPiece(DARK, from);
if (lightPiece === EMPTY && darkPiece === EMPTY) { if (lightPiece === EMPTY && darkPiece === EMPTY) {
throw { message: 'cannot move from empty square' }; throw new Error(`cannot move from empty square: ${from}${to}`);
} else if (lightPiece !== EMPTY && darkPiece !== EMPTY) { } else if (lightPiece !== EMPTY && darkPiece !== EMPTY) {
if (!this.isEmpty(to)) { if (!this.isEmpty(to)) {
throw { message: 'cannot capture with joined pieces' }; throw new Error(`cannot capture with joined pieces`);
} }
const fromIndex = squareIndex(from); const fromIndex = squareIndex(from);
@ -214,7 +226,7 @@ class Board {
const displaced = this.getPiece(moving, to); const displaced = this.getPiece(moving, to);
if (displaced !== EMPTY && this.getPiece(other, to) === EMPTY) { if (displaced !== EMPTY && this.getPiece(other, to) === EMPTY) {
throw { message: 'cannot join with piece of same color' }; throw new Error(`cannot join with piece of same color`);
} }
if (from === PHANTOM) { if (from === PHANTOM) {
@ -462,9 +474,38 @@ function diffHistory(game1, game2) {
class Game { class Game {
constructor(original) { constructor(original) {
if (typeof original === 'string') {
const moves = JSON.parse(original);
original = new Game();
if ('version' in moves === false) {
original._version = 1;
} else if (!Number.isInteger(moves.version) ||
moves.version < 1 || moves.version > CURRENT_VERSION) {
throw new Error('invalid version');
} else {
original._version = moves.version;
}
for (const move of moves.past) {
original.replayMove(move);
}
let n = 0;
for (const move of moves.future.slice().reverse()) {
original.replayMove(move);
n += 1;
}
for (let i = 0; i < n; ++i) {
original.undo();
}
}
if (original !== undefined) { if (original !== undefined) {
if (!(original instanceof this.constructor)) { if (!(original instanceof this.constructor)) {
throw { message: 'can only clone from another Game instance' }; throw new TypeError(`can only clone from another Game instance`);
} }
Object.assign(this, original, { Object.assign(this, original, {
@ -488,6 +529,7 @@ class Game {
this._undo = null; this._undo = null;
this._checkCache = {}; this._checkCache = {};
this._ignoreCheck = false; this._ignoreCheck = false;
this._version = CURRENT_VERSION;
/* set to false when the king or rook moves */ /* set to false when the king or rook moves */
this._castling = { this._castling = {
@ -525,6 +567,18 @@ class Game {
this._ignoreCheck = !!value; this._ignoreCheck = !!value;
} }
get version() {
return this._version;
}
toJSON() {
return JSON.stringify({
past: this._moves,
future: this._redo,
version: this._version,
});
}
canCapture(side, from) { canCapture(side, from) {
if (from === PHANTOM) { if (from === PHANTOM) {
return true; return true;
@ -732,7 +786,7 @@ class Game {
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 per side' }; throw new Error(`there should be exactly one king per side`);
} }
const king = kings[0]; const king = kings[0];
@ -894,7 +948,7 @@ class Game {
move(from, to, meta) { move(from, to, meta) {
if (this._status !== PLAYING) { if (this._status !== PLAYING) {
throw { message: "can't move, game is already over" }; throw new Error(`can't move, game is already over`);
} }
const side = this._player; const side = this._player;
@ -906,13 +960,13 @@ class Game {
const alongside = board.getPiece(other, from); const alongside = board.getPiece(other, from);
if (from === PHANTOM && !board.phantom) { if (from === PHANTOM && !board.phantom) {
throw { message: "attempted to continue a completed move" }; throw new Error(`attempted to continue a completed move`);
} }
const fromSquare = (from === PHANTOM) ? board.phantom.from : from; const fromSquare = (from === PHANTOM) ? board.phantom.from : from;
if (!this.isLegalMove(side, from, to, alongside === EMPTY)) { if (!this.isLegalMove(side, from, to, alongside === EMPTY)) {
throw { message: "illegal move", side: side, from: fromSquare, to: to }; throw new Error(`illegal move by ${side} side: ${fromSquare}${to}`);
} }
const move = { const move = {
@ -1053,7 +1107,7 @@ class Game {
resign(meta) { resign(meta) {
if (this._status !== PLAYING) { if (this._status !== PLAYING) {
throw { message: "can't resign, game is already over" }; throw new Error(`can't resign, game is already over`);
} }
const move = { const move = {
@ -1101,8 +1155,8 @@ class Game {
} }
replayMove(move) { replayMove(move) {
if (!move || move.side !== this._player) { if (otherSide(move.side) === this._player) {
throw { message: "other player's move", move: move }; throw new Error(`other player's move`);
} }
if (move.resign) { if (move.resign) {
@ -1164,6 +1218,7 @@ class Game {
renderHistory() { renderHistory() {
if (this._history === undefined) { if (this._history === undefined) {
const replay = new Game(); const replay = new Game();
replay._version = this._version;
for (const move of this._moves) { for (const move of this._moves) {
replay.replayMove(move); replay.replayMove(move);
} }
@ -1190,6 +1245,9 @@ export default {
/* Coordinates */ /* Coordinates */
ROWS, COLUMNS, PHANTOM, ROWS, COLUMNS, PHANTOM,
/* Versioning */
CURRENT_VERSION,
/* Miscellaneous */ /* Miscellaneous */
Util: { otherSide, offsetSquare, diffHistory }, Util: { otherSide, offsetSquare, diffHistory },
}; };

View File

@ -50,7 +50,7 @@ $(function (){
} else if (type === PS.PAWN) { } else if (type === PS.PAWN) {
return 'p'; return 'p';
} else { } else {
throw { message: 'unknown piece type', type: type }; throw new Error(`unknown piece type: ${type}`);
} }
} }
@ -60,7 +60,7 @@ $(function (){
} else if (side === PS.DARK) { } else if (side === PS.DARK) {
return 'd'; return 'd';
} else { } else {
throw { message: 'unknown side', side: side }; throw new Error(`unknown side: ${side}`);
} }
} }
@ -576,9 +576,8 @@ $(function (){
function putState() { function putState() {
const boardElem = $('#cb_board'); const boardElem = $('#cb_board');
const gameId = boardElem.data('gameId'); const gameId = boardElem.data('gameId');
const moves = { past: currentGame.moves, future: currentGame.redoMoves };
boardElem.data('last_state', currentGame.moves); boardElem.data('last_state', currentGame.moves);
putMeta({ board: moves }); putMeta({ board: JSON.parse(currentGame.toJSON()) });
} }
function putMeta(extra) { function putMeta(extra) {
@ -649,40 +648,13 @@ $(function (){
cancelGameCallback = IO.onGameUpdate(newId, function(data, gameId) { cancelGameCallback = IO.onGameUpdate(newId, function(data, gameId) {
if (data.modified > $('#cb_board').data('modified')) { if (data.modified > $('#cb_board').data('modified')) {
const board = data.board || { past: [], future: [] };
try { try {
const moves = { past: [...board.past], future: [...board.future] }; const newGame = new PS.Game(JSON.stringify(data.board));
const oldState = { past: currentGame.moves, future: currentGame.redoMoves }; const newState = JSON.parse(newGame.toJSON());
const oldState = JSON.parse(currentGame.toJSON());
if (!deepEqual(moves, oldState)) {
debug('got board', moves);
const newGame = new PS.Game();
try {
for (const move of moves.past) {
newGame.replayMove(move);
}
let n = 0;
try {
for (const move of moves.future.slice().reverse()) {
newGame.replayMove(move);
n += 1;
}
} catch (err) {
debug('Error replaying board redo state', err);
}
for (let i = 0; i < n; ++i) {
newGame.undo();
}
} catch (err) {
debug('Error replaying board state', err);
}
if (!deepEqual(newState, oldState)) {
debug('got board', newGame.moves);
setCurrentGame(newGame, newGame.moves.length > currentGame.moves.length); setCurrentGame(newGame, newGame.moves.length > currentGame.moves.length);
} }
} catch (err) { } catch (err) {