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 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 },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue