From a96e8f1e4ffb049633714669204f8e3549d27524 Mon Sep 17 00:00:00 2001 From: Jesse McDonald Date: Sun, 22 Mar 2020 01:38:34 -0500 Subject: [PATCH] major rewrite; breaking change; game logic in separate file --- index.html | 24 +- js/chess.js | 1171 --------------------------------------------- js/pacosako.js | 804 +++++++++++++++++++++++++++++++ js/pacosako_ui.js | 757 +++++++++++++++++++++++++++++ js/todo.js | 56 --- 5 files changed, 1563 insertions(+), 1249 deletions(-) delete mode 100644 js/chess.js create mode 100644 js/pacosako.js create mode 100644 js/pacosako_ui.js delete mode 100644 js/todo.js diff --git a/index.html b/index.html index 9b43308..28a1ece 100644 --- a/index.html +++ b/index.html @@ -245,26 +245,6 @@
pl
- -
-
- - - - - -
- - -
@@ -283,8 +263,8 @@ console.log = console.real_log; - - + + diff --git a/js/chess.js b/js/chess.js deleted file mode 100644 index 764e336..0000000 --- a/js/chess.js +++ /dev/null @@ -1,1171 +0,0 @@ -'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', - }); - return function (){ - const board = JSON.parse(init); - board.timestamp = new Date(Gun.state()).getTime(); - return board; - }; -})(); - -function cloneJSON(obj){ - return JSON.parse(JSON.stringify(obj)); -} - -function isLight(side){ - return side === 'l' || side === 'light'; -} - -function isDark(side){ - return side === 'd' || side === 'dark'; -} - -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 gameEnded(board){ - if (!board.move) { - return false; - } else if (board.move.took === 'k') { - return board.move.side; - } else if (board.move.resign) { - return otherSide(board.move.side); - } else { - return false; - } -} - -function legalMoves(board, side, type, from, canCapture){ - if (gameEnded(board)) { - 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(Gun.state()).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.resign) { - break; - } - - 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 += '#'; - } - } - - let winner = gameEnded(currentBoard); - if (winner) { - if (isLight(winner)) { - result += ' 1-0'; - } else if (isDark(winner)) { - result += ' 0-1'; - } else { - result += ' \u00bd-\u00bd'; /* 1/2-1/2 */ - } - } - - 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 = ''; - let winner = gameEnded(board); - if (winner) { - if (board.move.resign) { - msg += (isLight(board.move.side) ? 'Light' : 'Dark') + ' player resigned. '; - } - msg += (isLight(winner) ? 'Light' : 'Dark') + ' player won!'; - } else { - msg += (isLight(board.player) ? 'Light' : 'Dark') + " 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); - if (gameEnded(liveBoard)) { - $('#cb_pass').attr('disabled', true); - $('#cb_resign').attr('disabled', true); - } else { - $('#cb_pass').attr('disabled', liveBoard.phantom ? true : false); - $('#cb_resign').attr('disabled', 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_resign').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 winner = gameEnded(board); - let stat = !winner ? null : (board.move.took === 'k') ? 'mate' : 'ended'; - meta.put({ - gameId: gameId, - lightName: lightName, - darkName: darkName, - moves: countMoves(board), - timestamp: board.timestamp || new Date(Gun.state()).getTime(), - status: stat, - }); -} - -function switchGameId(newId){ - let boardElem = $('#cb_board'); - let gameId = boardElem.data('gameId'); - - if (newId === gameId) { - return; - } - - boardElem.data('gameId', newId); - location.hash = '#/' + newId; - - if (gameId) { - //gun.get(PacoSakoUUID).get('games').get(gameId).off(); - //gun.get(PacoSakoUUID).get('meta').get(gameId).off(); - location.reload(); - return; - } - - 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(Gun.state()).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 (!gameEnded(board) && !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(Gun.state()).getTime(); - putState(newBoard); - } - }); - - $('#cb_resign').on('click', function(){ - let board = $('#cb_board').data('board'); - if (!gameEnded(board)) { - let newBoard = cloneJSON(board); - newBoard.prior = board; - newBoard.move = { side: board.player, resign: true }; - newBoard.player = otherSide(board.player); - newBoard.timestamp = new Date(Gun.state()).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(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) { - 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(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; - } - } - }); - - 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: */ diff --git a/js/pacosako.js b/js/pacosako.js new file mode 100644 index 0000000..a5014d0 --- /dev/null +++ b/js/pacosako.js @@ -0,0 +1,804 @@ +'use strict'; +(function(factory) { + if (typeof define !== 'undefined' && define.amd) { + define([], factory); + } else if (typeof define !== 'undefined' && define.petal) { + define(['paco-sako'], [], factory); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + } else { + window.PacoSako = factory(); + } +})(function(){ + +/* Game states */ +const PLAYING = 'playing'; +const ENDED = 'ended'; + +/* Sides */ +const LIGHT = 'light'; +const DARK = 'dark'; + +/* Pieces */ +const KING = 'k'; +const QUEEN = 'q'; +const ROOK = 'r'; +const KNIGHT = 'n'; +const BISHOP = 'b'; +const PAWN = 'p'; +const EMPTY = ' '; + +const ALL_PIECES = EMPTY + PAWN + BISHOP + KNIGHT + ROOK + QUEEN + KING; + +/* Special squares */ +const PHANTOM = 'phantom'; + +/* a1...h1 and a8...h8 */ +const INITIAL_EDGE_ROW = ROOK + KNIGHT + BISHOP + QUEEN + KING + BISHOP + KNIGHT + ROOK; + +const ROWS = '12345678'; +const COLUMNS = 'abcdefgh'; + +function otherSide(side) { + if (side === LIGHT) { + return DARK; + } else if (side === DARK) { + return LIGHT; + } else { + throw { message: 'invalid side', side: side }; + } +} + +function squareIndex(square) { + if (typeof square !== 'string' || square.length !== 2) { + throw { message: 'invalid square', square: square }; + } + + const column = COLUMNS.indexOf(square[0].toLowerCase()); + const row = ROWS.indexOf(square[1]); + + if (column < 0 || row < 0) { + throw { message: 'invalid square', square: square }; + } + + return (row * 8) + column; +} + +function offsetSquare(from, columnsRight, rowsUp) { + let index = squareIndex(from); + let column = (index & 0b111) + columnsRight; + let row = (index >>> 3) + rowsUp; + + if (column >= 0 && column < 8 && row >= 0 && row < 8) { + return COLUMNS[column] + ROWS[row]; + } else { + return null; + } +} + +function encodePiece(side, type) { + let result = ALL_PIECES.indexOf(type); + + if (result < 0) { + throw { message: 'invalid piece', piece: type }; + } + + if (side === LIGHT) { + return (result << 4); + } else if (side === DARK) { + return result; + } else { + throw { message: 'invalid side', side: side }; + } +} + +function decodePiece(side, value) { + let sideShift = null; + + if (side === LIGHT) { + sideShift = 4; + } else if (side === DARK) { + sideShift = 0; + } else { + throw { message: 'invalid side', side: side }; + } + + let pieceValue = (value >>> sideShift) & 0b1111; + + if (pieceValue < ALL_PIECES.length) { + return ALL_PIECES[pieceValue]; + } else { + throw { message: 'invalid encoded piece', value: pieceValue }; + } +} + +class Board { + constructor(original) { + if (original !== null && original !== undefined) { + if (!(original instanceof this.constructor)) { + throw { message: 'can only clone from another Board instance' }; + } + + this._board = new Uint8Array(original._board); + this._phantom = JSON.parse(JSON.stringify(original._phantom)); + } else { + this._board = new Uint8Array(64); + this._board.fill(0); + this._phantom = null; + + for (let column = 0; column < 8; ++column) { + const letter = COLUMNS[column]; + this.putPiece(DARK, letter + '8', INITIAL_EDGE_ROW[column]); + this.putPiece(DARK, letter + '7', PAWN); + this.putPiece(LIGHT, letter + '2', PAWN); + this.putPiece(LIGHT, letter + '1', INITIAL_EDGE_ROW[column]); + } + } + } + + get phantom() { + let phantom = this._phantom; + if (phantom) { + return { side: phantom.side, type: phantom.type, from: phantom.from }; + } else { + return null; + } + } + + getPiece(side, square) { + if (square === PHANTOM) { + if (this._phantom && this._phantom.side === side) { + return this._phantom.type; + } else { + return EMPTY; + } + } + + return decodePiece(side, this._board[squareIndex(square)]); + } + + isEmpty(square) { + if (square === PHANTOM) { + return this._phantom === null; + } else { + return this._board[squareIndex(square)] === 0; + } + } + + putPiece(side, square, piece) { + const other = otherSide(side); + const otherPiece = this.getPiece(other, square); + const together = encodePiece(side, piece) | encodePiece(other, otherPiece); + this._board[squareIndex(square)] = together; + } + + makePhantom(side, square) { + const type = this.getPiece(side, square); + + if (type !== EMPTY) { + if (this._phantom) { + throw { message: 'phantom square is already occupied' }; + } + + this.putPiece(side, square, EMPTY); + + this._phantom = { + side: side, + type: type, + from: square, + }; + } + } + + move(from, to) { + if (this._phantom && from !== PHANTOM) { + throw { message: 'must complete prior move before moving other pieces' }; + } + + let lightPiece = this.getPiece(LIGHT, from); + let darkPiece = this.getPiece(DARK, from); + + if (lightPiece === EMPTY && darkPiece === EMPTY) { + throw { message: 'cannot move from empty square' }; + } else if (lightPiece !== EMPTY && darkPiece !== EMPTY) { + if (!this.isEmpty(to)) { + throw { message: 'cannot capture with joined pieces' }; + } + + const fromIndex = squareIndex(from); + const toIndex = squareIndex(to); + this._board[toIndex] = this._board[fromIndex]; + this._board[fromIndex] = 0; + } else { + const moving = (lightPiece === EMPTY) ? DARK : LIGHT; + const movingPiece = (lightPiece === EMPTY) ? darkPiece : lightPiece; + const other = (lightPiece === EMPTY) ? LIGHT : DARK; + const displaced = this.getPiece(moving, to); + + if (displaced !== EMPTY && this.getPiece(other, to) === EMPTY) { + throw { message: 'cannot join with piece of same color' }; + } + + if (from === PHANTOM) { + this._phantom = null; + } else { + this.putPiece(moving, from, EMPTY); + } + + this.makePhantom(moving, to); + this.putPiece(moving, to, movingPiece); + } + } + + validDestination(side, where, canCapture) { + let ours = this.getPiece(side, where); + let theirs = this.getPiece(otherSide(side), where); + return ((theirs === EMPTY) ? (ours === EMPTY) : (canCapture ? true : false)); + } + + scanPath(accum, side, from, canCapture, columnsRight, rowsUp, remainder) { + while (true) { + let there = offsetSquare(from, columnsRight, rowsUp); + + if (!there || !this.validDestination(side, there, canCapture)) { + return; + } + + accum.push(there); + + if (remainder < 1 || this.getPiece(otherSide(side), there) !== EMPTY) { + return; + } + + from = there; + remainder -= 1; + } + } + + findPieces(side, type, alongside) { + const other = otherSide(side); + let pieces = []; + + if (this._phantom && this._phantom.side === side && this._phantom.type === type) { + pieces.push(this._phantom.from); + } + + for (const row of ROWS) { + for (const column of COLUMNS) { + const here = column + row; + if (this.getPiece(side, here) === type) { + if (!alongside || this.getPiece(other, here) === alongside) { + pieces.push(here); + } + } + } + } + + return pieces; + } +} + +const ORTHO = [[-1, 0], [1, 0], [0, -1], [0, 1]]; +const DIAG = [[-1, -1], [-1, 1], [1, -1], [1, 1]]; +const ANY_DIR = ORTHO.concat(DIAG); + +const KNIGHT_DIR = + [[-1, 2], [ 1, 2], + [-2, 1], [ 2, 1], + [-2, -1], [ 2, -1], + [-1, -2], [ 1, -2]]; + +const NBSP = '\u00a0'; /* non-breaking space */ +const SHY = '\u00ad' /* soft hyphen */ +const ZWSP = '\u200b'; /* zero-width space */ + +class Game { + constructor(original) { + if (original !== undefined) { + if (!(original instanceof this.constructor)) { + throw { message: 'can only clone from another Game instance' }; + } + + this._board = new Board(original._board); + this._player = original._player; + this._status = original._status; + this._moves = JSON.parse(JSON.stringify(original._moves)); + this._redo = JSON.parse(JSON.stringify(original._redo)); + this._castling = JSON.parse(JSON.stringify(original._castling)); + } else { + this._board = new Board(); + this._player = LIGHT; + this._status = PLAYING; + this._moves = []; + this._redo = []; + + /* set to false when the king or rook moves */ + this._castling = {}; + this._castling[LIGHT] = { king: true, queen: true }; + this._castling[DARK] = { king: true, queen: true }; + } + } + + get board() { + return new Board(this._board); + } + + get player() { + return this._player; + } + + get status() { + return this._status; + } + + get moves() { + return JSON.parse(JSON.stringify(this._moves)); + } + + get redoMoves() { + return JSON.parse(JSON.stringify(this._redo)); + } + + legalMoves(side, from, canCapture) { + const board = this._board; + const type = board.getPiece(side, from); + + if (this._status !== PLAYING || type === EMPTY) { + return []; + } + + if (from === PHANTOM) { + /* this is valid because board.getPiece(side, PHANTOM) !== EMPTY */ + from = board.phantom.from; + } + + let legals = []; + + if (type === KING) { + for (const dir of ANY_DIR) { + board.scanPath(legals, side, from, false, dir[0], dir[1], 0); + } + + /* check for castling conditions */ + if (from === (side === DARK ? 'e8' : 'e1')) { + const row = from[1]; + + if (this._castling[side].king && + board.isEmpty('f' + row) && + board.isEmpty('g' + row)) { + legals.push('g' + row); + } + + if (this._castling[side].queen && + board.isEmpty('d' + row) && + board.isEmpty('c' + row) && + board.isEmpty('b' + row)) { + legals.push('c' + row); + } + } + } else if (type === QUEEN) { + for (const dir of ANY_DIR) { + board.scanPath(legals, side, from, canCapture, dir[0], dir[1], 8); + } + } else if (type === BISHOP) { + for (const dir of DIAG) { + board.scanPath(legals, side, from, canCapture, dir[0], dir[1], 8); + } + } else if (type === KNIGHT) { + for (const dir of KNIGHT_DIR) { + const there = offsetSquare(from, dir[0], dir[1]); + if (there && board.validDestination(side, there, canCapture)) { + legals.push(there); + } + } + } else if (type === ROOK) { + for (const dir of ORTHO) { + board.scanPath(legals, side, from, canCapture, dir[0], dir[1], 8); + } + } else if (type === PAWN) { + const dark = side === DARK; + 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 && board.validDestination(side, forward, false)) { + legals.push(forward); + if (dark ? (from[1] >= '7') : (from[1] <= '2')) { + if (forward2 && board.validDestination(side, forward2, false)) { + legals.push(forward2); + } + } + } + + if (canCapture) { + for (const there of [diagL, diagR]) { + if (there) { + if (board.getPiece(otherSide(side), there) !== EMPTY) { + legals.push(there); + } else if (forward2) { + let lastMove = null; + for (let i = this._moves.length; i > 0; --i) { + const move = this._moves[i - 1]; + if (move.side !== this._player) { + lastMove = move; + break; + } + } + + if (lastMove && lastMove.type === PAWN && + lastMove.from === there[0] + forward2[1] && + lastMove.to === there[0] + from[1]) { + /* en passant */ + legals.push(there); + } + } + } + } + } + } + + return legals; + } + + move(from, to, meta) { + if (this._status !== PLAYING) { + throw { message: "can't move, game is already over" }; + } + + const side = this._player; + const other = otherSide(side); + const board = this._board; + const type = board.getPiece(side, from); + const took = board.getPiece(other, to); + const replaced = board.getPiece(side, to); + const alongside = board.getPiece(other, from); + + if (from === PHANTOM && !board.phantom) { + throw { message: "attempted to continue a completed move" }; + } + + const fromSquare = (from === PHANTOM) ? board.phantom.from : from; + + const legals = this.legalMoves(side, from, alongside === EMPTY); + if (!legals.includes(to)) { + throw { message: "illegal move", side: side, from: fromSquare, to: to }; + } + + const move = { + side: side, + type: type, + from: fromSquare, + to: to, + }; + + if (from === PHANTOM) { + move.phantom = true; + } + + if (took !== EMPTY) { + move.took = took; + } + + if (replaced !== EMPTY) { + move.replaced = replaced; + } + + if (alongside !== EMPTY) { + move.alongside = alongside; + } + + if (meta !== undefined) { + move.meta = JSON.parse(JSON.stringify(meta)); + } + + board.move(from, to); + + if (type === KING) { + /* we already checked that this is a legal move, so it must be castling */ + if (from[0] === 'e' && to[0] === 'g') { + /* move the rook & any paired piece */ + board.move('h' + from[1], 'f' + from[1]); + move.castle = true; + } + else if (from[0] === 'e' && to[0] === 'c') { + /* move the rook & any paired piece */ + board.move('a' + from[1], 'd' + from[1]); + move.queen_castle = true; + } + + /* can't castle after moving the king */ + this._castling[side].king = false; + this._castling[side].queen = false; + } else if (type === ROOK) { + /* can't castle after moving the rook */ + if (from[0] === 'h') { + this._castling[side].king = false; + } else if (from[0] === 'a') { + this._castling[side].queen = false; + } + } else if (type === PAWN) { + if (fromSquare[0] !== to[0] && took === EMPTY) { + /* legal diagonal move but nothing at destination; must be en passant */ + move.en_passant = true; + move.took = PAWN; + + /* the location of the captured pawn */ + const where = to[0] + fromSquare[1]; + + /* move the opponent's pawn back one space to join our pawn */ + board.putPiece(other, where, EMPTY); + board.putPiece(other, to, PAWN); + + /* see if we replaced a piece from the other square */ + board.makePhantom(side, where); + } + + if (to[1] === (side === DARK ? '1' : '8')) { + move.promotion = QUEEN; /* TODO: allow other choices */ + board.putPiece(side, to, move.promotion); + } + } + + if (alongside === PAWN && to[1] === (other === DARK ? '1' : '8')) { + move.promotion = QUEEN; /* TODO: allow other choices */ + board.putPiece(other, to, move.promotion); + } + + if (board.phantom === null) { + this._player = otherSide(this._player); + } + + if (took === KING) { + this._status = ENDED; + } + + this._moves.push(move); + this._redo = []; + } + + resign(meta) { + if (this._status !== PLAYING) { + throw { message: "can't resign, game is already over" }; + } + + const move = { + side: this._player, + resign: true + }; + + if (meta !== undefined) { + move.meta = meta; + } + + this._status = ENDED; + this._moves.push(move); + this._redo = []; + } + + get lastMove() { + if (this._moves.length > 0) { + return JSON.parse(JSON.stringify(this._moves[this._moves.length - 1])); + } else { + return undefined; + } + } + + get winner() { + const move = this.lastMove; + if (move && move.resign) { + return otherSide(this.lastMove.side); + } else if (move && move.took === KING) { + return move.side; + } else { + return null; + } + } + + replayMove(move) { + if (!move || move.side !== this._player) { + throw { message: "other player's move", move: move }; + } + + if (move.resign) { + this.resign(); + } else if (move.phantom) { + this.move(PHANTOM, move.to, move.meta); + } else { + this.move(move.from, move.to, move.meta); + } + } + + get canUndo() { + return this._moves.length > 0; + } + + get canRedo() { + return this._redo.length > 0; + } + + undo() { + if (this.canUndo) { + /* preserve the last move and redo history */ + const lastMove = this._moves[this._moves.length - 1]; + const savedRedo = this._redo; + + /* replay all moves except the last in a new game object */ + const replay = new this.constructor(); + for (let i = 0; i < this._moves.length - 1; ++i) { + replay.replayMove(this._moves[i]); + } + + /* copy all the properties from the replayed game to this one */ + for (const prop in replay) { + this[prop] = replay[prop]; + } + + /* restore the original redo history and add the undone move */ + this._redo = savedRedo; + this._redo.push(lastMove); + } + } + + redo() { + if (this.canRedo) { + const savedRedo = this._redo; + this.replayMove(savedRedo.pop()); + this._redo = savedRedo; + } + } + + clearRedo() { + this._redo = []; + } + + countTurns() { + let n = 0; + let player = null; + + for (const move of this._moves) { + /* multiple consecutive moves by the same player are a single turn */ + if (move.side !== player) { + ++n; + } + + player = move.side; + } + + return n; + } + + renderHistory() { + let replay = new Game(); + let result = ''; + let n = 0; + + for (const move of this._moves) { + if (move.phantom) { + result += SHY + '*'; + } else { + if (n > 0 || move.side === DARK) { + result += ' '; + } + + if (move.side === LIGHT) { + ++n; + result += String(n) + '.' + NBSP; + } + } + + if (move.castle) { + result += 'O-O'; + } else if (move.queen_castle) { + result += 'O-O-O'; + } else if (move.side && move.type && move.from && move.to) { + let piece = ''; + + if (move.alongside) { + if (move.side === LIGHT) { + piece = move.type.toUpperCase() + move.alongside.toUpperCase(); + } else { + piece = move.alongside.toUpperCase() + move.type.toUpperCase(); + } + } else { + piece = move.type === PAWN ? '' : move.type.toUpperCase(); + } + + /* the second condition below is for en passant of a joined piece */ + if (!move.phantom || move.from !== replay.lastMove.to) { + const sameKind = replay.board.findPieces(move.side, move.type, move.alongside || EMPTY); + const legalFrom = []; + let sameFile = 0; /* column / letter */ + let sameRank = 0; /* row / number */ + + for (const where of sameKind) { + if (replay.legalMoves(move.side, where, true).includes(move.to)) { + legalFrom.push(where); + + if (where[0] === move.from[0]) { + sameFile += 1; + } + + if (where[1] === move.from[1]) { + sameRank += 1; + } + } + } + + /* always disambiguate captures by pawns (standard convention) */ + if (legalFrom.length !== 1 || (move.type === PAWN && move.took)) { + /* append file, rank, or both to disambiguate */ + if (sameFile === 1) { + piece += move.from[0]; + } else if (sameRank === 1) { + piece += move.from[1]; + } else { + piece += move.from; + } + } + } + + const took = move.took ? 'x' : ''; + result += piece + took + move.to; + } + + if (move.en_passant) { + result += 'e.p.'; + } + + if (move.promotion) { + result += '(' + move.promotion.toUpperCase() + ')'; + } + + if (move.took === KING) { + result += '#'; + } + + replay.replayMove(move); + } + + let winner = replay.winner; + + if (winner === LIGHT) { + result += ' 1-0'; + } else if (winner === DARK) { + result += ' 0-1'; + } else if (replay.status !== PLAYING) { + result += ' \u00bd-\u00bd'; /* 1/2-1/2 */ + } + + return result; + } +} + +return { + Board: Board, + Game: Game, + + PLAYING: PLAYING, + ENDED: ENDED, + + LIGHT: LIGHT, + DARK: DARK, + + KING: KING, + QUEEN: QUEEN, + ROOK: ROOK, + KNIGHT: KNIGHT, + BISHOP: BISHOP, + PAWN: PAWN, + EMPTY: EMPTY, + + ROWS: ROWS, + COLUMNS: COLUMNS, + PHANTOM: PHANTOM, + + Util: { + otherSide: otherSide, + offsetSquare: offsetSquare, + }, +}; + +}); + +/* vim:set expandtab sw=3 ts=8: */ diff --git a/js/pacosako_ui.js b/js/pacosako_ui.js new file mode 100644 index 0000000..45ee75e --- /dev/null +++ b/js/pacosako_ui.js @@ -0,0 +1,757 @@ +'use strict'; +$(function (){ + let gun = Gun({ + peers: ['https://jessemcdonald.info/gun'], + /* workaround for persistent errors accumulating which break synchronization */ + //localStorage: false, + }); + + const PS = window.PacoSako; + let currentGame = new PS.Game(); + let visibleGame = new PS.Game(currentGame); + let cancelGameCallback = function() {}; + let cancelMetaCallback = function() {}; + + /* for debug */ + window.getCurrentGame = function() { return currentGame; } + window.getVisibleGame = function() { return visibleGame; } + function debug() { console.log.apply(console, arguments); } + + 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 { + return $('#cb_' + where).first() + } + } + + function pieceStartDrag(ev, ui) { + const dragged = $(this); + const side = dragged.data('side'); + const type = dragged.data('type'); + const from = dragged.data('location'); + const board = currentGame.board; + const where = (from === PS.PHANTOM) ? board.phantom.from : from; + const canCapture = (from === PS.PHANTOM) || + board.getPiece(PS.Util.otherSide(side), where) === PS.EMPTY; + const legals = currentGame.legalMoves(side, from, canCapture); + for (const there of legals) { + cbSquare(there).addClass('cb-legal').droppable('enable'); + } + } + + function pieceStopDrag(ev, ui) { + $('#cb_board .cb-legal').removeClass('cb-legal').droppable('disable'); + } + + function placePiece(where, side, type, suffix) { + const code = pieceCode(side, type); + const piece_id = 'cb_piece_' + code + '_' + suffix; + const piece = $($('#' + piece_id)[0] || $('#cb_piece_' + code + ' img').clone()); + piece.attr('style', ''); + piece.attr('id', piece_id); + 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() { + $('#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'); + + 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])); + } + } + } + } + + let clss = game.player === PS.LIGHT ? '.cb-lt-piece' : '.cb-dk-piece'; + + if (board.phantom) { + let where = board.phantom.from; + placePiece(PS.PHANTOM, board.phantom.side, board.phantom.type, 'ph'); + cbSquare(PS.PHANTOM).appendTo(cbSquare(where)); + $('#cb_phantom .ui-draggable-disabled').filter(clss).draggable('enable'); + } else if (game.status === PS.PLAYING) { + $('#cb_board .ui-draggable-disabled').filter(clss).draggable('enable'); + } + + const lastMove = game.lastMove; + if (lastMove) { + if (lastMove.from) { + cbSquare(lastMove.from).addClass('cb-start'); + } + + if (lastMove.to) { + cbSquare(lastMove.to).addClass('cb-end'); + } + } + + let msg = ''; + let winner = game.winner; + 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 (game.status === PS.PLAYING) { + 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.renderHistory()); + + $('#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); + + if (!game.canRedo) { + $('#cb_undo').attr('disabled', !currentGame.canUndo); + $('#cb_redo').attr('disabled', !currentGame.canRedo); + if (currentGame.status === PS.PLAYING) { + $('#cb_pass').attr('disabled', currentGame.board.phantom ? true : false); + } else { + $('#cb_pass').attr('disabled', true); + } + $('#cb_resign').attr('disabled', currentGame.status !== PS.PLAYING); + $('#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_resign').attr('disabled', true); + $('#cb_board .ui-draggable').draggable('disable'); + $('#cb_board').removeClass('cb-live'); + $('#cb_board').addClass('cb-archive'); + } + } + + function setCurrentBoard(game) { + currentGame = game; + + /* navigation should not include the redo stack */ + visibleGame = new PS.Game(game); + visibleGame.clearRedo(); + renderBoard(); + + const moves = game.moves; + + const cb_board = $('#cb_board').first(); + if ($('#cb_notify')[0].checked) { + if (!deepEqual(moves, cb_board.data('last_state'))) { + /* ignore partial moves */ + if (!game.board.phantom) { + const gameString = cb_board.data('lightName') + ' vs. ' + cb_board.data('darkName'); + notify(gameString + '\n' + $('#cb_message').text()); + } + } + } + + 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; + } + + //const PacoSakoUUID = '7c38edd4-c931-49c8-9f1a-84de560815db'; /* V1 release */ + const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 release */ + + 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).get('games').get(gameId).put({ board: 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 winner = currentGame.winner; + const lastMove = currentGame.lastMove || {}; + const lastMeta = lastMove.meta || {}; + const status = !winner ? null : (lastMove.took === PS.KING) ? 'mate' : 'ended'; + gun.get(PacoSakoUUID).get('meta').get(gameId).put({ + lightName: lightName, + darkName: darkName, + moves: currentGame.countTurns(), + timestamp: lastMeta.timestamp || new Date(Gun.state()).getTime(), + status: status, + }); + } + + 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; + + if (gameId) { + //gun.get(PacoSakoUUID).get('games').get(gameId).off(); + //gun.get(PacoSakoUUID).get('meta').get(gameId).off(); + + location.reload(); + return; + } + + 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 */ + setCurrentBoard(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(''); + + (function(){ + let callback = function(d) { + if (d && d.board) { + try { + const moves = JSON.parse(d.board); + 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(); + } + + setCurrentBoard(newGame); + } catch (err) { + debug('Error replaying board state', err); + } + } + }; + + cancelGameCallback = function() { callback = function() {}; }; + + gun.get(PacoSakoUUID).get('games').get(newId).on(function(d){ + callback(d); + }); + })(); + + (function(){ + let callback = function(d) { + d = d || {}; + debug('got meta', d); + $('#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 || ''); + }; + + cancelMetaCallback = function() { callback = function() {}; }; + + gun.get(PacoSakoUUID).get('meta').get(newId).on(function(d){ + callback(d); + }); + })(); + + 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) {} + } + + 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 from = dragged.data('location'); + let to = this.id.replace(/^cb_/, ''); + dragged.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); + setCurrentBoard(currentGame); + putState(); + }, + }); + + $('#cb_undo').on('click', function(){ + if (currentGame.canUndo) { + currentGame.undo(); + $('#cb_board').data('last_state', currentGame.moves); + setCurrentBoard(currentGame); + putState(); + } + }); + + $('#cb_redo').on('click', function(){ + if (currentGame.canRedo) { + currentGame.redo(); + $('#cb_board').data('last_state', currentGame.moves); + setCurrentBoard(currentGame); + 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); + setCurrentBoard(currentGame); + putState(); + }); + + $('#cb_nav_first').on('click', function(){ + while (visibleGame.canUndo) { + visibleGame.undo(); + } + renderBoard(); + }); + + $('#cb_nav_prev_turn').on('click', function(){ + 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(); + }); + + $('#cb_nav_next_turn').on('click', function(){ + const player = visibleGame.player; + while (visibleGame.canRedo) { + visibleGame.redo(); + if (visibleGame.player !== player) { + break; + } + } + renderBoard(); + }); + + $('#cb_nav_last').on('click', function(){ + while (visibleGame.canRedo) { + visibleGame.redo(); + } + renderBoard(); + }); + + $('#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); + + 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) { + 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,key){ + if (key.match(/^[0-9a-f]{16}$/) && d) { + debug('got meta for key ' + key, d); + 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_' + 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; + } + } + }); + + 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)/); + switchGameId(foundId ? foundId[1] : randomId()); + + /* Low-level commands to be run from the JS console */ + window.Admin = { + convertFromV1: function() { + const PacoSakoUUIDv1 = '7c38edd4-c931-49c8-9f1a-84de560815db'; + gun.get(PacoSakoUUIDv1).get('games').map().once(function(d,key){ + if (d && d.board) { + debug('converting ' + key); + const game = new PS.Game(); + let moves = []; + + try { + let board = JSON.parse(d.board); + + while (board.prior) { + moves.push(board.move); + board = board.prior; + } + moves.reverse(); + + for (const move of moves) { + if (move.to) { + game.move(move.from, move.to, move.timestamp && { timestamp: move.timestamp }); + } else if (move.resign) { + game.resign(); + } else { + throw { message: "unknown move", move: move }; + } + } + + gun.get(PacoSakoUUID).get('games').get(key).put({ board: JSON.stringify({ + past: game.moves, + future: [], + })}); + } catch (err) { + debug('conversion of ' + key + ' failed', err, game, moves); + } + } + }); + + gun.get(PacoSakoUUIDv1).get('meta').map().once(function(d,key){ + if (d) { + debug('converting metadata for ' + key); + gun.get(PacoSakoUUID).get('meta').get(key).put({ + lightName: d.lightName, + darkName: d.darkName, + moves: d.moves, + timestamp: d.timestamp, + status: d.status, + }); + } + }); + }, + + cleanupMissingGames: function() { + gun.get(PacoSakoUUID).get('games').once(function(games){ + gun.get(PacoSakoUUID).get('meta').map().once(function(d,key){ + if (!(key in games)) { + gun.get(PacoSakoUUID).get('meta').get(key).put(null); + } + }); + }); + }, + }; +}); + +/* vim:set expandtab sw=3 ts=8: */ diff --git a/js/todo.js b/js/todo.js deleted file mode 100644 index eb309be..0000000 --- a/js/todo.js +++ /dev/null @@ -1,56 +0,0 @@ -var user = gun.user(); - -function UI(say, id){ - var li = $('#' + id).get(0) || $('
  • ').attr('id', id).appendTo('ul'); - $(li).text(say); -}; - -function auth(alias, pass){ - $('#message').text('Looking up alias "' + alias + '"...').show(); - gun.get('~@' + alias).once(function(){ - $('#message').text('Signing in as "' + alias + '"...').show(); - user.auth(alias, pass, function (ack){ - if (ack.err) { - $('#message').text(ack.err).show(); - } else { - $('#message').hide(); - $('#pass').val(''); - $('#sign').hide(); - $('#todo').show(); - user.get('said').map().once(UI); - } - }); - }); -}; - -$('#up').on('click', function(e){ - var alias = $('#alias').val(); - var pass = $('#pass').val(); - $('#message').text('Creating alias "' + alias + '"...').show(); - user.create(alias, pass, function (ack){ - if (ack.err) { - $('#message').text(ack.err).show(); - } else { - auth(alias, pass); - } - }); -}); - -$('#sign').on('submit', function(e){ - e.preventDefault(); - auth($('#alias').val(), $('#pass').val()); -}); - -$('#said').on('submit', function(e){ - e.preventDefault(); - if(!user.is){ return } - user.get('said').set($('#say').val()); - $('#say').val(''); -}); - -$('#sign_out').on('click', function (){ - $('ul').empty(); - user.leave(); - $('#sign').show(); - $('#todo').hide(); -});