'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); $('#game_link').attr('href', location.href); 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){ 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); } } } 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 = $(''); 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, }; /* * 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(); } }); /* vim:set expandtab sw=3 ts=8: */