'use strict'; let gun = Gun({ peers: ['https://jessemcdonald.info/gun'], /* workaround for persistent errors accumulating which break synchronization */ localStorage: false, }); let initialBoard = (function (){ let init = JSON.stringify({ light: [ 'rnbqkbnr', 'pppppppp', ' ', ' ', ' ', ' ', ' ', ' ', ], dark: [ ' ', ' ', ' ', ' ', ' ', ' ', 'pppppppp', 'rnbqkbnr', ], player: 'light', timestamp: new Date().getTime(), }); return function (){ return JSON.parse(init); } })(); function cloneJSON(obj){ return JSON.parse(JSON.stringify(obj)); } function isLight(side){ return side[0] === 'l'; } function isDark(side){ return side[0] === 'd'; } function normalizeSide(side){ return isDark(side) ? 'dark' : 'light'; } function otherSide(side){ return isDark(side) ? 'light' : 'dark'; } function boardGet(board, where, side){ side = normalizeSide(side); if (where === 'phantom') { if (!board.phantom || board.phantom.type[1] !== side[0]) { return ' '; } else { return board.phantom.type[0]; } } else { let column = 'abcdefgh'.indexOf(where[0]); let row = Number(where[1]) - 1; return board[side][row][column]; } } function boardPut(board, where, side, piece){ side = normalizeSide(side); let column = 'abcdefgh'.indexOf(where[0]); let row = Number(where[1]) - 1; let data = board[side][row]; let prefix = data.substring(0, column); let suffix = data.substring(column + 1, 8); board[side][row] = prefix + piece + suffix; } function movedFrom(board){ if (!board || !board.move || !board.move.from) { return null; } else if (board.move.from === 'phantom') { return board.prior.phantom.from; } else { return board.move.from; } } function movedTo(board){ if (!board || !board.move || !board.move.to) { return null; } else { return board.move.to; } } function countMoves(board) { let n = 0; while (board && board.prior) { if (board.prior.player !== board.player) { ++n; } board = board.prior; } return n; } function offsetSquare(from, columnsRight, rowsUp){ let column = 'abcdefgh'.indexOf(from[0]); let row = Number(from[1]) - 1; if (column < 0 || column > 7 || row < 0 || row > 7) { return null; } column += columnsRight; row += rowsUp; if (column < 0 || column > 7 || row < 0 || row > 7) { return null; } return 'abcdefgh'[column] + String(row + 1); } function validDestination(board, side, where, canCapture){ let ours = boardGet(board, where, side); let theirs = boardGet(board, where, otherSide(side)); return ((theirs === ' ') ? (ours === ' ') : canCapture); } function scanPath(accum, board, side, from, canCapture, columnsLeft, rowsUp, remainder){ while (true) { let there = offsetSquare(from, columnsLeft, rowsUp); if (!there || !validDestination(board, side, there, canCapture)) { return; } accum.push(there); if (boardGet(board, there, otherSide(side)) !== ' ' || remainder < 1) { return; } from = there; remainder -= 1; } } function hasMoved(board, side, where){ side = normalizeSide(side); while (true) { if (!board) { return false; } const move = board.move; if (move && move.side === side && move.to === where) { return true; } board = board.prior; } } function legalMoves(board, side, type, from, canCapture){ if (board.move && board.move.took === 'k') { /* checkmate, the game is over */ return []; } const ortho = [[-1, 0], [1, 0], [0, -1], [0, 1]]; const diag = [[-1, -1], [-1, 1], [1, -1], [1, 1]]; let legals = []; if (from === 'phantom') { from = board.phantom.from; } if (type === 'k') { for (const dir of ortho.concat(diag)) { scanPath(legals, board, side, from, false, dir[0], dir[1], 0); } if (from[0] === 'e' && from[1] === (isDark(side) ? '8' : '1')) { const other = otherSide(side); /* check for castling conditions */ if (!hasMoved(board, side, from)) { if (boardGet(board, 'f' + from[1], side) === ' ' && boardGet(board, 'f' + from[1], other) === ' ' && boardGet(board, 'g' + from[1], side) === ' ' && boardGet(board, 'g' + from[1], other) === ' ' && boardGet(board, 'h' + from[1], side) === 'r') { if (!hasMoved(board, side, 'h' + from[1])) { legals.push('g' + from[1]); } } if (boardGet(board, 'd' + from[1], side) === ' ' && boardGet(board, 'd' + from[1], other) === ' ' && boardGet(board, 'c' + from[1], side) === ' ' && boardGet(board, 'c' + from[1], other) === ' ' && boardGet(board, 'b' + from[1], side) === ' ' && boardGet(board, 'b' + from[1], other) === ' ' && boardGet(board, 'a' + from[1], side) === 'r') { if (!hasMoved(board, side, 'a' + from[1])) { legals.push('c' + from[1]); } } } } } else if (type === 'q') { for (const dir of ortho.concat(diag)) { scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8); } } else if (type === 'b') { for (const dir of diag) { scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8); } } else if (type === 'n') { for (const there of [offsetSquare(from, -1, 2), offsetSquare(from, 1, 2), offsetSquare(from, -2, 1), offsetSquare(from, 2, 1), offsetSquare(from, -2, -1), offsetSquare(from, 2, -1), offsetSquare(from, -1, -2), offsetSquare(from, 1, -2)]) { if (there && validDestination(board, side, there, canCapture)) { legals.push(there); } } } else if (type === 'r') { for (const dir of ortho) { scanPath(legals, board, side, from, canCapture, dir[0], dir[1], 8); } } else if (type === 'p') { const dark = isDark(side); const forward = offsetSquare(from, 0, dark ? -1 : 1); const forward2 = offsetSquare(from, 0, dark ? -2 : 2); const diagL = offsetSquare(from, -1, dark ? -1 : 1); const diagR = offsetSquare(from, 1, dark ? -1 : 1); if (forward && validDestination(board, side, forward, false)) { legals.push(forward); if (dark ? (from[1] >= '7') : (from[1] <= '2')) { if (forward2 && validDestination(board, side, forward2, false)) { legals.push(forward2); } } } if (canCapture) { for (const there of [diagL, diagR]) { if (there) { if (boardGet(board, there, otherSide(side)) !== ' ') { legals.push(there); } else if (forward2) { let otherBoard = board; while (otherBoard && otherBoard.move && otherBoard.move.side[0] === side[0]) { otherBoard = otherBoard.prior; } if (otherBoard && otherBoard.move) { const move = otherBoard.move; if (move.side[0] !== side[0] && move.type === 'p') { const from = movedFrom(otherBoard); if (from && from[0] === there[0] && from[1] === forward2[1]) { /* en passant */ legals.push(there); } } } } } } } } return legals; } function movePiece(priorBoard, side, from, to){ let other = otherSide(side); let type = boardGet(priorBoard, from, side); let took = boardGet(priorBoard, to, other); let replacedFrom = to; let replaced = boardGet(priorBoard, replacedFrom, side); let alongside = boardGet(priorBoard, from, other); let actuallyFrom = (from === 'phantom') ? priorBoard.phantom.from : from; const legals = legalMoves(priorBoard, side, type, from, alongside === ' '); if (!legals.includes(to)) { return priorBoard; } let undoBoard = priorBoard; if (undoBoard.subsequent) { undoBoard = cloneJSON(undoBoard); delete undoBoard.subsequent; } let board = cloneJSON(undoBoard); board.prior = undoBoard; board.timestamp = new Date().getTime(); board.move = { side: normalizeSide(side), type: type, from: from, to: to }; if (took !== ' ') { board.move.took = took; } if (replaced !== ' ') { board.move.replaced = replaced; } if (alongside !== ' ') { board.move.alongside = alongside; } if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'g') { board.move.castle = true; const alongsideRook = boardGet(board, 'h' + actuallyFrom[1], other); boardPut(board, 'h' + actuallyFrom[1], side, ' '); boardPut(board, 'h' + actuallyFrom[1], other, ' '); boardPut(board, 'f' + actuallyFrom[1], side, 'r'); boardPut(board, 'f' + actuallyFrom[1], other, alongsideRook); } else if (type === 'k' && actuallyFrom[0] === 'e' && to[0] === 'c') { board.move.queen_castle = true; const alongsideRook = boardGet(board, 'a' + actuallyFrom[1], other); boardPut(board, 'a' + actuallyFrom[1], side, ' '); boardPut(board, 'a' + actuallyFrom[1], other, ' '); boardPut(board, 'd' + actuallyFrom[1], side, 'r'); boardPut(board, 'd' + actuallyFrom[1], other, alongsideRook); } else if (type === 'p' && alongside === ' ' && replaced === ' ' && to[0] !== actuallyFrom[0]) { let otherBoard = priorBoard; /* scan for the opponent's last move, since this could be part of a chain */ while (otherBoard && otherBoard.move && otherBoard.move.side[0] === side[0]) { otherBoard = otherBoard.prior; } if (otherBoard && otherBoard.move) { const move = otherBoard.move; if (move.type === 'p' && move.to[0] === to[0] && move.to[1] === actuallyFrom[1]) { const moveFrom = movedFrom(otherBoard); if (move.side[0] === other[0] && moveFrom[1] != to[1]) { board.move.en_passant = true; board.move.took = 'p'; /* move the opponent's pawn back one space */ boardPut(board, move.to, other, ' '); boardPut(board, to, other, 'p'); /* see if we replaced a piece from the other square */ replacedFrom = move.to; replaced = boardGet(board, replacedFrom, side); boardPut(board, replacedFrom, side, ' '); } } } } if (type === 'p' && to[1] === (isDark(side) ? '1' : '8')) { board.move.promotion = 'q'; /* TODO: allow other choices */ type = 'q'; } if (alongside === 'p' && to[1] === (isDark(other) ? '1' : '8')) { board.move.promotion = 'q'; /* TODO: allow other choices */ alongside = 'q'; } if (from === 'phantom') { delete board.phantom; } else { boardPut(board, from, side, ' '); boardPut(board, from, other, ' '); } boardPut(board, to, side, type); if (alongside !== ' ') { boardPut(board, to, other, alongside); } if (replaced === ' ') { board.player = otherSide(board.player); } else { board.phantom = { from: replacedFrom, type: replaced + side[0] }; } return board; } function findPieces(board, side, type, alongside){ let pieces = []; const other = otherSide(side); if (board.phantom && board.phantom.type === type + side[0]) { pieces.push(board.phantom.from); } for (const row of "12345678") { for (const column of "abcdefgh") { const here = column + row; if (boardGet(board, here, side) === type) { if (!alongside || boardGet(board, here, other) === alongside) { pieces.push(here); } } } } return pieces; } function renderHistory(currentBoard) { let list = []; for (let board = currentBoard; board && board.move; board = board.prior) { list.push(board); } const NBSP = '\u00a0'; /* non-breaking space */ const SHY = '\u00ad' /* soft hyphen */ const ZWSP = '\u200b'; /* zero-width space */ let result = ''; let n = 0; while (list.length > 0) { const board = list.pop(); const move = board.move; if (move.from === 'phantom') { result += SHY + '*'; } else { if (n > 0 || isDark(move.side)) { result += ' '; } if (isLight(move.side)) { ++n; result += String(n) + '.' + NBSP; } } if (move.pass) { result += '...'; } else if (move.castle) { result += 'O-O'; } else if (move.queen_castle) { result += 'O-O-O'; } else { let piece = ''; if (move.alongside) { if (isLight(move.side)) { piece = move.type.toUpperCase() + move.alongside.toUpperCase(); } else { piece = move.alongside.toUpperCase() + move.type.toUpperCase(); } } else { piece = move.type === 'p' ? '' : move.type.toUpperCase(); } const from = movedFrom(board); if (move.from !== 'phantom' || from !== movedTo(board.prior)) { const sameKind = findPieces(board.prior, move.side, move.type, move.alongside || ' '); const legalFrom = []; let sameFile = 0; /* column / letter */ let sameRank = 0; /* row / number */ for (const where of sameKind) { if (legalMoves(board.prior, move.side, move.type, where, true).includes(move.to)) { legalFrom.push(where); if (where[0] === from[0]) { sameFile += 1; } if (where[1] === from[1]) { sameRank += 1; } } } if (legalFrom.length !== 1 || move.en_passant || (move.type === 'p' && move.took)) { /* append file, rank, or both to disambiguate */ if (sameFile === 1) { piece += from[0]; } else if (sameRank === 1) { piece += from[1]; } else { piece += from; } } } const took = move.took ? 'x' : ''; result += piece + took + move.to; } if (move.en_passant) { result += 'e.p.'; } if (move.promotion) { result += '(' + move.promotion[0].toUpperCase() + ')'; } if (move.took === 'k') { result += '#'; } } return result; } function pieceStartDrag(ev, ui){ const board = $('#cb_board').data('board'); const dragged = $(this); const type = dragged.data('type'); const from = dragged.data('location'); const where = (from === 'phantom') ? board.phantom.from : from; const canCapture = boardGet(board, from, otherSide(type[1])) === ' '; const legals = legalMoves(board, type[1], type[0], where, canCapture); for (const there of legals) { $('#cb_' + there).addClass('cb-legal').droppable('enable'); } } function pieceStopDrag(ev, ui){ $('#cb_board .cb-legal').removeClass('cb-legal').droppable('disable'); } function placePiece(where, type, count){ let piece_id = 'cb_piece_' + type + '_' + count; let piece = $($('#' + piece_id)[0] || $('#cb_piece_' + type + ' img').clone()); piece.attr('style', ''); piece.attr('id', piece_id); piece.data({ type: type, location: where }); piece.appendTo('#cb_' + where); piece.draggable({ disabled: true, containment: '#cb_inner', revert: 'invalid', zIndex: 100, start: pieceStartDrag, stop: pieceStopDrag, }); return piece; } function renderBoard(board){ $('#cb_board .cb-piece .ui-draggable').draggable('destroy'); $('#cb_board .cb-piece').attr('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'); for (const side of ['light', 'dark']) { let counters = {}; for (let row = 0; row < 8; ++row) { for (let column = 0; column < 8; ++column) { let here = 'abcdefgh'[column] + String(row+1); let type = board[side][row][column]; if (type !== ' ') { if (!counters[type]) { counters[type] = 0; } let count = ++counters[type]; placePiece(here, type + side[0], count); } } } } let clss = isLight(board.player) ? '.cb-lt-piece' : '.cb-dk-piece'; if (board.phantom) { let where = board.phantom.from; placePiece('phantom', board.phantom.type, 'ph'); $('#cb_phantom').appendTo('#cb_' + where); $('#cb_board .ui-draggable').draggable('disable'); $('#cb_phantom .ui-draggable-disabled').filter(clss).draggable('enable'); } else { $('#cb_board .ui-draggable').draggable('disable'); if (!board.move || board.move.took !== 'k') { $('#cb_board .ui-draggable-disabled').filter(clss).draggable('enable'); } } const start = movedFrom(board); if (start) { $('#cb_' + start).addClass('cb-start'); } const end = movedTo(board); if (end) { $('#cb_' + end).addClass('cb-end'); } let msg = ''; if (board.move && board.move.took === 'k') { msg = (isDark(board.move.side) ? 'Dark' : 'Light') + ' player won!'; } else { msg = (isDark(board.player) ? 'Dark' : 'Light') + " player's turn"; } $('#cb_message').text(msg); $('#cb_history').text(renderHistory(board)); } function setVisibleBoard(board){ const cb_board = $('#cb_board').first(); const currentBoard = cloneJSON(cb_board.data('board')); delete currentBoard.subsequent; const live = deepEqual(board, currentBoard); $('#cb_board').data('visible_board', board); renderBoard(board); $('#cb_nav_first').attr('disabled', board.prior ? false : true); $('#cb_nav_prev_turn').attr('disabled', board.prior ? false : true); $('#cb_nav_prev_state').attr('disabled', board.prior ? false : true); $('#cb_nav_next_state').attr('disabled', board.subsequent ? false : true); $('#cb_nav_next_turn').attr('disabled', board.subsequent ? false : true); if (live) { /* the 'visible' board may be missing .subsequent */ const liveBoard = $('#cb_board').data('board'); $('#cb_undo').attr('disabled', liveBoard.prior ? false : true); $('#cb_redo').attr('disabled', liveBoard.subsequent ? false : true); $('#cb_pass').attr('disabled', liveBoard.phantom ? true : false); $('#cb_nav_last').attr('disabled', true); $('#cb_board').addClass('cb-live'); $('#cb_board').removeClass('cb-archive'); } else { $('#cb_undo').attr('disabled', true); $('#cb_redo').attr('disabled', true); $('#cb_pass').attr('disabled', true); $('#cb_board .ui-draggable').draggable('disable'); $('#cb_nav_last').attr('disabled', false); $('#cb_board').removeClass('cb-live'); $('#cb_board').addClass('cb-archive'); } } function setCurrentBoard(board){ const cb_board = $('#cb_board').first(); cb_board.data('board', board); /* navigation should not include the redo stack */ const visible = cloneJSON(board); delete visible.subsequent; setVisibleBoard(visible); if ($('#cb_notify')[0].checked && !deepEqual(board, cb_board.data('last_state'))) { /* ignore partial moves and undo/redo */ if (!board.phantom && !board.subsequent) { const gameString = cb_board.data('lightName') + ' vs. ' + cb_board.data('darkName'); notify(gameString + '\n' + $('#cb_message').text()); } } cb_board.data('last_state', cloneJSON(board)); } function randomId(){ let res = ''; for (let i = 0; i < 4; ++i) { let part = Math.floor(Math.random() * 65536).toString(16); res = res + ('0000'.substring(part.length, 4) + part); } return res; } let PacoSakoUUID = '7c38edd4-c931-49c8-9f1a-84de560815db'; function putState(board){ let boardElem = $('#cb_board'); let gameId = boardElem.data('gameId'); let game = gun.get(PacoSakoUUID).get('games').get(gameId); boardElem.data('last_state', cloneJSON(board)); game.put({ board: JSON.stringify(board) }); putMeta(); } function putMeta(){ let board = $('#cb_board').data('board'); let gameId = $('#cb_board').data('gameId'); let lightName = $('#cb_light_name').val(); let darkName = $('#cb_dark_name').val(); let meta = gun.get(PacoSakoUUID).get('meta').get(gameId); let stat = (board.move && board.move.took === 'k') ? 'mate' : null; meta.put({ gameId: gameId, lightName: lightName, darkName: darkName, moves: countMoves(board), timestamp: board.timestamp || new Date().getTime(), status: stat, }); } function switchGameId(newId){ let boardElem = $('#cb_board'); let gameId = boardElem.data('gameId'); if (newId === gameId) { return; } if (gameId) { //gun.get(PacoSakoUUID).get('games').get(gameId).off(); //gun.get(PacoSakoUUID).get('meta').get(gameId).off(); } 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); closeNotifications(); /* this will be the starting state if no data is received from peers */ let newBoard = initialBoard(); setCurrentBoard(newBoard); boardElem.data('last_state', cloneJSON(newBoard)); boardElem.data('lightName', 'Light'); boardElem.data('darkName', 'Dark'); $('#cb_light_name').val(''); $('#cb_dark_name').val(''); gun.get(PacoSakoUUID).get('games').get(newId).on(function(d){ if (d && d.board && $('#cb_board').data('gameId') === newId) { const board = JSON.parse(d.board); setCurrentBoard(board); } }); gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){ if (d && $('#cb_board').data('gameId') === newId) { $('#cb_board').data('lightName', d.lightName || 'Light'); $('#cb_board').data('darkName', d.darkName || 'Dark'); $('#cb_light_name').val(d.lightName || ''); $('#cb_dark_name').val(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; } } 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(); } } 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 (){ try { if (Notification.permission === 'denied') { disableNotify(); } else { navigator.serviceWorker.register('sw.js').catch(disableNotify); } } catch (err) { disableNotify(); } if ('localStorage' in window) { function updateNotify(newValue){ const doNotify = newValue === 'on'; const cb_notify = $('#cb_notify')[0]; if (doNotify) { if (!cb_notify.checked) { cb_notify.checked = true; requestNotify(); } } else if (cb_notify.checked) { cb_notify.checked = false; } } $(window).on('storage', function(event){ if (event.originalEvent.key === '/pacosako/notify') { updateNotify(event.originalEvent.newValue); } }); updateNotify(window.localStorage.getItem('/pacosako/notify')); } $('.cb-square').droppable({ accept: '.cb-piece', disabled: true, deactivate: function(ev, ui){ $(this).droppable('disable'); }, drop: function(ev, ui) { let dragged = ui.draggable; let type = dragged.data('type'); let from = dragged.data('location'); let to = this.id.replace(/^cb_/, ''); dragged.appendTo('#cb_hidden'); let newBoard = movePiece($('#cb_board').data('board'), type[1], from, to); renderBoard(newBoard); putState(newBoard); }, }); function stepBack(board){ if (!board.prior) { return cloneJSON(board); } const redoBoard = cloneJSON(board); delete redoBoard.prior; const newBoard = cloneJSON(board.prior); newBoard.subsequent = redoBoard; return newBoard; } function stepForward(board){ if (!board.subsequent) { return cloneJSON(board); } const undoBoard = cloneJSON(board); delete undoBoard.subsequent; const newBoard = cloneJSON(board.subsequent); newBoard.prior = undoBoard; return newBoard; } $('#cb_undo').on('click', function(){ putState(stepBack($('#cb_board').data('board'))); }); $('#cb_redo').on('click', function(){ const board = stepForward($('#cb_board').data('board')); board.timestamp = new Date().getTime(); putState(board); }); $('#cb_nav_first').on('click', function(){ let board = $('#cb_board').data('visible_board'); if (board) { while (board.prior) { board = stepBack(board); } setVisibleBoard(board); } }); $('#cb_nav_prev_turn').on('click', function(){ let board = $('#cb_board').data('visible_board'); board = stepBack(board); const player = board.player; while (board.prior && board.prior.player === player) { board = stepBack(board); } setVisibleBoard(board); }); $('#cb_nav_prev_state').on('click', function(){ setVisibleBoard(stepBack($('#cb_board').data('visible_board'))); }); $('#cb_nav_next_state').on('click', function(){ setVisibleBoard(stepForward($('#cb_board').data('visible_board'))); }); $('#cb_nav_next_turn').on('click', function(){ let board = $('#cb_board').data('visible_board'); const player = board.player; board = stepForward(board); while (board.subsequent && board.player === player) { board = stepForward(board); } setVisibleBoard(board); }); $('#cb_nav_last').on('click', function(){ const visible = cloneJSON($('#cb_board').data('board')); delete visible.subsequent; setVisibleBoard(visible); }); $('#cb_reset').on('click', function(){ putState(initialBoard()); }); $('#cb_pass').on('click', function(){ let board = $('#cb_board').data('board'); if (!board.phantom) { let newBoard = cloneJSON(board); newBoard.prior = board; newBoard.move = { side: board.player, pass: true }; newBoard.player = otherSide(board.player); newBoard.timestamp = new Date().getTime(); putState(newBoard); } }); $('#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_/, '')); } } }); $('#cb_notify').on('change', function(){ if ('localStorage' in window) { window.localStorage.setItem('/pacosako/notify', this.checked ? 'on' : 'off'); } if (this.checked) { requestNotify(); } }); let updateMeta = function() { putMeta(); } $('#cb_light_name').on('input', updateMeta); $('#cb_dark_name').on('input', updateMeta); let gameId = location.hash.replace(/^#\//, ''); if (gameId.length !== 16) { gameId = randomId(); } switchGameId(gameId); function updateTitle(opt){ opt = $(opt); const then = opt.data('then'); const now = new Date().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) { age_str = ' (' + Math.floor((now - then) / (24*60*60*1000)) + 'd)'; } else if (opt.data('gameId') !== $('#cb_board').data('gameId')) { opt.remove(); return; } opt.text(opt.data('title') + age_str); } gun.get(PacoSakoUUID).get('meta').map().on(function(d){ if (d && d.gameId) { const lightName = d.lightName ? d.lightName : 'Light'; const darkName = d.darkName ? d.darkName : 'Dark'; const moves = !d.moves ? '' : (', ' + d.moves + (d.moves === 1 ? ' move' : ' moves')); let opt = $('#cb_game_' + d.gameId); if (!(d.lightName || d.darkName) && !d.moves && d.gameId !== $('#cb_board').data('gameId')) { if (opt.length >= 1) { opt.remove(); } } else { if (opt.length === 0) { opt = $(''); opt.attr('id', 'cb_game_' + d.gameId); } let stat = ''; if (d.status) { stat = ', ' + d.status; } opt.data('gameId', d.gameId); opt.data('title', lightName + ' vs. ' + darkName + moves + stat); opt.data('then', d.timestamp || new Date().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; } } }); 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){ let gameId = location.hash.replace(/^#\//, ''); if (gameId.length === 16) { switchGameId(gameId); } }; /* force page reload after four hours to keep client code up to date */ window.setTimeout(function(){ location.reload(); }, 4*3600*1000); }); /* vim:set expandtab sw=3 ts=8: */