move (de)serialization into the Game class & add rule versioning
This commit is contained in:
parent
503a26eda3
commit
b1caa69920
100
js/pacosako.js
100
js/pacosako.js
|
|
@ -31,26 +31,38 @@ const INITIAL_EDGE_ROW = ROOK + KNIGHT + BISHOP + QUEEN + KING + BISHOP + KNIGHT
|
|||
const ROWS = '12345678';
|
||||
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) {
|
||||
if (side === LIGHT) {
|
||||
return DARK;
|
||||
} else if (side === DARK) {
|
||||
return LIGHT;
|
||||
} else {
|
||||
throw { message: 'invalid side', side: side };
|
||||
throw new Error(`invalid side: ${side}`);
|
||||
}
|
||||
}
|
||||
|
||||
function squareIndex(square) {
|
||||
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 row = ROWS.indexOf(square[1]);
|
||||
|
||||
if (column < 0 || row < 0) {
|
||||
throw { message: 'invalid square', square: square };
|
||||
throw new Error(`invalid square: ${square}`);
|
||||
}
|
||||
|
||||
return (row * 8) + column;
|
||||
|
|
@ -72,7 +84,7 @@ function encodePiece(side, type) {
|
|||
let result = ALL_PIECES.indexOf(type);
|
||||
|
||||
if (result < 0) {
|
||||
throw { message: 'invalid piece', piece: type };
|
||||
throw new Error(`invalid piece: ${type}`);
|
||||
}
|
||||
|
||||
if (side === LIGHT) {
|
||||
|
|
@ -80,7 +92,7 @@ function encodePiece(side, type) {
|
|||
} else if (side === DARK) {
|
||||
return result;
|
||||
} 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) {
|
||||
sideShift = 0;
|
||||
} else {
|
||||
throw { message: 'invalid side', side: side };
|
||||
throw new Error(`invalid side: ${side}`);
|
||||
}
|
||||
|
||||
let pieceValue = (value >>> sideShift) & 0b1111;
|
||||
|
|
@ -100,7 +112,7 @@ function decodePiece(side, value) {
|
|||
if (pieceValue < ALL_PIECES.length) {
|
||||
return ALL_PIECES[pieceValue];
|
||||
} else {
|
||||
throw { message: 'invalid encoded piece', value: pieceValue };
|
||||
throw new Error(`invalid encoded piece: ${pieceValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +120,7 @@ class Board {
|
|||
constructor(original) {
|
||||
if (original !== null && original !== undefined) {
|
||||
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);
|
||||
|
|
@ -175,7 +187,7 @@ class Board {
|
|||
|
||||
if (type !== EMPTY) {
|
||||
if (this._phantom) {
|
||||
throw { message: 'phantom square is already occupied' };
|
||||
throw new Error(`phantom square is already occupied`);
|
||||
}
|
||||
|
||||
this.putPiece(side, square, EMPTY);
|
||||
|
|
@ -190,17 +202,17 @@ class Board {
|
|||
|
||||
move(from, to) {
|
||||
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 darkPiece = this.getPiece(DARK, from);
|
||||
|
||||
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) {
|
||||
if (!this.isEmpty(to)) {
|
||||
throw { message: 'cannot capture with joined pieces' };
|
||||
throw new Error(`cannot capture with joined pieces`);
|
||||
}
|
||||
|
||||
const fromIndex = squareIndex(from);
|
||||
|
|
@ -214,7 +226,7 @@ class Board {
|
|||
const displaced = this.getPiece(moving, to);
|
||||
|
||||
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) {
|
||||
|
|
@ -462,9 +474,38 @@ function diffHistory(game1, game2) {
|
|||
|
||||
class Game {
|
||||
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 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, {
|
||||
|
|
@ -488,6 +529,7 @@ class Game {
|
|||
this._undo = null;
|
||||
this._checkCache = {};
|
||||
this._ignoreCheck = false;
|
||||
this._version = CURRENT_VERSION;
|
||||
|
||||
/* set to false when the king or rook moves */
|
||||
this._castling = {
|
||||
|
|
@ -525,6 +567,18 @@ class Game {
|
|||
this._ignoreCheck = !!value;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return JSON.stringify({
|
||||
past: this._moves,
|
||||
future: this._redo,
|
||||
version: this._version,
|
||||
});
|
||||
}
|
||||
|
||||
canCapture(side, from) {
|
||||
if (from === PHANTOM) {
|
||||
return true;
|
||||
|
|
@ -732,7 +786,7 @@ class Game {
|
|||
const kings = [...this._board.findPieces(side, KING)];
|
||||
|
||||
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];
|
||||
|
|
@ -894,7 +948,7 @@ class Game {
|
|||
|
||||
move(from, to, meta) {
|
||||
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;
|
||||
|
|
@ -906,13 +960,13 @@ class Game {
|
|||
const alongside = board.getPiece(other, from);
|
||||
|
||||
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;
|
||||
|
||||
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 = {
|
||||
|
|
@ -1053,7 +1107,7 @@ class Game {
|
|||
|
||||
resign(meta) {
|
||||
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 = {
|
||||
|
|
@ -1101,8 +1155,8 @@ class Game {
|
|||
}
|
||||
|
||||
replayMove(move) {
|
||||
if (!move || move.side !== this._player) {
|
||||
throw { message: "other player's move", move: move };
|
||||
if (otherSide(move.side) === this._player) {
|
||||
throw new Error(`other player's move`);
|
||||
}
|
||||
|
||||
if (move.resign) {
|
||||
|
|
@ -1164,6 +1218,7 @@ class Game {
|
|||
renderHistory() {
|
||||
if (this._history === undefined) {
|
||||
const replay = new Game();
|
||||
replay._version = this._version;
|
||||
for (const move of this._moves) {
|
||||
replay.replayMove(move);
|
||||
}
|
||||
|
|
@ -1190,6 +1245,9 @@ export default {
|
|||
/* Coordinates */
|
||||
ROWS, COLUMNS, PHANTOM,
|
||||
|
||||
/* Versioning */
|
||||
CURRENT_VERSION,
|
||||
|
||||
/* Miscellaneous */
|
||||
Util: { otherSide, offsetSquare, diffHistory },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ $(function (){
|
|||
} else if (type === PS.PAWN) {
|
||||
return 'p';
|
||||
} 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) {
|
||||
return 'd';
|
||||
} else {
|
||||
throw { message: 'unknown side', side: side };
|
||||
throw new Error(`unknown side: ${side}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -576,9 +576,8 @@ $(function (){
|
|||
function putState() {
|
||||
const boardElem = $('#cb_board');
|
||||
const gameId = boardElem.data('gameId');
|
||||
const moves = { past: currentGame.moves, future: currentGame.redoMoves };
|
||||
boardElem.data('last_state', currentGame.moves);
|
||||
putMeta({ board: moves });
|
||||
putMeta({ board: JSON.parse(currentGame.toJSON()) });
|
||||
}
|
||||
|
||||
function putMeta(extra) {
|
||||
|
|
@ -649,40 +648,13 @@ $(function (){
|
|||
|
||||
cancelGameCallback = IO.onGameUpdate(newId, function(data, gameId) {
|
||||
if (data.modified > $('#cb_board').data('modified')) {
|
||||
const board = data.board || { past: [], future: [] };
|
||||
|
||||
try {
|
||||
const moves = { past: [...board.past], future: [...board.future] };
|
||||
const oldState = { past: currentGame.moves, future: currentGame.redoMoves };
|
||||
|
||||
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);
|
||||
}
|
||||
const newGame = new PS.Game(JSON.stringify(data.board));
|
||||
const newState = JSON.parse(newGame.toJSON());
|
||||
const oldState = JSON.parse(currentGame.toJSON());
|
||||
|
||||
if (!deepEqual(newState, oldState)) {
|
||||
debug('got board', newGame.moves);
|
||||
setCurrentGame(newGame, newGame.moves.length > currentGame.moves.length);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue