1073 lines
33 KiB
JavaScript
1073 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
import PS from './pacosako.js';
|
|
|
|
import Gun from 'gun';
|
|
import 'gun/nts';
|
|
import 'gun/sea';
|
|
import 'gun/lib/webrtc';
|
|
|
|
import deepEqual from 'deep-equal';
|
|
|
|
import 'webpack-jquery-ui/draggable';
|
|
import 'webpack-jquery-ui/droppable';
|
|
import 'webpack-jquery-ui/selectmenu';
|
|
import 'webpack-jquery-ui/css';
|
|
|
|
/* "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 (){
|
|
/* workaround for persistent errors accumulating which break synchronization */
|
|
if ('localStorage' in window) {
|
|
window.localStorage.removeItem('gun/');
|
|
window.localStorage.removeItem('gap/gun/');
|
|
}
|
|
|
|
let gun = Gun({
|
|
peers: ['https://jessemcdonald.info/gun'],
|
|
});
|
|
|
|
gun.on('in', function(msg) {
|
|
const filter = window.logGunIn;
|
|
if (filter && ((typeof filter !== 'function') || filter(msg)))
|
|
{
|
|
console.log('in msg: ', msg);
|
|
}
|
|
});
|
|
|
|
gun.on('out', function(msg) {
|
|
const filter = window.logGunOut;
|
|
if (filter && ((typeof filter !== 'function') || filter(msg)))
|
|
{
|
|
console.log('out msg: ', msg);
|
|
}
|
|
});
|
|
|
|
function debug() {
|
|
if (window.logDebug) {
|
|
console.log.apply(console, arguments);
|
|
}
|
|
}
|
|
|
|
window.logGunIn = false;
|
|
window.logGunOut = false;
|
|
window.logDebug = false;
|
|
|
|
let currentGame = new PS.Game();
|
|
let visibleGame = new PS.Game(currentGame);
|
|
let cancelGameCallback = function() {};
|
|
let cancelMetaCallback = function() {};
|
|
|
|
Gun.chain.onWithCancel = (function() {
|
|
function cancelCallback(data,key,msg,ev) {
|
|
if (ev && typeof ev.off === 'function') {
|
|
ev.off();
|
|
}
|
|
}
|
|
|
|
return function(tag, arg, eas, as) {
|
|
if (typeof tag === 'function') {
|
|
let callback = tag;
|
|
const cancelEv = function() {
|
|
callback = cancelCallback;
|
|
};
|
|
const wrapper = function() {
|
|
return callback.apply(this, arguments);
|
|
};
|
|
this.on(wrapper, arg, eas, as);
|
|
return cancelEv;
|
|
} else {
|
|
this.on(tag, arg, eas, as);
|
|
return null;
|
|
}
|
|
};
|
|
})();
|
|
|
|
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) {
|
|
try {
|
|
const preview = new PS.Game(currentGame);
|
|
preview.dropHistory();
|
|
preview.move(from, there);
|
|
if (preview.isInCheck(side)) {
|
|
continue;
|
|
}
|
|
} catch (err) {}
|
|
|
|
const square = cbSquare(there);
|
|
square.addClass('cb-legal')
|
|
if (event === 'drag') {
|
|
square.droppable('enable');
|
|
} else if (event === 'click') {
|
|
square.on('click.destination', squareClickDestination);
|
|
}
|
|
}
|
|
}
|
|
|
|
function pieceEndMove(piece, to) {
|
|
let from = piece.data('location');
|
|
piece.appendTo('#cb_hidden');
|
|
|
|
try {
|
|
const meta = { timestamp: new Date(Gun.state()).getTime() };
|
|
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) {
|
|
pieceEndMove(ui.draggable, cbSquareLocation(this));
|
|
}
|
|
|
|
function squareClickUnselect(ev, ui) {
|
|
renderBoard();
|
|
}
|
|
|
|
function squareClickSelect(ev, ui) {
|
|
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');
|
|
}
|
|
|
|
function pieceStartDrag(ev, ui) {
|
|
$('#cb_board .cb-selected').removeClass('cb-selected');
|
|
$('#cb_board .cb-square').off('click.destination');
|
|
pieceStartMove($(this), 'drag');
|
|
}
|
|
|
|
function pieceStopDrag(ev, ui) {
|
|
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.stop(true);
|
|
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: 'invalid',
|
|
zIndex: 100,
|
|
start: pieceStartDrag,
|
|
stop: pieceStopDrag,
|
|
});
|
|
return piece;
|
|
}
|
|
|
|
function renderBoard(animate) {
|
|
$('#cb_board .cb-piece').stop(true);
|
|
$('#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');
|
|
|
|
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);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const liveView = !game.canRedo;
|
|
const playing = game.status === PS.PLAYING;
|
|
|
|
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 clss = game.player === PS.LIGHT ? '.cb-lt-piece' : '.cb-dk-piece';
|
|
const pieces = $('#cb_board ' + clss)
|
|
pieces.parent().on('click.select', squareClickSelect);
|
|
pieces.draggable('enable');
|
|
}
|
|
|
|
if (game.isInCheck()) {
|
|
const clss = game.player === PS.LIGHT ? '.cb-lt-piece' : '.cb-dk-piece';
|
|
$('#cb_board ' + clss + '.cb-king').addClass('cb-in-check');
|
|
}
|
|
|
|
let msg = '';
|
|
let winner = game.winner;
|
|
|
|
if (!liveView) {
|
|
msg += '(Move ' + String(game.moves.length) + ' of ' + String(currentGame.moves.length) + ') ';
|
|
}
|
|
|
|
if (winner) {
|
|
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);
|
|
|
|
$('#cb_history').text(game.history || '');
|
|
|
|
$('#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) + '…';
|
|
}
|
|
|
|
const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 & V3 releases */
|
|
|
|
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);
|
|
gun.get(PacoSakoUUID + '/game/' + gameId).get('board').put(JSON.stringify(moves));
|
|
putMeta();
|
|
}
|
|
|
|
function putMeta(){
|
|
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 = !winner ? null : (lastMove.took === PS.KING) ? 'mate' : 'ended';
|
|
|
|
const meta = {
|
|
lightName,
|
|
darkName,
|
|
moves: turns,
|
|
timestamp: lastMeta.timestamp || new Date(Gun.state()).getTime(),
|
|
status,
|
|
};
|
|
|
|
const metaRef = gun.get(PacoSakoUUID + '/meta/' + gameId).put(meta);
|
|
|
|
if (lightName !== '' || darkName !== '' || turns !== 0) {
|
|
gun.get(PacoSakoUUID + '/meta').get(gameId).put(metaRef);
|
|
} else {
|
|
gun.get(PacoSakoUUID + '/meta').get(gameId).put(null);
|
|
}
|
|
}
|
|
|
|
function switchGameId(newId){
|
|
let boardElem = $('#cb_board');
|
|
let gameId = boardElem.data('gameId');
|
|
|
|
debug('switching from ' + gameId + ' to ' + newId);
|
|
if (newId === gameId) {
|
|
return;
|
|
}
|
|
|
|
cancelGameCallback();
|
|
cancelMetaCallback();
|
|
|
|
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);
|
|
|
|
/* 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 = gun.get(PacoSakoUUID + '/game/' + newId).onWithCancel(function(d) {
|
|
if (d && d.board) {
|
|
try {
|
|
const received = JSON.parse(d.board);
|
|
const moves = { past: [...received.past], future: [...received.future] };
|
|
const oldState = { past: currentGame.moves, future: currentGame.redoMoves };
|
|
|
|
if (deepEqual(moves, oldState)) {
|
|
/* we already have this */
|
|
return;
|
|
}
|
|
|
|
debug('got board', moves);
|
|
|
|
const newGame = new PS.Game();
|
|
for (const move of moves.past) {
|
|
newGame.replayMove(move);
|
|
}
|
|
|
|
let n = 0;
|
|
|
|
for (const move of moves.future.slice().reverse()) {
|
|
newGame.replayMove(move);
|
|
n += 1;
|
|
}
|
|
|
|
for (let i = 0; i < n; ++i) {
|
|
newGame.undo();
|
|
}
|
|
|
|
setCurrentGame(newGame, moves.past.length > currentGame.moves.length);
|
|
} catch (err) {
|
|
debug('Error replaying board state', err);
|
|
}
|
|
}
|
|
});
|
|
|
|
cancelMetaCallback = gun.get(PacoSakoUUID + '/meta').get(newId).onWithCancel(function(d) {
|
|
d = d || {};
|
|
debug('got meta', d);
|
|
$('#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 || ''));
|
|
});
|
|
|
|
let selOpt = $('#cb_game_' + newId);
|
|
if (selOpt.length === 1) {
|
|
$('#cb_select_game')[0].selectedIndex = selOpt.index();
|
|
} else {
|
|
$('#cb_select_game')[0].selectedIndex = -1;
|
|
}
|
|
|
|
$('#jitsi_link').attr('href', 'https://meet.jit.si/PacoSaco_' + newId);
|
|
|
|
refreshSelectOptions();
|
|
}
|
|
|
|
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().getTime();
|
|
const then = $(window).data('notifyAfter');
|
|
if (!then || now >= then) {
|
|
try { notifyAudio.play(); } catch (err) {}
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
navigator.serviceWorker.register('sw.js').catch(disableNotify);
|
|
|
|
if (Notification.permission === 'denied') {
|
|
disableNotify();
|
|
}
|
|
} catch (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').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(Gun.state()).getTime() };
|
|
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_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_/, ''));
|
|
}
|
|
}
|
|
});
|
|
|
|
let updateMeta = function() { putMeta(); }
|
|
$('#cb_light_name').on('input', updateMeta);
|
|
$('#cb_dark_name').on('input', updateMeta);
|
|
|
|
function updateTitle(opt){
|
|
opt = $(opt);
|
|
|
|
const then = opt.data('then');
|
|
const now = new Date(Gun.state()).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) {
|
|
/* prune entries with no activity in 14 days, but leave the current game */
|
|
if (opt.data('gameId') !== $('#cb_board').data('gameId')) {
|
|
opt.remove();
|
|
return;
|
|
}
|
|
}
|
|
age_str = ' (' + Math.floor((now - then) / (24*60*60*1000)) + 'd)';
|
|
}
|
|
|
|
const newText = opt.data('title') + age_str;
|
|
if (opt.text() !== newText) {
|
|
opt.text(newText);
|
|
}
|
|
}
|
|
|
|
const refreshSelectOptions = (function(){
|
|
function updateSelectMeta(d, key) {
|
|
d = d || {};
|
|
debug('got meta for key ' + key, d);
|
|
const lightName = shortenName(String(d.lightName || 'Light'));
|
|
const darkName = shortenName(String(d.darkName || 'Dark'));
|
|
const moves = !d.moves ? '' :
|
|
(', ' + d.moves + (d.moves === 1 ? ' turn' : ' turns'));
|
|
|
|
let opt = $('#cb_game_' + key);
|
|
|
|
if (!d.lightName && !d.darkName && !d.moves && key !== $('#cb_board').data('gameId')) {
|
|
if (opt.length >= 1) {
|
|
opt.remove();
|
|
}
|
|
} else {
|
|
if (opt.length === 0) {
|
|
opt = $('<option></option>');
|
|
opt.attr('id', 'cb_game_' + key);
|
|
}
|
|
|
|
let stat = '';
|
|
if (d.status) {
|
|
stat = ', ' + d.status;
|
|
}
|
|
|
|
opt.data('gameId', key);
|
|
opt.data('title', lightName + ' vs. ' + darkName + moves + stat);
|
|
opt.data('then', d.timestamp || new Date(Gun.state()).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;
|
|
}
|
|
}
|
|
|
|
let cancelAll = function(){};
|
|
|
|
return function() {
|
|
cancelAll();
|
|
|
|
$('#cb_select_game .cb-game-option').remove();
|
|
updateSelectMeta(null, $('#cb_board').data('gameId'));
|
|
|
|
const cancellers = {};
|
|
let cancelMeta = gun.get(PacoSakoUUID + '/meta').onWithCancel(function(meta) {
|
|
for (const gameId in meta) { /* use of 'in' here is deliberate */
|
|
/* 'gameId' may include extra GUN fields like '_' */
|
|
if (gameId.match(/^[0-9a-f]{16}$/)) {
|
|
if (!Gun.obj.is(meta[gameId])) {
|
|
updateSelectMeta(null, gameId);
|
|
if (gameId in cancellers) {
|
|
cancellers[gameId]();
|
|
delete cancellers[gameId];
|
|
}
|
|
} else if (!(gameId in cancellers)) {
|
|
cancellers[gameId] = gun.get(meta[gameId]).onWithCancel(function(d) {
|
|
updateSelectMeta(d, gameId);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}, { change: true });
|
|
|
|
cancelAll = function() {
|
|
cancelMeta();
|
|
cancelMeta = function(){};
|
|
for (const gameId in cancellers) {
|
|
cancellers[gameId]();
|
|
delete cancellers[gameId];
|
|
}
|
|
cancelAll = function(){};
|
|
};
|
|
|
|
return cancelAll;
|
|
};
|
|
})();
|
|
|
|
refreshSelectOptions();
|
|
|
|
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){
|
|
const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/);
|
|
if (foundId) {
|
|
switchGameId(foundId[1]);
|
|
}
|
|
};
|
|
|
|
/* force page reload after four hours to keep client code up to date */
|
|
window.setTimeout(function(){ location.reload(); }, 4*3600*1000);
|
|
|
|
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 = {
|
|
convertFromV2: function() {
|
|
gun.get(PacoSakoUUID).get('games').map().once(function(d,key){
|
|
if (d && d.board) {
|
|
debug('converting ' + key);
|
|
gun.get(PacoSakoUUID + '/game/' + key).get('board').put(d.board);
|
|
}
|
|
});
|
|
|
|
gun.get(PacoSakoUUID).get('meta').map().once(function(d,key){
|
|
if (d) {
|
|
debug('converting metadata for ' + key);
|
|
gun.get(PacoSakoUUID + '/meta').get(key).put(d);
|
|
}
|
|
});
|
|
},
|
|
|
|
getCurrentGame: function() { return currentGame; },
|
|
getVisibleGame: function() { return visibleGame; },
|
|
setCurrentGame: setCurrentGame,
|
|
setVisibleGame: setVisibleGame,
|
|
refresh: function() { setCurrentGame(currentGame); },
|
|
renderBoard: renderBoard,
|
|
putState: putState,
|
|
putMeta: putMeta,
|
|
gun: gun,
|
|
PacoSakoUUID: PacoSakoUUID,
|
|
};
|
|
});
|
|
|
|
/* vim:set expandtab sw=3 ts=8: */
|