1439 lines
44 KiB
JavaScript
1439 lines
44 KiB
JavaScript
'use strict';
|
|
|
|
import PS from './pacosako.js';
|
|
import IO from './pacosako_io.js';
|
|
|
|
import deepEqual from 'deep-equal';
|
|
|
|
import 'webpack-jquery-ui/draggable';
|
|
import 'webpack-jquery-ui/droppable';
|
|
import 'webpack-jquery-ui/selectmenu';
|
|
import 'webpack-jquery-ui/fold-effect';
|
|
import 'webpack-jquery-ui/css';
|
|
|
|
import 'jquery-ui-touch-punch';
|
|
|
|
import escape from 'lodash/escape';
|
|
|
|
import jBox from 'jbox';
|
|
import 'jbox/dist/jBox.all.css';
|
|
|
|
import {Workbox, messageSW} from 'workbox-window';
|
|
|
|
import {ResizeSensor, ElementQueries} from 'css-element-queries';
|
|
ElementQueries.listen();
|
|
|
|
/* "Waterdrop" by Porphyr (freesound.org/people/Porphyr) / CC BY 3.0 (creativecommons.org/licenses/by/3.0) */
|
|
import Waterdrop from '../mp3/191678__porphyr__waterdrop.mp3';
|
|
|
|
$(function (){
|
|
function debug() {
|
|
if (window.logDebug) {
|
|
console.log.apply(console, arguments);
|
|
}
|
|
}
|
|
|
|
window.logDebug = false;
|
|
|
|
let currentGame = new PS.Game();
|
|
let visibleGame = new PS.Game(currentGame);
|
|
let cancelGameCallback = function() {};
|
|
|
|
function gameMessage(game) {
|
|
let msg = '';
|
|
const winner = game.winner;
|
|
if (winner) {
|
|
const lastMove = game.lastMove;
|
|
if (game.status === PS.CHECKMATE) {
|
|
msg += 'Checkmate! ';
|
|
} else if (lastMove && lastMove.resign) {
|
|
msg += (lastMove.side === PS.LIGHT ? 'Light' : 'Dark') + ' player resigned. ';
|
|
}
|
|
msg += (winner === PS.LIGHT ? 'Light' : 'Dark') + ' player won!';
|
|
} else if (game.status === PS.PLAYING) {
|
|
if (game.isInCheck()) {
|
|
msg += 'Check! ';
|
|
}
|
|
msg += (game.player === PS.LIGHT ? 'Light' : 'Dark') + ' player\'s turn.';
|
|
} else {
|
|
msg += 'Game ended in a draw.';
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
function pieceTypeClass(type) {
|
|
if (type === PS.KING) {
|
|
return 'cb-king';
|
|
} else if (type === PS.QUEEN) {
|
|
return 'cb-queen';
|
|
} else if (type === PS.ROOK) {
|
|
return 'cb-rook';
|
|
} else if (type === PS.KNIGHT) {
|
|
return 'cb-knight';
|
|
} else if (type === PS.BISHOP) {
|
|
return 'cb-bishop';
|
|
} else if (type === PS.PAWN) {
|
|
return 'cb-pawn';
|
|
} else {
|
|
throw new Error(`unknown piece type: ${type}`);
|
|
}
|
|
}
|
|
|
|
function pieceSideClass(side) {
|
|
if (side === PS.LIGHT) {
|
|
return 'cb-lt-piece';
|
|
} else if (side === PS.DARK) {
|
|
return 'cb-dk-piece';
|
|
} else {
|
|
throw new Error(`unknown side: ${side}`);
|
|
}
|
|
}
|
|
|
|
function cbSquare(where) {
|
|
if (where === PS.PHANTOM) {
|
|
let square = $('#cb_phantom');
|
|
if (square.length < 1) {
|
|
square = $(`<div id="cb_phantom" class="cb-phantom"></div>`);
|
|
square.hide().appendTo('body').droppable({
|
|
accept: '.cb-piece',
|
|
disabled: true,
|
|
deactivate: function(ev, ui){
|
|
$(this).droppable('disable');
|
|
},
|
|
drop: squareDropDestination,
|
|
});
|
|
}
|
|
return square;
|
|
} else if (where.match(/^[a-h][1-8]$/)) {
|
|
return $('#cb_' + where).first()
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function cbPiece(where, side) {
|
|
const square = cbSquare(where);
|
|
if (!square) {
|
|
return null;
|
|
} else if (side === true) {
|
|
return square.find('.cb-piece');
|
|
} else {
|
|
return square.find('.' + pieceSideClass(side)).first();
|
|
}
|
|
}
|
|
|
|
function cbSquareLocation(square) {
|
|
const id = $(square).attr('id');
|
|
|
|
if (id === "cb_phantom") {
|
|
return PS.PHANTOM;
|
|
}
|
|
|
|
const found = id.match(/^cb_([a-h][1-8])$/);
|
|
if (found) {
|
|
return found[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function pieceStartMove(piece, event) {
|
|
const side = piece.data('side');
|
|
const type = piece.data('type');
|
|
const from = piece.data('location');
|
|
const legals = currentGame.legalMoves(side, from);
|
|
for (const there of legals) {
|
|
if (currentGame.isDeadEnd(from, there)) {
|
|
continue;
|
|
}
|
|
|
|
const square = cbSquare(there);
|
|
square.addClass('cb-legal')
|
|
if (event === 'drag') {
|
|
square.droppable('enable');
|
|
} else if (event === 'click') {
|
|
square.on('click.destination', squareClickDestination);
|
|
}
|
|
}
|
|
if (event === 'drag') {
|
|
cbSquare(from).droppable('enable');
|
|
}
|
|
}
|
|
|
|
function pieceEndMove(piece, to, animate) {
|
|
let from = piece.data('location');
|
|
piece.appendTo('#cb_hidden');
|
|
|
|
try {
|
|
const meta = { timestamp: +new Date() };
|
|
currentGame.move(from, to, meta);
|
|
} catch (err) {
|
|
debug('unable to move', err);
|
|
animate = false;
|
|
}
|
|
|
|
notifyLocalMove(currentGame, $('#cb_board').data('gameId'));
|
|
setCurrentGame(currentGame, animate);
|
|
putState();
|
|
}
|
|
|
|
function squareClickDestination(ev, ui) {
|
|
let selected = $('#cb_board .cb-selected');
|
|
if (selected.length !== 1) {
|
|
renderBoard();
|
|
return;
|
|
}
|
|
|
|
pieceEndMove(selected, cbSquareLocation(this), true);
|
|
}
|
|
|
|
function squareDropDestination(ev, ui) {
|
|
const droppedAt = cbSquareLocation(this);
|
|
if ($(ui.draggable).data('location') == droppedAt) {
|
|
squareClickSelect.call(cbSquare(droppedAt));
|
|
} else {
|
|
pieceEndMove(ui.draggable, droppedAt);
|
|
}
|
|
}
|
|
|
|
function squareClickUnselect(ev, ui) {
|
|
renderBoard();
|
|
}
|
|
|
|
function squareClickSelect(ev, ui) {
|
|
renderBoard();
|
|
const clicked = $(this).children('.cb-piece.ui-draggable').not('.ui-draggable-disabled');
|
|
clicked.addClass('cb-selected');
|
|
$('#cb_board .cb-square').off('click.select');
|
|
clicked.parent().on('click.unselect', squareClickUnselect);
|
|
pieceStartMove(clicked, 'click');
|
|
|
|
if ($('#cb_board .cb-legal').length < 1) {
|
|
/* cancel the selection since there is nowhere to move the piece to */
|
|
renderBoard();
|
|
}
|
|
}
|
|
|
|
function pieceStartDrag(ev, ui) {
|
|
const dragged = $(this);
|
|
$('#cb_board .cb-selected').removeClass('cb-selected');
|
|
$('#cb_board .cb-legal').removeClass('cb-legal');
|
|
dragged.data('saved-style', dragged.attr('style'));
|
|
$('#cb_board').data('dragging_from', dragged.data('location'));
|
|
pieceStartMove(dragged, 'drag');
|
|
}
|
|
|
|
function pieceStopDrag(ev, ui) {
|
|
const dragged = $(this);
|
|
dragged.attr('style', dragged.data('saved-style'));
|
|
dragged.removeData('saved-style');
|
|
dragged.css('z-index', '');
|
|
if ($('#cb_board').data('dragging_from') === dragged.data('location')) {
|
|
if ($('#cb_board .cb-selected').length < 1) {
|
|
renderBoard();
|
|
}
|
|
}
|
|
}
|
|
|
|
function placePiece(where, side, type) {
|
|
const piece = $(`<div class="cb-piece"></div>`);
|
|
piece.addClass(pieceSideClass(side));
|
|
piece.addClass(pieceTypeClass(type));
|
|
piece.data({ side: side, type: type, location: where });
|
|
piece.appendTo(cbSquare(where));
|
|
piece.draggable({
|
|
disabled: true,
|
|
containment: '#cb_inner',
|
|
revert: true,
|
|
zIndex: 100,
|
|
start: pieceStartDrag,
|
|
stop: pieceStopDrag,
|
|
});
|
|
return piece;
|
|
}
|
|
|
|
function renderBoard(animate) {
|
|
$('#cb_board').removeData('dragging_from');
|
|
$('#cb_board .cb-piece').remove();
|
|
$('#cb_board .cb-square').off('click.select');
|
|
$('#cb_board .cb-square').off('click.unselect');
|
|
$('#cb_board .cb-square').off('click.destination');
|
|
$('#cb_board .cb-start').removeClass('cb-start');
|
|
$('#cb_board .cb-end').removeClass('cb-end');
|
|
$('#cb_board .cb-legal').removeClass('cb-legal');
|
|
$('#cb_explain_check').text('');
|
|
$('#cb_phantom').remove();
|
|
|
|
const game = visibleGame;
|
|
const board = game.board;
|
|
|
|
for (const side of [PS.LIGHT, PS.DARK]) {
|
|
for (const row of PS.ROWS) {
|
|
for (const column of PS.COLUMNS) {
|
|
const here = column + row;
|
|
const type = board.getPiece(side, here);
|
|
if (type !== PS.EMPTY) {
|
|
placePiece(here, side, type);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const lastMove = game.lastMove;
|
|
if (lastMove) {
|
|
if (lastMove.from) {
|
|
cbSquare(lastMove.from).addClass('cb-start');
|
|
}
|
|
|
|
if (lastMove.to) {
|
|
cbSquare(lastMove.to).addClass('cb-end');
|
|
}
|
|
|
|
if (lastMove.from && lastMove.to && animate) {
|
|
const sideMoved = lastMove.alongside ? true : lastMove.side;
|
|
const pieces = cbPiece(lastMove.to, sideMoved);
|
|
if (pieces.length > 0) {
|
|
const fromRect = cbSquare(lastMove.from)[0].getBoundingClientRect();
|
|
const toRect = cbSquare(lastMove.to)[0].getBoundingClientRect();
|
|
const movedDown = toRect.top - fromRect.top;
|
|
const movedRight = toRect.left - fromRect.left;
|
|
for (const domPiece of pieces) {
|
|
const piece = $(domPiece);
|
|
const originalTop = parseFloat(piece.css('top'));
|
|
const originalLeft = parseFloat(piece.css('left'));
|
|
const originalStyle = piece.attr('style') || null;
|
|
piece.css({
|
|
'z-index': 100,
|
|
'top': originalTop - movedDown,
|
|
'left': originalLeft - movedRight,
|
|
}).animate({
|
|
'top': originalTop,
|
|
'left': originalLeft,
|
|
}, {
|
|
always: function() {
|
|
piece.attr('style', originalStyle);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (game.player === PS.LIGHT) {
|
|
$('#cb_board').removeClass('cb-dk-turn').addClass('cb-lt-turn');
|
|
} else {
|
|
$('#cb_board').removeClass('cb-lt-turn').addClass('cb-dk-turn');
|
|
}
|
|
|
|
const liveView = !game.canRedo;
|
|
const playing = game.status === PS.PLAYING;
|
|
const clss = pieceSideClass(game.player);
|
|
|
|
const phantom = board.phantom;
|
|
if (phantom) {
|
|
const piece = placePiece(PS.PHANTOM, phantom.side, phantom.type);
|
|
cbSquare(PS.PHANTOM).appendTo(cbSquare(phantom.from)).show();
|
|
|
|
if (liveView && playing) {
|
|
piece.draggable('enable');
|
|
piece.addClass('cb-selected');
|
|
pieceStartMove(piece, 'click');
|
|
}
|
|
} else if (liveView && playing) {
|
|
const pieces = $('#cb_board .' + clss)
|
|
pieces.parent().on('click.select', squareClickSelect);
|
|
pieces.draggable('enable');
|
|
}
|
|
|
|
const check = game.isInCheck();
|
|
const king = $('#cb_board .cb-king.' + clss);
|
|
|
|
if (check) {
|
|
king.addClass('cb-in-check');
|
|
}
|
|
|
|
if (typeof check === 'string') {
|
|
$('#cb_explain_check').text(`(Check: ${check})`);
|
|
}
|
|
|
|
let msg = '';
|
|
|
|
if (!liveView) {
|
|
msg += '(Move ' + String(game.moves.length) + ' of ' + String(currentGame.moves.length) + ') ';
|
|
}
|
|
|
|
msg += gameMessage(game);
|
|
$('#cb_message').text(msg);
|
|
|
|
const viewHistory = game.history || '';
|
|
const fullHistory = currentGame.history || '';
|
|
const futureHistory = fullHistory.slice(viewHistory.length);
|
|
$('#cb_history_past').html(escape(viewHistory).replace(/\d+\./g, '<b>$&</b>'));
|
|
$('#cb_history_future').html(escape(futureHistory).replace(/\d+\./g, '<b>$&</b>'));
|
|
|
|
$('#cb_nav_first').attr('disabled', !game.canUndo);
|
|
$('#cb_nav_prev_turn').attr('disabled', !game.canUndo);
|
|
$('#cb_nav_prev_state').attr('disabled', !game.canUndo);
|
|
$('#cb_nav_next_state').attr('disabled', !game.canRedo);
|
|
$('#cb_nav_next_turn').attr('disabled', !game.canRedo);
|
|
$('#cb_nav_last').attr('disabled', !game.canRedo);
|
|
|
|
$('#cb_undo').attr('disabled', !liveView || !currentGame.canUndo);
|
|
$('#cb_redo').attr('disabled', !liveView || !currentGame.canRedo);
|
|
$('#cb_resign').attr('disabled', !liveView || !playing);
|
|
|
|
if (liveView) {
|
|
$('#cb_board').addClass('cb-live').removeClass('cb-archive');
|
|
} else {
|
|
$('#cb_board').removeClass('cb-live').addClass('cb-archive');
|
|
}
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
let cbBoard = $('#cb_board').first();
|
|
for (const klass of cbBoard.prop('classList')) {
|
|
if (klass.match(/^cb-theme-/)) {
|
|
cbBoard.removeClass(klass);
|
|
}
|
|
}
|
|
cbBoard.addClass('cb-theme-' + theme);
|
|
}
|
|
|
|
function setVisibleGame(game, animate) {
|
|
/* navigation should not include the redo stack */
|
|
visibleGame = new PS.Game(game);
|
|
visibleGame.clearRedo();
|
|
renderBoard(animate);
|
|
}
|
|
|
|
function setCurrentGame(game, animate) {
|
|
currentGame = game;
|
|
setVisibleGame(game, animate);
|
|
}
|
|
|
|
function randomId(){
|
|
let res = '';
|
|
for (let i = 0; i < 4; ++i) {
|
|
const part = Math.floor(Math.random() * 65536).toString(16);
|
|
res = res + ('0000'.substring(part.length, 4) + part);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function shortenName(name) {
|
|
name = name.replace(/^\s+/, '');
|
|
name = name.replace(/\s+$/, '');
|
|
name = name.replace(/\s+/, ' ');
|
|
|
|
let found = name.match(/^(.{0,20})(\s|$)/);
|
|
if (found) {
|
|
return found[1];
|
|
}
|
|
|
|
return name.slice(0, 20) + '…';
|
|
}
|
|
|
|
let noticeBox = null;
|
|
|
|
function openNoticeBox(content) {
|
|
if (noticeBox) {
|
|
noticeBox.setContent(content);
|
|
noticeBox.open();
|
|
} else {
|
|
noticeBox = new jBox('Notice', {
|
|
autoClose: false,
|
|
closeOnEsc: false,
|
|
closeOnClick: false,
|
|
content: content,
|
|
delayClose: 2000,
|
|
delayOpen: 100,
|
|
onClose() {
|
|
if (noticeBox === this) {
|
|
noticeBox = null;
|
|
}
|
|
},
|
|
onCloseComplete() {
|
|
this.destroy();
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const updateQueue = {
|
|
head: 0, /* next item to be sent */
|
|
tail: 0, /* ID to assign to the next update added to the queue */
|
|
sending: false,
|
|
idle: Promise.resolve(true), /* resolves to true if updates succeeded */
|
|
signal_idle: function() {},
|
|
|
|
add(gameId, data, modified) {
|
|
const wasEmpty = this.isEmpty();
|
|
data = Object.assign({}, data, { modified });
|
|
|
|
this[this.tail] = { gameId, data };
|
|
this.tail += 1;
|
|
|
|
if (!this.sending) {
|
|
openNoticeBox('Saving...');
|
|
this.sending = true;
|
|
this.idle = new Promise((resolve) => {
|
|
this.signal_idle = resolve;
|
|
});
|
|
this.sendNext();
|
|
}
|
|
},
|
|
|
|
isEmpty() {
|
|
return this.head === this.tail;
|
|
},
|
|
|
|
peek() {
|
|
if (!this.isEmpty()) {
|
|
return this[this.head];
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
remove() {
|
|
if (!this.isEmpty()) {
|
|
const first = this[this.head];
|
|
delete this[this.head];
|
|
this.head += 1;
|
|
return first;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
sendNext() {
|
|
const queue = this;
|
|
const update = queue.remove();
|
|
|
|
if (update === undefined) {
|
|
return;
|
|
}
|
|
|
|
/* merge updates for the same game with the same baseline into a single request */
|
|
let peek;
|
|
while ((peek = queue.peek()) !== undefined && peek.gameId === update.gameId
|
|
&& peek.data.modified === update.data.modified) {
|
|
Object.assign(update.data, peek.data);
|
|
queue.remove();
|
|
}
|
|
|
|
IO.sendUpdate(update.gameId, update.data).then((response) => {
|
|
if (response && response.data && 'modified' in response.data) {
|
|
/*
|
|
* If we queued two or more updates with the same .modified (thus
|
|
* based on the same server data), send the later update(s) with the
|
|
* new .modified assigned by the server as a result of this update.
|
|
*/
|
|
for (let i = queue.head; i !== queue.tail; i += 1) {
|
|
if (queue[i].gameId === update.gameId && queue[i].data.modified === update.data.modified) {
|
|
queue[i].data.modified = response.data.modified;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Send future updates with the new modified time, and prevent loading
|
|
* older data from the server in case the connection is lagging.
|
|
*/
|
|
const cbBoard = $('#cb_board');
|
|
if (cbBoard.data('gameId') === update.gameId
|
|
&& cbBoard.data('modified') === update.data.modified) {
|
|
cbBoard.data('modified', response.data.modified);
|
|
}
|
|
}
|
|
|
|
if (queue.isEmpty()) {
|
|
queue.sending = false;
|
|
queue.signal_idle(true);
|
|
/* close the Saving... notice*/
|
|
noticeBox.close({ ignoreDelay: true });
|
|
} else {
|
|
queue.sendNext();
|
|
}
|
|
}).catch((err) => {
|
|
openNoticeBox('Failed to send update to server.');
|
|
noticeBox.close();
|
|
debug('update error', err);
|
|
|
|
/* additional updates are unlikely to succeed, so empty the queue */
|
|
while (!queue.isEmpty()) {
|
|
queue.remove();
|
|
}
|
|
|
|
queue.sending = false;
|
|
queue.signal_idle(false);
|
|
|
|
/* force a reset back to the latest server data */
|
|
if (update.gameId === $('#cb_board').data('gameId')) {
|
|
switchGameId(update.gameId, true);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
function putState() {
|
|
const boardElem = $('#cb_board');
|
|
const gameId = boardElem.data('gameId');
|
|
notifyLocalMove(currentGame, boardElem.data('gameId'));
|
|
putMeta({ board: JSON.parse(currentGame.toJSON()) });
|
|
}
|
|
|
|
function putMeta(extra) {
|
|
function player(side) {
|
|
return (side === PS.LIGHT ? 'light' : 'dark');
|
|
}
|
|
const gameId = $('#cb_board').data('gameId');
|
|
const lightName = $('#cb_light_name').val();
|
|
const darkName = $('#cb_dark_name').val();
|
|
const turns = currentGame.turns;
|
|
const winner = currentGame.winner;
|
|
const lastMove = currentGame.lastMove || {};
|
|
const lastMeta = lastMove.meta || {};
|
|
const status =
|
|
(currentGame.status === PS.PLAYING) ? `${player(currentGame.player)}'s turn` :
|
|
(currentGame.status === PS.CHECKMATE) ? `checkmate — ${player(winner)} won` :
|
|
(currentGame.status === PS.RESIGNED) ? `${player(lastMove.side)} resigned` :
|
|
'game ended';
|
|
|
|
const meta = {
|
|
lightName,
|
|
darkName,
|
|
moves: turns,
|
|
timestamp: lastMeta.timestamp || +new Date(),
|
|
status,
|
|
};
|
|
|
|
if (extra) {
|
|
Object.assign(meta, extra);
|
|
}
|
|
|
|
updateQueue.add(gameId, meta, $('#cb_board').data('modified'));
|
|
}
|
|
|
|
function setNotifyChecked(doNotify) {
|
|
const cbNotify = $('#cb_notify');
|
|
cbNotify.prop('checked', doNotify);
|
|
if (doNotify) {
|
|
cbNotify.removeClass('fa-bell-slash').addClass('fa-bell');
|
|
} else {
|
|
cbNotify.removeClass('fa-bell').addClass('fa-bell-slash');
|
|
}
|
|
}
|
|
|
|
function switchGameId(newId, force) {
|
|
const boardElem = $('#cb_board');
|
|
const gameId = boardElem.data('gameId');
|
|
|
|
debug('switching from ' + gameId + ' to ' + newId);
|
|
if (newId === gameId && !force) {
|
|
return;
|
|
}
|
|
|
|
/* cancel to reload data, but keep polling if game ID will be the same */
|
|
cancelGameCallback(newId === gameId);
|
|
|
|
boardElem.data('gameId', newId);
|
|
boardElem.data('modified', 0);
|
|
history.replaceState(null, document.title, '#/' + newId);
|
|
|
|
/* this will be the starting state if no data is received from peers */
|
|
setCurrentGame(new PS.Game());
|
|
notifyLocalMove(null, newId);
|
|
|
|
boardElem.data('lightName', 'Light');
|
|
boardElem.data('darkName', 'Dark');
|
|
$('#cb_light_name').val('');
|
|
$('#cb_dark_name').val('');
|
|
|
|
cancelGameCallback = IO.onGameUpdate(newId, function(data, gameId) {
|
|
updateQueue.idle.then(() => {
|
|
if (data.modified > $('#cb_board').data('modified')) {
|
|
try {
|
|
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) {
|
|
debug('Error parsing board data', err);
|
|
}
|
|
|
|
const d = data || {};
|
|
$('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light')));
|
|
$('#cb_board').data('darkName', shortenName(String(d.darkName || 'Dark')));
|
|
$('#cb_light_name').val(String(d.lightName || ''));
|
|
$('#cb_dark_name').val(String(d.darkName || ''));
|
|
|
|
$('#cb_board').data('modified', data.modified);
|
|
}
|
|
});
|
|
});
|
|
|
|
const notifyList = $('#cb_notify').data('gameList');
|
|
const doNotify = notifyList.includes('*') || notifyList.includes(newId);
|
|
setNotifyChecked(doNotify);
|
|
if (doNotify) {
|
|
requestNotify();
|
|
}
|
|
|
|
const reverseList = $('#cb_reverse').data('gameList');
|
|
const doReverse = reverseList.includes('*') || reverseList.includes(newId);
|
|
$('#cb_reverse').prop('checked', doReverse);
|
|
arrangeBoard(doReverse);
|
|
|
|
/* Ensure that the selected game is in the list (for new games). */
|
|
if ($('#game_tile_' + newId).length < 1) {
|
|
updateSelectGameMeta({}, newId);
|
|
}
|
|
|
|
/* This is in case the old tile no longer qualifies to be in the list. */
|
|
const oldGameTile = $('#game_tile_' + gameId);
|
|
if (oldGameTile.length >= 1) {
|
|
updateSelectGameMeta(oldGameTile.data('gameMeta'), gameId);
|
|
}
|
|
|
|
$('.game-tile-selected').removeClass('game-tile-selected');
|
|
$('#game_tile_' + newId).addClass('game-tile-selected');
|
|
}
|
|
|
|
function disableNotify(){
|
|
setNotifyChecked(false);
|
|
$('#cb_notify').attr('disabled', true);
|
|
}
|
|
|
|
function requestNotify(){
|
|
try {
|
|
Notification.requestPermission(function (permission){
|
|
if (permission === 'denied') {
|
|
disableNotify();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
disableNotify();
|
|
}
|
|
}
|
|
|
|
const notifyAudio = new Audio(Waterdrop);
|
|
|
|
function playNotifySound(){
|
|
try { notifyAudio.play(); } catch (err) {}
|
|
}
|
|
|
|
function notify(body) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
async function closeNotifications() {
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
|
|
if ('getNotifications' in registration) {
|
|
const notifications = await registration.getNotifications({tag: 'notice'});
|
|
|
|
for (const notification of notifications) {
|
|
notification.close();
|
|
}
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
function arrangeBoard(reversed) {
|
|
let rows = '9876543210'.split('');
|
|
let columns = 'LabcdefghR'.split('');
|
|
const boardElem = $('#cb_board');
|
|
|
|
if (reversed) {
|
|
rows.reverse();
|
|
columns.reverse();
|
|
boardElem.addClass('cb-reversed');
|
|
} else {
|
|
boardElem.removeClass('cb-reversed');
|
|
}
|
|
|
|
for (const row of rows) {
|
|
const rowElem = $('#cb_row' + row);
|
|
rowElem.appendTo(boardElem);
|
|
for (const column of columns) {
|
|
$('#cb_' + column + row).appendTo(rowElem);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
let confirmBox = null;
|
|
|
|
Promise.resolve().then(async () => {
|
|
const wb = new Workbox('sw.js');
|
|
let latest_sw = null;
|
|
|
|
function showSkipWaitingPrompt(event) {
|
|
latest_sw = event.sw;
|
|
|
|
if (!confirmBox) {
|
|
confirmBox = new jBox('Confirm', {
|
|
attach: null,
|
|
content: "A new version is available. Update the page?",
|
|
confirmButton: `<span class="update-confirm-button">Update</span>`,
|
|
cancelButton: 'Not now',
|
|
closeOnConfirm: false,
|
|
async confirm() {
|
|
/* The SW should signal us to reload, but do it after 20s regardless. */
|
|
setTimeout(() => { window.location.reload(); }, 20000);
|
|
messageSW(latest_sw, {type: 'SKIP_WAITING'});
|
|
$('.update-confirm-button').text('Updating…');
|
|
},
|
|
onClose() {
|
|
if (confirmBox === this) {
|
|
confirmBox = null;
|
|
}
|
|
},
|
|
onCloseComplete() {
|
|
this.destroy();
|
|
},
|
|
});
|
|
|
|
confirmBox.open();
|
|
}
|
|
}
|
|
|
|
function reloadForUpdate(event) {
|
|
window.location.reload();
|
|
}
|
|
|
|
wb.addEventListener('installed', (event) => {
|
|
try {
|
|
if (Notification.permission === 'denied') {
|
|
disableNotify();
|
|
}
|
|
} catch (err) {
|
|
disableNotify();
|
|
}
|
|
});
|
|
|
|
wb.addEventListener('waiting', showSkipWaitingPrompt);
|
|
wb.addEventListener('externalwaiting', showSkipWaitingPrompt);
|
|
|
|
wb.addEventListener('controlling', reloadForUpdate);
|
|
wb.addEventListener('externalactivated', reloadForUpdate);
|
|
|
|
const registration = await wb.register();
|
|
|
|
/* Check for updates every 4h without reloading the page. */
|
|
setInterval(() => { wb.update(); }, 4*3600*1000);
|
|
|
|
window.Admin.workbox = wb;
|
|
}).catch((err) => {
|
|
console.error('failed to register the service worker', err);
|
|
disableNotify();
|
|
});
|
|
}
|
|
|
|
const LS_KEY_NOTIFY = 'pacosako/notify';
|
|
const LS_KEY_SOUND = 'pacosako/sound';
|
|
const LS_KEY_REVERSE = 'pacosako/reverse';
|
|
const LS_KEY_THEME = 'pacosako/theme';
|
|
|
|
if ('localStorage' in window) {
|
|
const fromStorage = function fromStorage(key, value) {
|
|
function updatePerGameFlag(key, value, selector, onchange) {
|
|
let gameList = undefined;
|
|
const gameId = $('#cb_board').data('gameId');
|
|
if (value === 'on') {
|
|
gameList = ['*']
|
|
} else if (value === null || value === 'off') {
|
|
gameList = [];
|
|
} else {
|
|
try {
|
|
gameList = JSON.parse(value);
|
|
if (!Array.isArray(gameList)) {
|
|
throw new TypeError(`expected an array for ${key}`);
|
|
}
|
|
for (const item of gameList) {
|
|
if (typeof item !== 'string') {
|
|
throw new TypeError(`expected an array of strings for ${key}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
debug(`error parsing game list for ${key}`, err);
|
|
gameList = [];
|
|
}
|
|
}
|
|
const enabled = gameList.includes('*') || gameList.includes(gameId);
|
|
const checkbox = $(selector).first();
|
|
const wasChecked = checkbox.prop('checked');
|
|
checkbox.data('gameList', gameList);
|
|
checkbox.prop('checked', enabled);
|
|
if (enabled !== wasChecked) {
|
|
onchange(enabled);
|
|
}
|
|
}
|
|
|
|
debug('from localStorage', { key, value });
|
|
if (key === LS_KEY_NOTIFY) {
|
|
updatePerGameFlag(key, value, '#cb_notify', (enabled) => {
|
|
setNotifyChecked(enabled);
|
|
if (enabled) {
|
|
requestNotify();
|
|
}
|
|
});
|
|
} else if (key === LS_KEY_SOUND) {
|
|
const doSound = value === 'on';
|
|
const cb_sound = $('#cb_sound')[0];
|
|
cb_sound.checked = doSound;
|
|
} else if (key === LS_KEY_REVERSE) {
|
|
updatePerGameFlag(key, value, '#cb_reverse', (enabled) => {
|
|
arrangeBoard(enabled);
|
|
});
|
|
} else if (key === LS_KEY_THEME) {
|
|
if (value === null) {
|
|
value = 'pacosako';
|
|
}
|
|
const cb_theme = $('#cb_select_theme');
|
|
if (value !== cb_theme.val()) {
|
|
cb_theme.val(value);
|
|
if (!cb_theme.val()) {
|
|
value = 'pacosako';
|
|
cb_theme.val(value);
|
|
}
|
|
applyTheme(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
$(window).on('storage', function(event){
|
|
fromStorage(event.originalEvent.key, event.originalEvent.newValue);
|
|
});
|
|
|
|
for (const key of [LS_KEY_NOTIFY, LS_KEY_SOUND, LS_KEY_REVERSE, LS_KEY_THEME]) {
|
|
const value = window.localStorage.getItem(key);
|
|
fromStorage(key, value);
|
|
}
|
|
}
|
|
|
|
$('.cb-square').droppable({
|
|
accept: '.cb-piece',
|
|
disabled: true,
|
|
deactivate: function(ev, ui){
|
|
$(this).droppable('disable');
|
|
},
|
|
drop: squareDropDestination,
|
|
});
|
|
|
|
/* Maximum length of gameList for per-game flags like notify and reverse */
|
|
const GAMES_TO_REMEMBER = 50;
|
|
|
|
function perGameFlagChanged(key, selector) {
|
|
if ('localStorage' in window) {
|
|
const checkbox = $(selector);
|
|
const checked = checkbox.prop('checked');
|
|
const gameId = $('#cb_board').data('gameId');
|
|
let gameList = checkbox.data('gameList') || [];
|
|
if (gameList.includes('*')) {
|
|
gameList = Object.keys(IO.getCachedMeta());
|
|
}
|
|
gameList = gameList.filter((x) => x !== gameId);
|
|
if (checked) {
|
|
/* Ensure the new gameId is at the front of the list */
|
|
gameList.unshift(gameId);
|
|
}
|
|
gameList = gameList.slice(0, GAMES_TO_REMEMBER);
|
|
checkbox.data('gameList', gameList);
|
|
window.localStorage.setItem(key, JSON.stringify(gameList));
|
|
}
|
|
}
|
|
|
|
$('#cb_notify').on('change', function(){
|
|
perGameFlagChanged(LS_KEY_NOTIFY, this);
|
|
setNotifyChecked(this.checked);
|
|
if (this.checked) {
|
|
requestNotify();
|
|
}
|
|
});
|
|
|
|
$('#cb_sound').on('change', function(){
|
|
if ('localStorage' in window) {
|
|
window.localStorage.setItem(LS_KEY_SOUND, this.checked ? 'on' : 'off');
|
|
}
|
|
});
|
|
|
|
$('#cb_reverse').on('change', function(){
|
|
perGameFlagChanged(LS_KEY_REVERSE, this);
|
|
arrangeBoard(this.checked);
|
|
});
|
|
|
|
$('#cb_undo').on('click', function(){
|
|
if (currentGame.canUndo) {
|
|
currentGame.undo();
|
|
notifyLocalMove(currentGame, $('#cb_board').data('gameId'));
|
|
setCurrentGame(currentGame);
|
|
putState();
|
|
}
|
|
});
|
|
|
|
$('#cb_redo').on('click', function(){
|
|
if (currentGame.canRedo) {
|
|
currentGame.redo();
|
|
notifyLocalMove(currentGame, $('#cb_board').data('gameId'));
|
|
setCurrentGame(currentGame, true);
|
|
putState();
|
|
}
|
|
});
|
|
|
|
$('#cb_resign').on('click', function(){
|
|
try {
|
|
const meta = { timestamp: +new Date() };
|
|
currentGame.resign(meta);
|
|
} catch (err) {
|
|
debug('unable to resign', err);
|
|
}
|
|
|
|
notifyLocalMove(currentGame, $('#cb_board').data('gameId'));
|
|
setCurrentGame(currentGame);
|
|
putState();
|
|
});
|
|
|
|
$('#cb_nav_first').on('click', function(){
|
|
while (visibleGame.canUndo) {
|
|
visibleGame.undo();
|
|
}
|
|
renderBoard();
|
|
});
|
|
|
|
$('#cb_nav_prev_turn').on('click', function(){
|
|
if (visibleGame.canUndo) {
|
|
visibleGame.undo();
|
|
const player = visibleGame.player;
|
|
while (visibleGame.canUndo) {
|
|
visibleGame.undo();
|
|
if (visibleGame.player !== player) {
|
|
visibleGame.redo();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
renderBoard();
|
|
});
|
|
|
|
$('#cb_nav_prev_state').on('click', function(){
|
|
if (visibleGame.canUndo) {
|
|
visibleGame.undo();
|
|
}
|
|
renderBoard();
|
|
});
|
|
|
|
$('#cb_nav_next_state').on('click', function(){
|
|
if (visibleGame.canRedo) {
|
|
visibleGame.redo();
|
|
}
|
|
renderBoard(true);
|
|
});
|
|
|
|
$('#cb_nav_next_turn').on('click', function(){
|
|
const player = visibleGame.player;
|
|
while (visibleGame.canRedo) {
|
|
visibleGame.redo();
|
|
if (visibleGame.player !== player) {
|
|
break;
|
|
}
|
|
}
|
|
renderBoard(true);
|
|
});
|
|
|
|
$('#cb_nav_last').on('click', function(){
|
|
while (visibleGame.canRedo) {
|
|
visibleGame.redo();
|
|
}
|
|
renderBoard(true);
|
|
});
|
|
|
|
$('#cb_select_theme').on('change', function(){
|
|
const theme = $('#cb_select_theme').val();
|
|
debug('cb_select_theme changed to ' + theme);
|
|
if ('localStorage' in window) {
|
|
window.localStorage.setItem(LS_KEY_THEME, theme);
|
|
}
|
|
applyTheme(theme);
|
|
});
|
|
|
|
$('#cb_choose_game').on('click', function() {
|
|
if (selectBox) {
|
|
selectBox.open();
|
|
}
|
|
});
|
|
|
|
let helpBox = null;
|
|
$('#help').on('click', function() {
|
|
helpBox = helpBox || new jBox('Modal', {
|
|
title: '<h2>Rule Reference</h2>',
|
|
content: $('#rules_content'),
|
|
blockScroll: false,
|
|
blockScrollAdjust: false,
|
|
isolateScroll: false,
|
|
footer: `<div id="help_badges"></div>`,
|
|
closeButton: 'title',
|
|
onCreated() {
|
|
$('.badges').appendTo('#help_badges');
|
|
},
|
|
});
|
|
helpBox.open();
|
|
});
|
|
|
|
let settingBox = null;
|
|
$('#settings').on('click', function() {
|
|
settingBox = settingBox || new jBox('Modal', {
|
|
title: '<h2>Settings</h2>',
|
|
content: $('#settings_content'),
|
|
blockScroll: false,
|
|
blockScrollAdjust: false,
|
|
isolateScroll: false,
|
|
closeButton: 'title',
|
|
});
|
|
settingBox.open();
|
|
});
|
|
|
|
$('#cb_light_name, #cb_dark_name').on('input', function() { putMeta(); });
|
|
|
|
let gameSelectContent = $(`<div style="display: none"></div>`).appendTo('body');
|
|
let gameTiles = $(`
|
|
<div class="game-tiles">
|
|
<div class="game-tile new-game-tile">
|
|
<div class="new-game-text">New<br>Game</div>
|
|
</div>
|
|
</div>
|
|
`).appendTo(gameSelectContent);
|
|
|
|
var selectBoxIntervalID = undefined;
|
|
var selectBox = new jBox('Modal', {
|
|
content: gameSelectContent,
|
|
blockScroll: false,
|
|
blockScrollAdjust: false,
|
|
isolateScroll: false,
|
|
delayClose: 750,
|
|
onOpen() {
|
|
if (selectBoxIntervalID !== undefined) {
|
|
clearInterval(selectBoxIntervalID);
|
|
}
|
|
selectBoxIntervalID = setInterval(() => {
|
|
updateTileAges();
|
|
}, 15000);
|
|
updateTileAges();
|
|
},
|
|
onCloseComplete() {
|
|
if (selectBoxIntervalID !== undefined) {
|
|
clearInterval(selectBoxIntervalID);
|
|
selectBoxIntervalID = undefined;
|
|
}
|
|
},
|
|
position: { x: 'center', y: 'center' },
|
|
reposition: true,
|
|
});
|
|
|
|
$('.new-game-tile').on('click', function() {
|
|
switchGameId(randomId());
|
|
selectBox.close();
|
|
});
|
|
|
|
function inWords(n, singular, plural) {
|
|
const words = [
|
|
'zero', 'one', 'two', 'three', 'four',
|
|
'five', 'six', 'seven', 'eight', 'nine'
|
|
];
|
|
return `${words[n] || n} ${n === 1 ? singular : (plural || (singular + 's'))}`;
|
|
}
|
|
|
|
function updateTileAges() {
|
|
const now = +new Date();
|
|
|
|
for (const tileElem of $('.game-tile.existing-game-tile')) {
|
|
const tile = $(tileElem);
|
|
|
|
const game = tile.data('gameMeta');
|
|
if (!game || !game.timestamp) {
|
|
continue;
|
|
}
|
|
|
|
const then = game.timestamp;
|
|
|
|
let age_str = '';
|
|
if ((now - then) < 60*1000) {
|
|
age_str = `just now`;
|
|
} else if ((now - then) < 60*60*1000) {
|
|
const minutes = Math.floor((now - then) / (60*1000));
|
|
age_str = `${inWords(minutes, 'minute')} ago`;
|
|
} else if ((now - then) < 24*60*60*1000) {
|
|
const hours = Math.floor((now - then) / (60*60*1000));
|
|
age_str = `${inWords(hours, 'hour')} ago`;
|
|
} else {
|
|
const days = Math.floor((now - then) / (24*60*60*1000));
|
|
age_str = `${inWords(days, 'day')} ago`;
|
|
}
|
|
|
|
tile.find('.game-tile-age').text(age_str);
|
|
}
|
|
}
|
|
|
|
function updateSelectGameMeta(data, gameId) {
|
|
data = Object.assign({}, data, {
|
|
gameId: gameId || data.gameId,
|
|
timestamp: data.timestamp || +new Date(),
|
|
});
|
|
|
|
if (typeof gameId !== 'string' || !gameId.match(/^[0-9a-f]{16}$/)) {
|
|
debug('invalid game ID', gameId);
|
|
return;
|
|
}
|
|
|
|
const currentGameId = $('#cb_board').data('gameId');
|
|
const oldTile = $('#game_tile_' + gameId).first();
|
|
|
|
if (!data.lightName && !data.darkName && !data.moves && gameId !== currentGameId) {
|
|
oldTile.removeAttr('id');
|
|
if (oldTile.length >= 1) {
|
|
oldTile.hide({
|
|
effect: "fold",
|
|
complete() {
|
|
oldTile.remove();
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const tile = oldTile.length ? oldTile :
|
|
$(`<div class="game-tile existing-game-tile"></div>`).hide();
|
|
tile.attr('id', 'game_tile_' + gameId).data('gameMeta', data).empty();
|
|
tile.off('click');
|
|
|
|
if (gameId === $('#cb_board').data('gameId')) {
|
|
tile.addClass("game-tile-selected");
|
|
}
|
|
|
|
const titleBlock = $('<div class="game-tile-title"></div>').appendTo(tile);
|
|
|
|
$('<span class="game-tile-lt-name"></span>').appendTo(titleBlock)
|
|
.text(shortenName(String(data.lightName || 'Light')));
|
|
|
|
$(`<span class="game-tile-title-vs">vs.</span>`).appendTo(titleBlock);
|
|
|
|
$('<span class="game-tile-dk-name"></span>').appendTo(titleBlock)
|
|
.text(shortenName(String(data.darkName || 'Dark')));
|
|
|
|
$('<div class="game-tile-spacer"></div>').appendTo(tile);
|
|
|
|
if (data.status) {
|
|
$('<div class="game-tile-status"></div>').appendTo(tile).text(data.status);
|
|
}
|
|
|
|
if (data.moves) {
|
|
$('<div class="game-tile-moves"></div>').appendTo(tile)
|
|
.text(inWords(data.moves, 'turn'));
|
|
}
|
|
|
|
$('<div></div>').addClass('game-tile-age').appendTo(tile);
|
|
|
|
tile.on('click', function() {
|
|
$('.game-tile-selected').removeClass('game-tile-selected');
|
|
tile.addClass('game-tile-selected');
|
|
setTimeout(() => { switchGameId(gameId); }, 0);
|
|
selectBox.close();
|
|
});
|
|
|
|
const list = gameTiles.find('.existing-game-tile').get();
|
|
list.push(tile[0]);
|
|
list.sort(function(a,b) {
|
|
const then_a = $(a).data('gameMeta').timestamp;
|
|
const then_b = $(b).data('gameMeta').timestamp;
|
|
return (then_a < then_b) ? 1 : (then_a === then_b) ? 0 : -1;
|
|
});
|
|
$(list).appendTo(gameTiles);
|
|
updateTileAges();
|
|
|
|
tile.show("fold");
|
|
|
|
selectBox.setContent(gameSelectContent);
|
|
}
|
|
|
|
IO.onMetaUpdate(updateSelectGameMeta);
|
|
|
|
const lastNotifyState = {};
|
|
|
|
function notifyForGame(meta, gameId) {
|
|
const notifyList = $('#cb_notify').data('gameList') || [];
|
|
if (!notifyList.includes('*') && !notifyList.includes(gameId)) {
|
|
return;
|
|
}
|
|
|
|
lastNotifyState[gameId] = lastNotifyState[gameId] || {};
|
|
const lastState = lastNotifyState[gameId];
|
|
const lastMetaState = lastState.meta || {};
|
|
const changed =
|
|
meta.status !== lastMetaState.status ||
|
|
meta.moves !== lastMetaState.moves ||
|
|
meta.timestamp !== lastMetaState.timestamp;
|
|
|
|
if (lastState.meta && changed) {
|
|
IO.getGameState(gameId).then((data) => {
|
|
let game = undefined;
|
|
try {
|
|
game = new PS.Game(JSON.stringify(data.board));
|
|
} catch (err) {
|
|
debug('failed to parse game for notification', err);
|
|
return;
|
|
}
|
|
|
|
const moves = game.moves;
|
|
|
|
if (!deepEqual(moves, lastState.moves)) {
|
|
if (!game.board.phantom && moves.length > (lastState.moves || []).length) {
|
|
const lightName = shortenName(String(data.lightName || 'Light'));
|
|
const darkName = shortenName(String(data.darkName || 'Dark'));
|
|
const message = gameMessage(game);
|
|
const gameString = `${lightName} vs. ${darkName}\n${message}`;
|
|
closeNotifications().then(() => {
|
|
notify(gameString);
|
|
});
|
|
|
|
if ($('#cb_sound').prop('checked')) {
|
|
playNotifySound();
|
|
}
|
|
}
|
|
|
|
lastState.moves = moves;
|
|
}
|
|
}).catch((err) => {
|
|
debug('failed to retrieve game data for notification', err);
|
|
});
|
|
}
|
|
|
|
lastState.meta = meta;
|
|
}
|
|
|
|
function notifyLocalMove(game, gameId) {
|
|
if (!game) {
|
|
delete lastNotifyState[gameId];
|
|
|
|
/* Preload the last-known state if we already have data for this game */
|
|
const localMeta = IO.getCachedMeta();
|
|
if (gameId in localMeta) {
|
|
notifyForGame(localMeta[gameId], gameId);
|
|
}
|
|
} else {
|
|
lastNotifyState[gameId] = lastNotifyState[gameId] || {};
|
|
lastNotifyState[gameId].moves = game.moves;
|
|
}
|
|
}
|
|
|
|
IO.onMetaUpdate(notifyForGame);
|
|
|
|
window.onpopstate = function(event){
|
|
const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/);
|
|
if (foundId) {
|
|
switchGameId(foundId[1]);
|
|
}
|
|
};
|
|
|
|
const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/);
|
|
if (foundId) {
|
|
switchGameId(foundId[1]);
|
|
} else {
|
|
switchGameId(randomId());
|
|
}
|
|
|
|
function adjustBoardSize() {
|
|
let container = $('#cb_container');
|
|
let outerWidth = container.width();
|
|
let outerHeight = container.height();
|
|
let size = ((outerWidth < outerHeight) ? outerWidth : outerHeight) - 24;
|
|
$('#cb_board').css({
|
|
width: size + 'px',
|
|
height: size + 'px',
|
|
top: ((outerHeight - size) / 2) + 'px',
|
|
left: ((outerWidth - size) / 2) + 'px',
|
|
visibility: '',
|
|
});
|
|
}
|
|
|
|
new ResizeSensor($('#cb_container'), adjustBoardSize);
|
|
adjustBoardSize();
|
|
|
|
(function() {
|
|
const body = $('body').first();
|
|
let horizontal = false;
|
|
|
|
function updateLayout() {
|
|
const oldHorizontal = horizontal;
|
|
horizontal = body.width() >= (1.5 * body.height());
|
|
if (horizontal !== oldHorizontal) {
|
|
if (horizontal) {
|
|
$('#cb_container').prependTo('#page');
|
|
$('#header').appendTo('#board_ui');
|
|
$('#cb_status').appendTo('#board_ui');
|
|
$('#cb_names').appendTo('#board_ui');
|
|
$('#cb_navigate').appendTo('#board_ui');
|
|
$('#board_ui').appendTo('#page');
|
|
$('#page').removeClass('vertical-layout').addClass('horizontal-layout');
|
|
} else {
|
|
$('#header').appendTo('#board_ui');
|
|
$('#cb_container').appendTo('#board_ui');
|
|
$('#cb_status').appendTo('#board_ui');
|
|
$('#cb_names').appendTo('#board_ui');
|
|
$('#cb_navigate').appendTo('#board_ui');
|
|
$('#board_ui').appendTo('#page');
|
|
$('#page').removeClass('horizontal-layout').addClass('vertical-layout');
|
|
}
|
|
}
|
|
}
|
|
|
|
new ResizeSensor(body, updateLayout);
|
|
updateLayout();
|
|
})();
|
|
|
|
/* Low-level commands to be run from the JS console */
|
|
window.Admin = {
|
|
getGameId() { return $('#cb_board').data('gameId'); },
|
|
getCurrentGame() { return currentGame; },
|
|
getVisibleGame() { return visibleGame; },
|
|
setCurrentGame,
|
|
setVisibleGame,
|
|
refresh() { setCurrentGame(currentGame); },
|
|
renderBoard,
|
|
putState,
|
|
putMeta,
|
|
PS,
|
|
IO,
|
|
$,
|
|
};
|
|
|
|
(function() {
|
|
const match = location.href.match('^https://jessemcdonald.info/~nybble/paco_sako/(.*)');
|
|
if (match) {
|
|
/* Redirect to the new URL */
|
|
location.href = `https://pacosako.jessemcdonald.info/${match[1]}`;
|
|
}
|
|
})();
|
|
});
|
|
|
|
/* vim:set expandtab sw=3 ts=8: */
|