'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'; /* "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'); 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 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 (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, '$&')); $('#cb_history_future').html(escape(futureHistory).replace(/\d+\./g, '$&')); $('#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) { 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) ? '' : (currentGame.status === PS.CHECKMATE) ? 'checkmate' : (currentGame.status === PS.RESIGNED) ? 'resigned' : '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); } } } 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, .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 = $(`
`).appendTo('body'); let gameTiles = $(`
New
Game
`).appendTo(gameSelectContent); var selectBox = new jBox('Modal', { content: gameSelectContent, blockScroll: false, blockScrollAdjust: false, isolateScroll: false, delayClose: 750, }); $('.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(tiles) { const now = +new Date(); for (const tileElem of (tiles || $('.game-tile.existing-game'))) { 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); } } window.setInterval(() => { updateTileAges(); }, 15000); 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 : $(`
`).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 = $('
').appendTo(tile); $('').appendTo(titleBlock) .text(shortenName(String(data.lightName || 'Light'))); $(`vs.`).appendTo(titleBlock); $('').appendTo(titleBlock) .text(shortenName(String(data.darkName || 'Dark'))); $('
').appendTo(tile); if (data.status) { $('
').appendTo(tile).text(data.status); } if (data.moves) { $('
').appendTo(tile) .text(inWords(data.moves, 'turn')); } $('
').addClass('game-tile-age').appendTo(tile); updateTileAges(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); 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]); } }; /* 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 = { 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(); } }); /* vim:set expandtab sw=3 ts=8: */