1117 lines
33 KiB
JavaScript
1117 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
let gun = Gun({
|
|
peers: ['https://jessemcdonald.info/gun'],
|
|
/* workaround for persistent errors accumulating which break synchronization */
|
|
localStorage: false,
|
|
});
|
|
|
|
let initialBoard = (function (){
|
|
let init = JSON.stringify({
|
|
light: [
|
|
'rnbqkbnr',
|
|
'pppppppp',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
],
|
|
dark: [
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
' ',
|
|
'pppppppp',
|
|
'rnbqkbnr',
|
|
],
|
|
player: 'light',
|
|
timestamp: new Date().getTime(),
|
|
});
|
|
return function (){ return JSON.parse(init); }
|
|
})();
|
|
|
|
function cloneJSON(obj){
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
|
|
function isLight(side){
|
|
return side[0] === 'l';
|
|
}
|
|
|
|
function isDark(side){
|
|
return side[0] === 'd';
|
|
}
|
|
|
|
function normalizeSide(side){
|
|
return isDark(side) ? 'dark' : 'light';
|
|
}
|
|
|
|
function otherSide(side){
|
|
return isDark(side) ? 'light' : 'dark';
|
|
}
|
|
|
|
function boardGet(board, where, side){
|
|
side = normalizeSide(side);
|
|
if (where === 'phantom') {
|
|
if (!board.phantom || board.phantom.type[1] !== side[0]) {
|
|
return ' ';
|
|
} else {
|
|
return board.phantom.type[0];
|
|
}
|
|
} else {
|
|
let column = 'abcdefgh'.indexOf(where[0]);
|
|
let row = Number(where[1]) - 1;
|
|
return board[side][row][column];
|
|
}
|
|
}
|
|
|
|
function boardPut(board, where, side, piece){
|
|
side = normalizeSide(side);
|
|
let column = 'abcdefgh'.indexOf(where[0]);
|
|
let row = Number(where[1]) - 1;
|
|
let data = board[side][row];
|
|
let prefix = data.substring(0, column);
|
|
let suffix = data.substring(column + 1, 8);
|
|
board[side][row] = prefix + piece + suffix;
|
|
}
|
|
|
|
function movedFrom(board){
|
|
if (!board || !board.move || !board.move.from) {
|
|
return null;
|
|
} else if (board.move.from === 'phantom') {
|
|
return board.prior.phantom.from;
|
|
} else {
|
|
return board.move.from;
|
|
}
|
|
}
|
|
|
|
function movedTo(board){
|
|
if (!board || !board.move || !board.move.to) {
|
|
return null;
|
|
} else {
|
|
return board.move.to;
|
|
}
|
|
}
|
|
|
|
function countMoves(board) {
|
|
let n = 0;
|
|
while (board && board.prior) {
|
|
if (board.prior.player !== board.player) {
|
|
++n;
|
|
}
|
|
board = board.prior;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function offsetSquare(from, columnsRight, rowsUp){
|
|
let column = 'abcdefgh'.indexOf(from[0]);
|
|
let row = Number(from[1]) - 1;
|
|
|
|
if (column < 0 || column > 7 || row < 0 || row > 7) {
|
|
return null;
|
|
}
|
|
|
|
column += columnsRight;
|
|
row += rowsUp;
|
|
|
|
if (column < 0 || column > 7 || row < 0 || row > 7) {
|
|
return null;
|
|
}
|
|
|
|
return 'abcdefgh'[column] + String(row + 1);
|
|
}
|
|
|
|
function validDestination(board, side, where, canCapture){
|
|
let ours = boardGet(board, where, side);
|
|
let theirs = boardGet(board, where, otherSide(side));
|
|
return ((theirs === ' ') ? (ours === ' ') : canCapture);
|
|
}
|
|
|
|
function scanPath(accum, board, side, from, canCapture, columnsLeft, rowsUp, remainder){
|
|
while (true) {
|
|
let there = offsetSquare(from, columnsLeft, rowsUp);
|
|
if (!there || !validDestination(board, side, there, canCapture)) { return; }
|
|
accum.push(there);
|
|
if (boardGet(board, there, otherSide(side)) !== ' ' || remainder < 1) { return; }
|
|
from = there;
|
|
remainder -= 1;
|
|
}
|
|
}
|
|
|
|
function hasMoved(board, side, where){
|
|
side = normalizeSide(side);
|
|
|
|
while (true) {
|
|
if (!board) {
|
|
return false;
|
|
}
|
|
|
|
const move = board.move;
|
|
|
|
if (move && move.side === side && move.to === where) {
|
|
return true;
|
|
}
|
|
|
|
board = board.prior;
|
|
}
|
|
}
|
|
|
|
function legalMoves(board, side, type, from, canCapture){
|
|
if (board.move && board.move.took === 'k') {
|
|
/* checkmate, the game is over */
|
|
return [];
|
|
}
|
|
|
|
const ortho = [[-1, 0], [1, 0], [0, -1], [0, 1]];
|
|
const diag = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
|
|
let legals = [];
|
|
|
|
if (from === 'phantom') {
|
|
from = board.phantom.from;
|
|
}
|
|
|
|
if (type === 'k') {
|
|
for (const dir of ortho.concat(diag)) {
|
|
scanPath(legals, board, side, from, false, dir[0], dir[1], 0);
|
|
}
|
|
if (from[0] === 'e' && from[1] === (isDark(side) ? '8' : '1')) {
|
|
const other = otherSide(side);
|
|
/* check for castling conditions */
|
|
if (!hasMoved(board, side, from)) {
|
|
if (boardGet(board, 'f' + from[1], side) === ' ' &&
|
|
boardGet(board, 'f' + from[1], other) === ' ' &&
|
|
boardGet(board, 'g' + from[1], side) === ' ' &&
|
|
boardGet(board, 'g' + from[1], other) === ' ' &&
|
|
boardGet(board, 'h' + from[1], side) === 'r') {
|
|
if (!hasMoved(board, side, 'h' + from[1])) {
|
|
legals.push('g' + from[1]);
|
|
}
|
|
}
|
|
if (boardGet(board, 'd' + from[1], side) === ' ' &&
|
|
boardGet(board, 'd' + from[1], other) === ' ' &&
|
|
boardGet(board, 'c' + from[1], side) === ' ' &&
|
|
boardGet(board, 'c' + from[1], other) === ' ' &&
|
|
boardGet(board, 'b' + from[1], side) === ' ' &&
|
|
boardGet(board, 'b' + from[1], other) === ' ' &&
|
|
boardGet(board, 'a' + from[1], side) === 'r') {
|
|
if (!hasMoved(board, side, 'a' + from[1])) {
|
|
legals.push('c' + from[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (type === 'q') {
|
|
for (const dir of ortho.concat(diag)) {
|
|
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
|
|
}
|
|
} else if (type === 'b') {
|
|
for (const dir of diag) {
|
|
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
|
|
}
|
|
} else if (type === 'n') {
|
|
for (const there of [offsetSquare(from, -1, 2), offsetSquare(from, 1, 2),
|
|
offsetSquare(from, -2, 1), offsetSquare(from, 2, 1),
|
|
offsetSquare(from, -2, -1), offsetSquare(from, 2, -1),
|
|
offsetSquare(from, -1, -2), offsetSquare(from, 1, -2)]) {
|
|
if (there && validDestination(board, side, there, canCapture)) {
|
|
legals.push(there);
|
|
}
|
|
}
|
|
} else if (type === 'r') {
|
|
for (const dir of ortho) {
|
|
scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8);
|
|
}
|
|
} else if (type === 'p') {
|
|
const dark = isDark(side);
|
|
const forward = offsetSquare(from, 0, dark ? -1 : 1);
|
|
const forward2 = offsetSquare(from, 0, dark ? -2 : 2);
|
|
const diagL = offsetSquare(from, -1, dark ? -1 : 1);
|
|
const diagR = offsetSquare(from, 1, dark ? -1 : 1);
|
|
|
|
if (forward && validDestination(board, side, forward, false)) {
|
|
legals.push(forward);
|
|
if (dark ? (from[1] >= '7') : (from[1] <= '2')) {
|
|
if (forward2 && validDestination(board, side, forward2, false)) {
|
|
legals.push(forward2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (canCapture) {
|
|
for (const there of [diagL, diagR]) {
|
|
if (there) {
|
|
if (boardGet(board, there, otherSide(side)) !== ' ') {
|
|
legals.push(there);
|
|
} else if (forward2) {
|
|
let otherBoard = board;
|
|
while (otherBoard && otherBoard.move && otherBoard.move.side[0] === side[0]) {
|
|
otherBoard = otherBoard.prior;
|
|
}
|
|
if (otherBoard && otherBoard.move) {
|
|
const move = otherBoard.move;
|
|
if (move.side[0] !== side[0] && move.type === 'p') {
|
|
const from = movedFrom(otherBoard);
|
|
if (from && from[0] === there[0] && from[1] === forward2[1]) {
|
|
/* en passant */
|
|
legals.push(there);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return legals;
|
|
}
|
|
|
|
function movePiece(priorBoard, side, from, to){
|
|
let other = otherSide(side);
|
|
let type = boardGet(priorBoard, from, side);
|
|
let took = boardGet(priorBoard, to, other);
|
|
let replacedFrom = to;
|
|
let replaced = boardGet(priorBoard, replacedFrom, side);
|
|
let alongside = boardGet(priorBoard, from, other);
|
|
let actuallyFrom = (from === 'phantom') ? priorBoard.phantom.from : from;
|
|
|
|
const legals = legalMoves(priorBoard, side, type, from, alongside === ' ');
|
|
if (!legals.includes(to)) {
|
|
return priorBoard;
|
|
}
|
|
|
|
let undoBoard = priorBoard;
|
|
if (undoBoard.subsequent) {
|
|
undoBoard = cloneJSON(undoBoard);
|
|
delete undoBoard.subsequent;
|
|
}
|
|
|
|
let board = cloneJSON(undoBoard);
|
|
board.prior = undoBoard;
|
|
|
|
board.timestamp = new Date().getTime();
|
|
|
|
board.move = {
|
|
side: normalizeSide(side),
|
|
type: type,
|
|
from: from,
|
|
to: to
|
|
};
|
|
|
|
if (took !== ' ') {
|
|
board.move.took = took;
|
|
}
|
|
|
|
if (replaced !== ' ') {
|
|
board.move.replaced = replaced;
|
|
}
|
|
|
|
if (alongside !== ' ') {
|
|
board.move.alongside = alongside;
|
|
}
|
|
|
|
if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'g') {
|
|
board.move.castle = true;
|
|
const alongsideRook = boardGet(board, 'h' + actuallyFrom[1], other);
|
|
boardPut(board, 'h' + actuallyFrom[1], side, ' ');
|
|
boardPut(board, 'h' + actuallyFrom[1], other, ' ');
|
|
boardPut(board, 'f' + actuallyFrom[1], side, 'r');
|
|
boardPut(board, 'f' + actuallyFrom[1], other, alongsideRook);
|
|
}
|
|
else if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'c') {
|
|
board.move.queen_castle = true;
|
|
const alongsideRook = boardGet(board, 'a' + actuallyFrom[1], other);
|
|
boardPut(board, 'a' + actuallyFrom[1], side, ' ');
|
|
boardPut(board, 'a' + actuallyFrom[1], other, ' ');
|
|
boardPut(board, 'd' + actuallyFrom[1], side, 'r');
|
|
boardPut(board, 'd' + actuallyFrom[1], other, alongsideRook);
|
|
}
|
|
else if (type === 'p' && alongside === ' ' && replaced === ' ' && to[0] !== actuallyFrom[0]) {
|
|
let otherBoard = priorBoard;
|
|
/* scan for the opponent's last move, since this could be part of a chain */
|
|
while (otherBoard && otherBoard.move && otherBoard.move.side[0] === side[0]) {
|
|
otherBoard = otherBoard.prior;
|
|
}
|
|
if (otherBoard && otherBoard.move) {
|
|
const move = otherBoard.move;
|
|
if (move.type === 'p' && move.to[0] === to[0] && move.to[1] === actuallyFrom[1]) {
|
|
const moveFrom = movedFrom(otherBoard);
|
|
if (move.side[0] === other[0] && moveFrom[1] != to[1]) {
|
|
board.move.en_passant = true;
|
|
board.move.took = 'p';
|
|
/* move the opponent's pawn back one space */
|
|
boardPut(board, move.to, other, ' ');
|
|
boardPut(board, to, other, 'p');
|
|
/* see if we replaced a piece from the other square */
|
|
replacedFrom = move.to;
|
|
replaced = boardGet(board, replacedFrom, side);
|
|
boardPut(board, replacedFrom, side, ' ');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type === 'p' && to[1] === (isDark(side) ? '1' : '8')) {
|
|
board.move.promotion = 'q'; /* TODO: allow other choices */
|
|
type = 'q';
|
|
}
|
|
|
|
if (alongside === 'p' && to[1] === (isDark(other) ? '1' : '8')) {
|
|
board.move.promotion = 'q'; /* TODO: allow other choices */
|
|
alongside = 'q';
|
|
}
|
|
|
|
if (from === 'phantom') {
|
|
delete board.phantom;
|
|
} else {
|
|
boardPut(board, from, side, ' ');
|
|
boardPut(board, from, other, ' ');
|
|
}
|
|
|
|
boardPut(board, to, side, type);
|
|
|
|
if (alongside !== ' ') {
|
|
boardPut(board, to, other, alongside);
|
|
}
|
|
|
|
if (replaced === ' ') {
|
|
board.player = otherSide(board.player);
|
|
} else {
|
|
board.phantom = { from: replacedFrom, type: replaced + side[0] };
|
|
}
|
|
|
|
return board;
|
|
}
|
|
|
|
function findPieces(board, side, type, alongside){
|
|
let pieces = [];
|
|
const other = otherSide(side);
|
|
|
|
if (board.phantom && board.phantom.type === type + side[0]) {
|
|
pieces.push(board.phantom.from);
|
|
}
|
|
|
|
for (const row of "12345678") {
|
|
for (const column of "abcdefgh") {
|
|
const here = column + row;
|
|
if (boardGet(board, here, side) === type) {
|
|
if (!alongside || boardGet(board, here, other) === alongside) {
|
|
pieces.push(here);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return pieces;
|
|
}
|
|
|
|
function renderHistory(currentBoard) {
|
|
let list = [];
|
|
|
|
for (let board = currentBoard; board && board.move; board = board.prior) {
|
|
list.push(board);
|
|
}
|
|
|
|
const NBSP = '\u00a0'; /* non-breaking space */
|
|
const SHY = '\u00ad' /* soft hyphen */
|
|
const ZWSP = '\u200b'; /* zero-width space */
|
|
|
|
let result = '';
|
|
let n = 0;
|
|
|
|
while (list.length > 0) {
|
|
const board = list.pop();
|
|
const move = board.move;
|
|
|
|
if (move.from === 'phantom') {
|
|
result += SHY + '*';
|
|
} else {
|
|
if (n > 0 || isDark(move.side)) {
|
|
result += ' ';
|
|
}
|
|
|
|
if (isLight(move.side)) {
|
|
++n;
|
|
result += String(n) + '.' + NBSP;
|
|
}
|
|
}
|
|
|
|
if (move.pass) {
|
|
result += '...';
|
|
} else if (move.castle) {
|
|
result += 'O-O';
|
|
} else if (move.queen_castle) {
|
|
result += 'O-O-O';
|
|
} else {
|
|
let piece = '';
|
|
|
|
if (move.alongside) {
|
|
if (isLight(move.side)) {
|
|
piece = move.type.toUpperCase() + move.alongside.toUpperCase();
|
|
} else {
|
|
piece = move.alongside.toUpperCase() + move.type.toUpperCase();
|
|
}
|
|
} else {
|
|
piece = move.type === 'p' ? '' : move.type.toUpperCase();
|
|
}
|
|
|
|
const from = movedFrom(board);
|
|
|
|
if (move.from !== 'phantom' || from !== movedTo(board.prior)) {
|
|
const sameKind = findPieces(board.prior, move.side, move.type, move.alongside || ' ');
|
|
const legalFrom = [];
|
|
let sameFile = 0; /* column / letter */
|
|
let sameRank = 0; /* row / number */
|
|
|
|
for (const where of sameKind) {
|
|
if (legalMoves(board.prior, move.side, move.type, where, true).includes(move.to)) {
|
|
legalFrom.push(where);
|
|
|
|
if (where[0] === from[0]) {
|
|
sameFile += 1;
|
|
}
|
|
|
|
if (where[1] === from[1]) {
|
|
sameRank += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (legalFrom.length !== 1 || move.en_passant || (move.type === 'p' && move.took)) {
|
|
/* append file, rank, or both to disambiguate */
|
|
if (sameFile === 1) {
|
|
piece += from[0];
|
|
} else if (sameRank === 1) {
|
|
piece += from[1];
|
|
} else {
|
|
piece += from;
|
|
}
|
|
}
|
|
}
|
|
|
|
const took = move.took ? 'x' : '';
|
|
result += piece + took + move.to;
|
|
}
|
|
|
|
if (move.en_passant) {
|
|
result += 'e.p.';
|
|
}
|
|
|
|
if (move.promotion) {
|
|
result += '(' + move.promotion[0].toUpperCase() + ')';
|
|
}
|
|
|
|
if (move.took === 'k') {
|
|
result += '#';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function pieceStartDrag(ev, ui){
|
|
const board = $('#cb_board').data('board');
|
|
const dragged = $(this);
|
|
const type = dragged.data('type');
|
|
const from = dragged.data('location');
|
|
const where = (from === 'phantom') ? board.phantom.from : from;
|
|
const canCapture = boardGet(board, from, otherSide(type[1])) === ' ';
|
|
const legals = legalMoves(board, type[1], type[0], where, canCapture);
|
|
for (const there of legals) {
|
|
$('#cb_' + there).addClass('cb-legal').droppable('enable');
|
|
}
|
|
}
|
|
|
|
function pieceStopDrag(ev, ui){
|
|
$('#cb_board .cb-legal').removeClass('cb-legal').droppable('disable');
|
|
}
|
|
|
|
function placePiece(where, type, count){
|
|
let piece_id = 'cb_piece_' + type + '_' + count;
|
|
let piece = $($('#' + piece_id)[0] || $('#cb_piece_' + type + ' img').clone());
|
|
piece.attr('style', '');
|
|
piece.attr('id', piece_id);
|
|
piece.data({ type: type, location: where });
|
|
piece.appendTo('#cb_' + where);
|
|
piece.draggable({
|
|
disabled: true,
|
|
containment: '#cb_inner',
|
|
revert: 'invalid',
|
|
zIndex: 100,
|
|
start: pieceStartDrag,
|
|
stop: pieceStopDrag,
|
|
});
|
|
return piece;
|
|
}
|
|
|
|
function renderBoard(board){
|
|
$('#cb_board .cb-piece .ui-draggable').draggable('destroy');
|
|
$('#cb_board .cb-piece').attr('style', '').appendTo('#cb_hidden');
|
|
$('#cb_board .cb-start').removeClass('cb-start');
|
|
$('#cb_board .cb-end').removeClass('cb-end');
|
|
$('#cb_board .cb-legal').removeClass('cb-legal');
|
|
$('#cb_phantom').appendTo('#cb_hidden');
|
|
|
|
for (const side of ['light', 'dark']) {
|
|
let counters = {};
|
|
for (let row = 0; row < 8; ++row) {
|
|
for (let column = 0; column < 8; ++column) {
|
|
let here = 'abcdefgh'[column] + String(row+1);
|
|
let type = board[side][row][column];
|
|
if (type !== ' ') {
|
|
if (!counters[type]) {
|
|
counters[type] = 0;
|
|
}
|
|
let count = ++counters[type];
|
|
placePiece(here, type + side[0], count);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let clss = isLight(board.player) ? '.cb-lt-piece' : '.cb-dk-piece';
|
|
|
|
if (board.phantom) {
|
|
let where = board.phantom.from;
|
|
placePiece('phantom', board.phantom.type, 'ph');
|
|
$('#cb_phantom').appendTo('#cb_' + where);
|
|
$('#cb_board .ui-draggable').draggable('disable');
|
|
$('#cb_phantom .ui-draggable-disabled').filter(clss).draggable('enable');
|
|
} else {
|
|
$('#cb_board .ui-draggable').draggable('disable');
|
|
if (!board.move || board.move.took !== 'k') {
|
|
$('#cb_board .ui-draggable-disabled').filter(clss).draggable('enable');
|
|
}
|
|
}
|
|
|
|
const start = movedFrom(board);
|
|
if (start) {
|
|
$('#cb_' + start).addClass('cb-start');
|
|
}
|
|
|
|
const end = movedTo(board);
|
|
if (end) {
|
|
$('#cb_' + end).addClass('cb-end');
|
|
}
|
|
|
|
let msg = '';
|
|
if (board.move && board.move.took === 'k') {
|
|
msg = (isDark(board.move.side) ? 'Dark' : 'Light') + ' player won!';
|
|
} else {
|
|
msg = (isDark(board.player) ? 'Dark' : 'Light') + " player's turn";
|
|
}
|
|
$('#cb_message').text(msg);
|
|
|
|
$('#cb_history').text(renderHistory(board));
|
|
}
|
|
|
|
function setVisibleBoard(board){
|
|
const cb_board = $('#cb_board').first();
|
|
|
|
const currentBoard = cloneJSON(cb_board.data('board'));
|
|
delete currentBoard.subsequent;
|
|
|
|
const live = deepEqual(board, currentBoard);
|
|
|
|
$('#cb_board').data('visible_board', board);
|
|
renderBoard(board);
|
|
|
|
$('#cb_nav_first').attr('disabled', board.prior ? false : true);
|
|
$('#cb_nav_prev_turn').attr('disabled', board.prior ? false : true);
|
|
$('#cb_nav_prev_state').attr('disabled', board.prior ? false : true);
|
|
$('#cb_nav_next_state').attr('disabled', board.subsequent ? false : true);
|
|
$('#cb_nav_next_turn').attr('disabled', board.subsequent ? false : true);
|
|
|
|
if (live) {
|
|
/* the 'visible' board may be missing .subsequent */
|
|
const liveBoard = $('#cb_board').data('board');
|
|
$('#cb_undo').attr('disabled', liveBoard.prior ? false : true);
|
|
$('#cb_redo').attr('disabled', liveBoard.subsequent ? false : true);
|
|
$('#cb_pass').attr('disabled', liveBoard.phantom ? true : false);
|
|
$('#cb_nav_last').attr('disabled', true);
|
|
$('#cb_board').addClass('cb-live');
|
|
$('#cb_board').removeClass('cb-archive');
|
|
} else {
|
|
$('#cb_undo').attr('disabled', true);
|
|
$('#cb_redo').attr('disabled', true);
|
|
$('#cb_pass').attr('disabled', true);
|
|
$('#cb_board .ui-draggable').draggable('disable');
|
|
$('#cb_nav_last').attr('disabled', false);
|
|
$('#cb_board').removeClass('cb-live');
|
|
$('#cb_board').addClass('cb-archive');
|
|
}
|
|
}
|
|
|
|
function setCurrentBoard(board){
|
|
const cb_board = $('#cb_board').first();
|
|
cb_board.data('board', board);
|
|
|
|
/* navigation should not include the redo stack */
|
|
const visible = cloneJSON(board);
|
|
delete visible.subsequent;
|
|
setVisibleBoard(visible);
|
|
|
|
if ($('#cb_notify')[0].checked && !deepEqual(board, cb_board.data('last_state'))) {
|
|
/* ignore partial moves and undo/redo */
|
|
if (!board.phantom && !board.subsequent) {
|
|
const gameString = cb_board.data('lightName') + ' vs. ' + cb_board.data('darkName');
|
|
notify(gameString + '\n' + $('#cb_message').text());
|
|
}
|
|
}
|
|
|
|
cb_board.data('last_state', cloneJSON(board));
|
|
}
|
|
|
|
function randomId(){
|
|
let res = '';
|
|
for (let i = 0; i < 4; ++i) {
|
|
let part = Math.floor(Math.random() * 65536).toString(16);
|
|
res = res + ('0000'.substring(part.length, 4) + part);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
let PacoSakoUUID = '7c38edd4-c931-49c8-9f1a-84de560815db';
|
|
|
|
function putState(board){
|
|
let boardElem = $('#cb_board');
|
|
let gameId = boardElem.data('gameId');
|
|
let game = gun.get(PacoSakoUUID).get('games').get(gameId);
|
|
boardElem.data('last_state', cloneJSON(board));
|
|
game.put({ board: JSON.stringify(board) });
|
|
putMeta();
|
|
}
|
|
|
|
function putMeta(){
|
|
let board = $('#cb_board').data('board');
|
|
let gameId = $('#cb_board').data('gameId');
|
|
let lightName = $('#cb_light_name').val();
|
|
let darkName = $('#cb_dark_name').val();
|
|
let meta = gun.get(PacoSakoUUID).get('meta').get(gameId);
|
|
let stat = (board.move && board.move.took === 'k') ? 'mate' : null;
|
|
meta.put({
|
|
gameId: gameId,
|
|
lightName: lightName,
|
|
darkName: darkName,
|
|
moves: countMoves(board),
|
|
timestamp: board.timestamp || new Date().getTime(),
|
|
status: stat,
|
|
});
|
|
}
|
|
|
|
function switchGameId(newId){
|
|
let boardElem = $('#cb_board');
|
|
let gameId = boardElem.data('gameId');
|
|
|
|
if (newId === gameId) {
|
|
return;
|
|
}
|
|
|
|
if (gameId) {
|
|
//gun.get(PacoSakoUUID).get('games').get(gameId).off();
|
|
//gun.get(PacoSakoUUID).get('meta').get(gameId).off();
|
|
}
|
|
|
|
boardElem.data('gameId', newId);
|
|
location.hash = '#/' + newId;
|
|
|
|
const notifyAfter = new Date().getTime() + 2000;
|
|
$(window).data('notifyAfter', new Date().getTime() + 2000);
|
|
window.setTimeout(function() {
|
|
/* Delete the notification block in case the system time is changed backward */
|
|
if ($(window).data('notifyAfter') === notifyAfter) {
|
|
$(window).removeData('notifyAfter');
|
|
}
|
|
}, 2000);
|
|
closeNotifications();
|
|
|
|
/* this will be the starting state if no data is received from peers */
|
|
let newBoard = initialBoard();
|
|
setCurrentBoard(newBoard);
|
|
boardElem.data('last_state', cloneJSON(newBoard));
|
|
|
|
boardElem.data('lightName', 'Light');
|
|
boardElem.data('darkName', 'Dark');
|
|
$('#cb_light_name').val('');
|
|
$('#cb_dark_name').val('');
|
|
|
|
gun.get(PacoSakoUUID).get('games').get(newId).on(function(d){
|
|
if (d && d.board && $('#cb_board').data('gameId') === newId) {
|
|
const board = JSON.parse(d.board);
|
|
setCurrentBoard(board);
|
|
}
|
|
});
|
|
|
|
gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){
|
|
if (d && $('#cb_board').data('gameId') === newId) {
|
|
$('#cb_board').data('lightName', d.lightName || 'Light');
|
|
$('#cb_board').data('darkName', d.darkName || 'Dark');
|
|
$('#cb_light_name').val(d.lightName || '');
|
|
$('#cb_dark_name').val(d.darkName || '');
|
|
}
|
|
});
|
|
|
|
let selOpt = $('#cb_game_' + newId);
|
|
if (selOpt.length === 1) {
|
|
$('#cb_select_game')[0].selectedIndex = selOpt.index();
|
|
} else {
|
|
$('#cb_select_game')[0].selectedIndex = -1;
|
|
}
|
|
}
|
|
|
|
function disableNotify(){
|
|
$('#cb_notify')[0].checked = false;
|
|
$('#cb_notify').attr('disabled', true);
|
|
}
|
|
|
|
function requestNotify(){
|
|
try {
|
|
Notification.requestPermission(function (permission){
|
|
if (permission === 'denied') {
|
|
disableNotify();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
disableNotify();
|
|
}
|
|
}
|
|
|
|
function notify(body) {
|
|
const now = new Date().getTime();
|
|
const then = $(window).data('notifyAfter');
|
|
if (!then || now >= then) {
|
|
try {
|
|
Notification.requestPermission(function(permission){
|
|
if (permission === 'granted') {
|
|
navigator.serviceWorker.ready.then(function(registration){
|
|
registration.showNotification('Paco Ŝako', {
|
|
body: body,
|
|
tag: 'notice',
|
|
});
|
|
});
|
|
} else if (permission === 'denied') {
|
|
disableNotify();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
disableNotify();
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeNotifications(){
|
|
try {
|
|
navigator.serviceWorker.ready.then(function(registration){
|
|
registration.getNotifications({tag: 'notice'}).then(function(notifications){
|
|
for (const notification of notifications) {
|
|
notification.close();
|
|
}
|
|
});
|
|
});
|
|
} catch (err) {}
|
|
}
|
|
|
|
$(function (){
|
|
try {
|
|
if (Notification.permission === 'denied') {
|
|
disableNotify();
|
|
} else {
|
|
navigator.serviceWorker.register('sw.js').catch(disableNotify);
|
|
}
|
|
} catch (err) {
|
|
disableNotify();
|
|
}
|
|
|
|
if ('localStorage' in window) {
|
|
function updateNotify(newValue){
|
|
const doNotify = newValue === 'on';
|
|
const cb_notify = $('#cb_notify')[0];
|
|
if (doNotify) {
|
|
if (!cb_notify.checked) {
|
|
cb_notify.checked = true;
|
|
requestNotify();
|
|
}
|
|
} else if (cb_notify.checked) {
|
|
cb_notify.checked = false;
|
|
}
|
|
}
|
|
$(window).on('storage', function(event){
|
|
if (event.originalEvent.key === '/pacosako/notify') {
|
|
updateNotify(event.originalEvent.newValue);
|
|
}
|
|
});
|
|
updateNotify(window.localStorage.getItem('/pacosako/notify'));
|
|
}
|
|
|
|
$('.cb-square').droppable({
|
|
accept: '.cb-piece',
|
|
disabled: true,
|
|
deactivate: function(ev, ui){
|
|
$(this).droppable('disable');
|
|
},
|
|
drop: function(ev, ui) {
|
|
let dragged = ui.draggable;
|
|
let type = dragged.data('type');
|
|
let from = dragged.data('location');
|
|
let to = this.id.replace(/^cb_/, '');
|
|
dragged.appendTo('#cb_hidden');
|
|
|
|
let newBoard = movePiece($('#cb_board').data('board'), type[1], from, to);
|
|
renderBoard(newBoard);
|
|
putState(newBoard);
|
|
},
|
|
});
|
|
|
|
function stepBack(board){
|
|
if (!board.prior) {
|
|
return cloneJSON(board);
|
|
}
|
|
|
|
const redoBoard = cloneJSON(board);
|
|
delete redoBoard.prior;
|
|
|
|
const newBoard = cloneJSON(board.prior);
|
|
newBoard.subsequent = redoBoard;
|
|
|
|
return newBoard;
|
|
}
|
|
|
|
function stepForward(board){
|
|
if (!board.subsequent) {
|
|
return cloneJSON(board);
|
|
}
|
|
|
|
const undoBoard = cloneJSON(board);
|
|
delete undoBoard.subsequent;
|
|
|
|
const newBoard = cloneJSON(board.subsequent);
|
|
newBoard.prior = undoBoard;
|
|
|
|
return newBoard;
|
|
}
|
|
|
|
$('#cb_undo').on('click', function(){
|
|
putState(stepBack($('#cb_board').data('board')));
|
|
});
|
|
|
|
$('#cb_redo').on('click', function(){
|
|
const board = stepForward($('#cb_board').data('board'));
|
|
board.timestamp = new Date().getTime();
|
|
putState(board);
|
|
});
|
|
|
|
$('#cb_nav_first').on('click', function(){
|
|
let board = $('#cb_board').data('visible_board');
|
|
if (board) {
|
|
while (board.prior) {
|
|
board = stepBack(board);
|
|
}
|
|
setVisibleBoard(board);
|
|
}
|
|
});
|
|
|
|
$('#cb_nav_prev_turn').on('click', function(){
|
|
let board = $('#cb_board').data('visible_board');
|
|
board = stepBack(board);
|
|
const player = board.player;
|
|
while (board.prior && board.prior.player === player) {
|
|
board = stepBack(board);
|
|
}
|
|
setVisibleBoard(board);
|
|
});
|
|
|
|
$('#cb_nav_prev_state').on('click', function(){
|
|
setVisibleBoard(stepBack($('#cb_board').data('visible_board')));
|
|
});
|
|
|
|
$('#cb_nav_next_state').on('click', function(){
|
|
setVisibleBoard(stepForward($('#cb_board').data('visible_board')));
|
|
});
|
|
|
|
$('#cb_nav_next_turn').on('click', function(){
|
|
let board = $('#cb_board').data('visible_board');
|
|
const player = board.player;
|
|
board = stepForward(board);
|
|
while (board.subsequent && board.player === player) {
|
|
board = stepForward(board);
|
|
}
|
|
setVisibleBoard(board);
|
|
});
|
|
|
|
$('#cb_nav_last').on('click', function(){
|
|
const visible = cloneJSON($('#cb_board').data('board'));
|
|
delete visible.subsequent;
|
|
setVisibleBoard(visible);
|
|
});
|
|
|
|
$('#cb_reset').on('click', function(){
|
|
putState(initialBoard());
|
|
});
|
|
|
|
$('#cb_pass').on('click', function(){
|
|
let board = $('#cb_board').data('board');
|
|
if (!board.phantom) {
|
|
let newBoard = cloneJSON(board);
|
|
newBoard.prior = board;
|
|
newBoard.move = { side: board.player, pass: true };
|
|
newBoard.player = otherSide(board.player);
|
|
newBoard.timestamp = new Date().getTime();
|
|
putState(newBoard);
|
|
}
|
|
});
|
|
|
|
$('#cb_select_game').on('change', function(){
|
|
let optIndex = $('#cb_select_game')[0].selectedIndex;
|
|
if (optIndex === 0) {
|
|
switchGameId(randomId());
|
|
} else if (optIndex >= 1) {
|
|
let opt = $('#cb_select_game option')[optIndex];
|
|
if (opt) {
|
|
switchGameId(opt.id.replace(/^cb_game_/, ''));
|
|
}
|
|
}
|
|
});
|
|
|
|
$('#cb_notify').on('change', function(){
|
|
if ('localStorage' in window) {
|
|
window.localStorage.setItem('/pacosako/notify', this.checked ? 'on' : 'off');
|
|
}
|
|
if (this.checked) {
|
|
requestNotify();
|
|
}
|
|
});
|
|
|
|
let updateMeta = function() { putMeta(); }
|
|
$('#cb_light_name').on('input', updateMeta);
|
|
$('#cb_dark_name').on('input', updateMeta);
|
|
|
|
let gameId = location.hash.replace(/^#\//, '');
|
|
if (gameId.length !== 16) {
|
|
gameId = randomId();
|
|
}
|
|
|
|
switchGameId(gameId);
|
|
|
|
function updateTitle(opt){
|
|
opt = $(opt);
|
|
|
|
const then = opt.data('then');
|
|
const now = new Date().getTime();
|
|
let age_str = '';
|
|
if (then > now) {
|
|
age_str = ' (future)';
|
|
} else if ((now - then) < 60*60*1000) {
|
|
age_str = ' (' + Math.floor((now - then) / (60*1000)) + 'm)';
|
|
} else if ((now - then) < 24*60*60*1000) {
|
|
age_str = ' (' + Math.floor((now - then) / (60*60*1000)) + 'h)';
|
|
} else if ((now - then) < 14*24*60*60*1000) {
|
|
age_str = ' (' + Math.floor((now - then) / (24*60*60*1000)) + 'd)';
|
|
} else if (opt.data('gameId') !== $('#cb_board').data('gameId')) {
|
|
opt.remove();
|
|
return;
|
|
}
|
|
opt.text(opt.data('title') + age_str);
|
|
}
|
|
|
|
gun.get(PacoSakoUUID).get('meta').map().on(function(d){
|
|
if (d && d.gameId) {
|
|
const lightName = d.lightName ? d.lightName : 'Light';
|
|
const darkName = d.darkName ? d.darkName : 'Dark';
|
|
const moves = !d.moves ? '' :
|
|
(', ' + d.moves + (d.moves === 1 ? ' move' : ' moves'));
|
|
|
|
let opt = $('#cb_game_' + d.gameId);
|
|
|
|
if (!(d.lightName || d.darkName) && !d.moves && d.gameId !== $('#cb_board').data('gameId')) {
|
|
if (opt.length >= 1) {
|
|
opt.remove();
|
|
}
|
|
} else {
|
|
if (opt.length === 0) {
|
|
opt = $('<option></option>');
|
|
opt.attr('id', 'cb_game_' + d.gameId);
|
|
}
|
|
|
|
let stat = '';
|
|
if (d.status) {
|
|
stat = ', ' + d.status;
|
|
}
|
|
|
|
opt.data('gameId', d.gameId);
|
|
opt.data('title', lightName + ' vs. ' + darkName + moves + stat);
|
|
opt.data('then', d.timestamp || new Date().getTime());
|
|
opt.addClass('cb-game-option');
|
|
opt.appendTo('#cb_select_game');
|
|
updateTitle(opt);
|
|
|
|
let select = $('#cb_select_game');
|
|
let list = select.children('.cb-game-option').get();
|
|
list.sort(function(a,b) {
|
|
const then_a = $(a).data('then');
|
|
const then_b = $(b).data('then');
|
|
return (then_a < then_b) ? 1 : (then_a === then_b) ? 0 : -1;
|
|
});
|
|
|
|
for (const e of list) {
|
|
$(e).appendTo(select);
|
|
}
|
|
}
|
|
|
|
let selOpt = $('#cb_game_' + $('#cb_board').data('gameId'));
|
|
if (selOpt.length === 1) {
|
|
$('#cb_select_game')[0].selectedIndex = selOpt.index();
|
|
} else {
|
|
$('#cb_select_game')[0].selectedIndex = -1;
|
|
}
|
|
}
|
|
});
|
|
|
|
window.setInterval(function(){
|
|
$('#cb_select_game').first().children('.cb-game-option').each(function(idx,opt){
|
|
updateTitle(opt);
|
|
});
|
|
}, 15000);
|
|
|
|
window.setInterval(function(){
|
|
const peers = gun._.opt.peers;
|
|
let n_open = 0;
|
|
let n_total = 0;
|
|
for (const peerId in peers) {
|
|
++n_total;
|
|
try {
|
|
const peer = peers[peerId];
|
|
if (peer.constructor === RTCPeerConnection) {
|
|
if (peer.connectionState === 'connected') {
|
|
++n_open;
|
|
}
|
|
} else if (peer.wire && peer.wire.constructor === WebSocket) {
|
|
if (peer.wire.readyState === peer.wire.OPEN) {
|
|
++n_open;
|
|
}
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
const newText = 'Synchronizing with ' + n_open + (n_open === 1 ? ' peer' : ' peers') + ' (' + n_total + ' total).';
|
|
if (newText !== $('#cb_peers').text()) {
|
|
$('#cb_peers').text(newText);
|
|
}
|
|
}, 1000);
|
|
|
|
window.onpopstate = function(event){
|
|
let gameId = location.hash.replace(/^#\//, '');
|
|
if (gameId.length === 16) {
|
|
switchGameId(gameId);
|
|
}
|
|
};
|
|
|
|
/* force page reload after four hours to keep client code up to date */
|
|
window.setTimeout(function(){ location.reload(); }, 4*3600*1000);
|
|
});
|
|
|
|
/* vim:set expandtab sw=3 ts=8: */
|