paco_sako/js/pacosako_ui.js

1274 lines
39 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';
/* "Waterdrop" by Porphyr (freesound.org/people/Porphyr) / CC BY 3.0 (creativecommons.org/licenses/by/3.0) */
import Waterdrop from '../wav/191678__porphyr__waterdrop.wav';
$(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 pieceTypeCode(type) {
if (type === PS.KING) {
return 'k';
} else if (type === PS.QUEEN) {
return 'q';
} else if (type === PS.ROOK) {
return 'r';
} else if (type === PS.KNIGHT) {
return 'n';
} else if (type === PS.BISHOP) {
return 'b';
} else if (type === PS.PAWN) {
return 'p';
} else {
throw { message: 'unknown piece type', type: type };
}
}
function pieceSideCode(side) {
if (side === PS.LIGHT) {
return 'l';
} else if (side === PS.DARK) {
return 'd';
} else {
throw { message: 'unknown side', side: side };
}
}
function pieceCode(side, type) {
return pieceTypeCode(type) + pieceSideCode(side);
}
function cbSquare(where) {
if (where === PS.PHANTOM) {
return $('#cb_phantom').first()
} else if (where.match(/^[a-h][1-8]$/)) {
return $('#cb_' + where).first()
} else {
return null;
}
}
function cbSquareLocation(square) {
const id = square.id || square[0].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) {
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);
}
$('#cb_board').data('last_state', currentGame.moves);
setCurrentGame(currentGame);
putState();
}
function squareClickDestination(ev, ui) {
let selected = $('#cb_board .cb-selected');
if (selected.length !== 1) {
renderBoard();
return;
}
pieceEndMove(selected, cbSquareLocation(this));
}
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')) {
renderBoard();
}
}
function placePiece(where, side, type, suffix) {
const code = pieceCode(side, type);
const piece_id = 'cb_piece_' + code + '_' + suffix;
let piece = $('#' + piece_id);
if (piece.length < 1) {
piece = $('#cb_piece_' + code).clone().prop({ id: piece_id });
}
piece.finish();
piece.attr('id', piece_id);
piece.removeClass('cb-selected');
piece.removeClass('cb-in-check');
piece.removeAttr('style');
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').finish();
$('#cb_board .cb-square').off('click.select');
$('#cb_board .cb-square').off('click.unselect');
$('#cb_board .cb-square').off('click.destination');
$('#cb_board .cb-piece.cb-selected').removeClass('cb-selected');
$('#cb_board .cb-piece.cb-in-check').removeClass('cb-in-check');
$('#cb_board .cb-piece').removeAttr('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');
$('#cb_explain_check').text('');
const game = visibleGame;
const board = game.board;
for (const side of [PS.LIGHT, PS.DARK]) {
const counters = {};
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) {
if (!counters[type]) {
counters[type] = 0;
}
placePiece(here, side, type, String(++counters[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) {
let pieces = cbSquare(lastMove.to).find('.cb-piece');
if (!lastMove.alongside) {
pieces = pieces.filter(lastMove.side === PS.LIGHT ? '.cb-lt-piece' : '.cb-dk-piece');
}
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 = game.player === PS.LIGHT ? '.cb-lt-piece' : '.cb-dk-piece';
const phantom = board.phantom;
if (phantom) {
const piece = placePiece(PS.PHANTOM, phantom.side, phantom.type, 'ph');
cbSquare(PS.PHANTOM).appendTo(cbSquare(phantom.from));
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 ' + clss + '.cb-king');
if (check) {
king.addClass('cb-in-check');
}
if (typeof check === 'string') {
$('#cb_explain_check').text(`(Check: ${check})`);
}
let msg = '';
let winner = game.winner;
if (!liveView) {
msg += '(Move ' + String(game.moves.length) + ' of ' + String(currentGame.moves.length) + ') ';
}
if (winner) {
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 (playing) {
if (game.isInCheck()) {
msg += 'Check! ';
}
msg += (game.player === PS.LIGHT ? 'Light' : 'Dark') + ' player\'s turn.';
} else {
msg += 'Game ended in a draw.';
}
$('#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) {
closeNotifications();
currentGame = game;
setVisibleGame(game, animate);
const moves = game.moves;
const cb_board = $('#cb_board').first();
if (!deepEqual(moves, cb_board.data('last_state'))) {
/* ignore partial moves */
if (!game.board.phantom) {
if ($('#cb_notify')[0].checked) {
const gameString = cb_board.data('lightName') + ' vs. ' + cb_board.data('darkName');
notify(gameString + '\n' + $('#cb_message').text());
}
if ($('#cb_sound')[0].checked) {
playNotifySound();
}
}
}
cb_board.data('last_state', moves);
}
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,
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.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;
/* 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;
/* 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');
const moves = { past: currentGame.moves, future: currentGame.redoMoves };
boardElem.data('last_state', currentGame.moves);
putMeta({ board: moves });
}
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 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);
const notifyAfter = +new Date() + 2000;
$(window).data('notifyAfter', +new Date() + 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);
/* this will be the starting state if no data is received from peers */
setCurrentGame(new PS.Game());
boardElem.data('last_state', currentGame.moves);
boardElem.data('lightName', 'Light');
boardElem.data('darkName', 'Dark');
$('#cb_light_name').val('');
$('#cb_dark_name').val('');
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);
}
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);
}
});
$('#jitsi_link').attr('href', 'https://meet.jit.si/PacoSaco_' + newId);
$('#game_link').attr('href', location.href);
/* 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(){
$('#cb_notify')[0].checked = 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(){
const now = +new Date();
const then = $(window).data('notifyAfter');
if (!then || now >= then) {
try { notifyAudio.play(); } catch (err) {}
}
}
function notify(body) {
const now = +new Date();
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){
try {
if ('getNotifications' in registration) {
registration.getNotifications({tag: 'notice'}).then(function(notifications){
for (const notification of notifications) {
notification.close();
}
});
}
} catch (err) {}
});
} 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');
function showSkipWaitingPrompt(event) {
if (confirmBox) {
confirmBox.close({ ignoreDelay: true });
}
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,
confirm() {
/* The SW should signal us to reload, but do it after 20s regardless. */
setTimeout(() => { window.location.reload(); }, 20000);
messageSW(event.sw, {type: 'SKIP_WAITING'});
$('.update-confirm-button').text('Updating…');
},
onCloseComplete() {
this.destroy();
},
});
confirmBox.open();
}
wb.addEventListener('installed', (event) => {
try {
if (Notification.permission === 'denied') {
disableNotify();
}
} catch (err) {
disableNotify();
}
});
wb.addEventListener('controlling', (event) => {
if (event.isUpdate) {
window.location.reload();
}
});
wb.addEventListener('waiting', showSkipWaitingPrompt);
wb.addEventListener('externalwaiting', showSkipWaitingPrompt);
const registration = await wb.register();
if ('update' in registration) {
/* Check for updates every 4h without reloading the page. */
setInterval(() => { registration.update(); }, 4*3600*1000);
} else {
console.log('service worker update method not supported, disabling update checks');
}
}).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) {
debug('from localStorage', { key: key, value: value });
if (key === LS_KEY_NOTIFY) {
const doNotify = value === 'on';
const cb_notify = $('#cb_notify')[0];
const wasChecked = cb_notify.checked;
cb_notify.checked = doNotify;
if (doNotify && !wasChecked) {
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) {
const doReverse = value === 'on';
const cb_reverse = $('#cb_reverse')[0];
cb_reverse.checked = doReverse;
arrangeBoard(doReverse);
} else if (key === LS_KEY_THEME) {
const cb_theme = $('#cb_select_theme');
if (value !== cb_theme.val()) {
cb_theme.val(value);
if (!cb_theme.val()) {
value = 'traditional';
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);
if (value !== null) {
fromStorage(key, value);
}
}
}
$('.cb-square, .cb-phantom').droppable({
accept: '.cb-piece',
disabled: true,
deactivate: function(ev, ui){
$(this).droppable('disable');
},
drop: squareDropDestination,
});
$('#cb_notify').on('change', function(){
if ('localStorage' in window) {
window.localStorage.setItem(LS_KEY_NOTIFY, this.checked ? 'on' : 'off');
}
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(){
debug('cb_reverse changed to ' + this.checked);
if ('localStorage' in window) {
window.localStorage.setItem(LS_KEY_REVERSE, this.checked ? 'on' : 'off');
}
arrangeBoard(this.checked);
});
$('#cb_undo').on('click', function(){
if (currentGame.canUndo) {
currentGame.undo();
$('#cb_board').data('last_state', currentGame.moves);
setCurrentGame(currentGame);
putState();
}
});
$('#cb_redo').on('click', function(){
if (currentGame.canRedo) {
currentGame.redo();
$('#cb_board').data('last_state', currentGame.moves);
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);
}
$('#cb_board').data('last_state', currentGame.moves);
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();
}
});
$('#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);
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());
}
/* Low-level commands to be run from the JS console */
window.Admin = {
getCurrentGame: function() { return currentGame; },
getVisibleGame: function() { return visibleGame; },
setCurrentGame,
setVisibleGame,
refresh: function() { setCurrentGame(currentGame); },
renderBoard,
putState,
putMeta,
IO,
};
/*
* Safari uses the container's *height* instead of its *width* as
* required by W3C standards for relative margin/padding values.
* This breaks the CSS that should be ensuring a 1:1 aspect ratio.
*/
function fixSafariPadding() {
let squares = $('#cb_board .cb-square');
for (let square of squares) {
square = $(square);
const width = square.width();
square.css('height', width + 'px');
}
}
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
$(window).on('resize', fixSafariPadding);
fixSafariPadding();
}
(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: */