add "private copy" feature for client-only exploration of moves

This commit is contained in:
Jesse D. McDonald 2020-05-14 23:38:49 -05:00
parent 6dd6c2e3d6
commit 6495c49022
5 changed files with 147 additions and 49 deletions

View File

@ -179,6 +179,7 @@ button#settings, button#cb_choose_game {
#cb_message { #cb_message {
height: 1.2em; height: 1.2em;
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
flex: 1 1 auto;
} }
#cb_explain_check { #cb_explain_check {
@ -478,6 +479,39 @@ button#settings, button#cb_choose_game {
z-index: 1; z-index: 1;
} }
.cb-private .cb-hide-if-private {
display: none !important;
}
.cb-show-if-private {
display: none !important;
}
.cb-private .cb-show-if-private {
display: initial !important;
}
div.hbox {
display: flex;
flex-flow: row nowrap;
justify-content: stretch;
align-items: stretch;
}
.private-link {
flex: 0 1 auto;
margin: 0.1em;
color: black;
}
.private-link:visited {
color: black;
}
.private-link:hover {
color: blue;
}
.jBox-title h2 { .jBox-title h2 {
margin: 0; margin: 0;
} }

View File

@ -17,10 +17,10 @@
<div id="board_ui"> <div id="board_ui">
<div id="header"> <div id="header">
<button id="settings" title="Settings" type="button"><span class="fas fa-cog"></span></button> <button id="settings" title="Settings" type="button"><span class="fas fa-cog"></span></button>
<button id="cb_choose_game" title="Choose Game" type="button"><span class="fas fa-list"></span></button> <button id="cb_choose_game" title="Choose Game" type="button" class="cb-hide-if-private"><span class="fas fa-list"></span></button>
<h1>Paco Ŝako</h1> <h1>Paco Ŝako<span class="cb-show-if-private" style="display: none"> - Private Mode</span></h1>
<div class="checkbox-container"><input id="cb_reverse" title="Reverse Board" type="checkbox" autocomplete="off" class="image-checkbox fas fa-sync"></div> <div class="checkbox-container"><input id="cb_reverse" title="Reverse Board" type="checkbox" autocomplete="off" class="image-checkbox fas fa-sync"></div>
<div class="checkbox-container"><input id="cb_notify" title="Notify" type="checkbox" autocomplete="off" class="image-checkbox fas fa-bell-slash"></div> <div class="checkbox-container cb-hide-if-private"><input id="cb_notify" title="Notify" type="checkbox" autocomplete="off" class="image-checkbox fas fa-bell-slash"></div>
<button id="help" title="Help" type="button"><span class="fas fa-question-circle"></span></button> <button id="help" title="Help" type="button"><span class="fas fa-question-circle"></span></button>
</div> </div>
@ -149,7 +149,10 @@
</table> </table>
</div> </div>
<div id="cb_status"> <div id="cb_status">
<div id="cb_message"></div> <div class="hbox">
<div id="cb_message"></div>
<div class="cb-hide-if-private"><a class="private-link" target="_blank" title="Open Private Copy"><span class="fas fa-copy"></span></a></div>
</div>
<div id="cb_explain_check"></div> <div id="cb_explain_check"></div>
<div id="cb_scrollable"> <div id="cb_scrollable">
<div id="cb_history"> <div id="cb_history">
@ -157,7 +160,7 @@
</div> </div>
</div> </div>
</div> </div>
<div id="cb_names"> <div id="cb_names" class="cb-hide-if-private">
<div id="cb_names_text"> <div id="cb_names_text">
<input id="cb_light_name" autocomplete="off" placeholder="Light"> <input id="cb_light_name" autocomplete="off" placeholder="Light">
<span class="cb-names-vs">vs.</span> <span class="cb-names-vs">vs.</span>
@ -172,8 +175,8 @@
<div class="nav-spacer"></div> <div class="nav-spacer"></div>
<button id="cb_nav_first" title="View First Turn" type="button" disabled="true"><span class="fas fa-fast-backward"></span></button> <button id="cb_nav_first" title="View First Turn" type="button" disabled="true"><span class="fas fa-fast-backward"></span></button>
<button id="cb_nav_prev_turn" title="View Prior Turn" type="button" disabled="true"><span class="fas fa-backward"></span></button> <button id="cb_nav_prev_turn" title="View Prior Turn" type="button" disabled="true"><span class="fas fa-backward"></span></button>
<button id="cb_nav_prev_state" title="View Prior State" type="button" disabled="true"><span class="fas fa-play fa-flip-horizontal"></span></button> <button id="cb_nav_prev_state" title="View Prior Move" type="button" disabled="true"><span class="fas fa-play fa-flip-horizontal"></span></button>
<button id="cb_nav_next_state" title="View Next State" type="button" disabled="true"><span class="fas fa-play"></span></button> <button id="cb_nav_next_state" title="View Next Move" type="button" disabled="true"><span class="fas fa-play"></span></button>
<button id="cb_nav_next_turn" title="View Next Turn" type="button" disabled="true"><span class="fas fa-forward"></span></button> <button id="cb_nav_next_turn" title="View Next Turn" type="button" disabled="true"><span class="fas fa-forward"></span></button>
<button id="cb_nav_last" title="View Current Move" type="button" disabled="true"><span class="fas fa-fast-forward"></span></button> <button id="cb_nav_last" title="View Current Move" type="button" disabled="true"><span class="fas fa-fast-forward"></span></button>
</div> </div>

View File

@ -573,7 +573,30 @@ class Game {
return this._version; return this._version;
} }
toJSON() { toJSON(style) {
function shrinkMove(move) {
if (move.resign) {
return { side: move.side, resign: true };
} else if (move.phantom) {
return { side: move.side, phantom: true, to: move.to };
} else {
return { side: move.side, from: move.from, to: move.to };
}
}
if (style === 'minify') {
/* Just the fields that are used by the constructor to replay the game */
/* Omits metadata and extra annotation fields */
const state = { past: [], future: [], version: this._version };
for (const move of this._moves) {
state.past.push(shrinkMove(move));
}
for (const move of this._redo) {
state.future.push(shrinkMove(move));
}
return JSON.stringify(state);
}
return JSON.stringify({ return JSON.stringify({
past: this._moves, past: this._moves,
future: this._redo, future: this._redo,

View File

@ -23,6 +23,9 @@ import {Workbox, messageSW} from 'workbox-window';
import {ResizeSensor, ElementQueries} from 'css-element-queries'; import {ResizeSensor, ElementQueries} from 'css-element-queries';
ElementQueries.listen(); ElementQueries.listen();
import {Buffer} from 'buffer';
import pako from 'pako';
/* "Waterdrop" by Porphyr (freesound.org/people/Porphyr) / CC BY 3.0 (creativecommons.org/licenses/by/3.0) */ /* "Waterdrop" by Porphyr (freesound.org/people/Porphyr) / CC BY 3.0 (creativecommons.org/licenses/by/3.0) */
import Waterdrop from '../mp3/191678__porphyr__waterdrop.mp3'; import Waterdrop from '../mp3/191678__porphyr__waterdrop.mp3';
@ -408,6 +411,14 @@ $(function (){
function setCurrentGame(game, animate) { function setCurrentGame(game, animate) {
currentGame = game; currentGame = game;
setVisibleGame(game, animate); setVisibleGame(game, animate);
const state = game.toJSON('minify');
const deflated = pako.deflate(Buffer.from(state));
const encoded = Buffer.from(deflated).toString('base64');
if ($('#page').hasClass('cb-private')) {
history.replaceState(null, document.title, `#/private/${encoded}`);
} else {
$('.private-link').attr('href', `#/private/${encoded}`);
}
} }
function randomId(){ function randomId(){
@ -583,7 +594,12 @@ $(function (){
function player(side) { function player(side) {
return (side === PS.LIGHT ? 'light' : 'dark'); return (side === PS.LIGHT ? 'light' : 'dark');
} }
const gameId = $('#cb_board').data('gameId'); const gameId = $('#cb_board').data('gameId');
if (gameId === 'private') {
return;
}
const lightName = $('#cb_light_name').val(); const lightName = $('#cb_light_name').val();
const darkName = $('#cb_dark_name').val(); const darkName = $('#cb_dark_name').val();
const turns = currentGame.turns; const turns = currentGame.turns;
@ -646,38 +662,47 @@ $(function (){
$('#cb_light_name').val(''); $('#cb_light_name').val('');
$('#cb_dark_name').val(''); $('#cb_dark_name').val('');
cancelGameCallback = IO.onGameUpdate(newId, function(data/*, gameId*/) { if (newId === 'private') {
updateQueue.idle.then(() => { cancelGameCallback = function() {};
if (data.modified > $('#cb_board').data('modified')) { } else {
try { cancelGameCallback = IO.onGameUpdate(newId, function(data/*, gameId*/) {
const newGame = new PS.Game(JSON.stringify(data.board)); updateQueue.idle.then(() => {
const newState = JSON.parse(newGame.toJSON()); if (data.modified > $('#cb_board').data('modified')) {
const oldState = JSON.parse(currentGame.toJSON()); try {
const newGame = new PS.Game(JSON.stringify(data.board));
const newState = JSON.parse(newGame.toJSON());
const oldState = JSON.parse(currentGame.toJSON());
if (!deepEqual(newState, oldState)) { if (!deepEqual(newState, oldState)) {
debug('got board', newGame.moves); debug('got board', newGame.moves);
setCurrentGame(newGame, newGame.moves.length > currentGame.moves.length); setCurrentGame(newGame, newGame.moves.length > currentGame.moves.length);
}
} catch (err) {
debug('Error parsing board data', err);
} }
} catch (err) {
debug('Error parsing board data', err); const d = data || {};
$('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light')));
$('#cb_board').data('darkName', shortenName(String(d.darkName || 'Dark')));
$('#cb_light_name').val(String(d.lightName || ''));
$('#cb_dark_name').val(String(d.darkName || ''));
$('#cb_board').data('modified', data.modified);
} }
});
const d = data || {};
$('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light')));
$('#cb_board').data('darkName', shortenName(String(d.darkName || 'Dark')));
$('#cb_light_name').val(String(d.lightName || ''));
$('#cb_dark_name').val(String(d.darkName || ''));
$('#cb_board').data('modified', data.modified);
}
}); });
});
const notifyList = $('#cb_notify').data('gameList'); const notifyList = $('#cb_notify').data('gameList');
const doNotify = notifyList.includes('*') || notifyList.includes(newId); const doNotify = notifyList.includes('*') || notifyList.includes(newId);
setNotifyChecked(doNotify); setNotifyChecked(doNotify);
if (doNotify) { if (doNotify) {
requestNotify(); requestNotify();
}
/* Ensure that the selected game is in the list (for new games). */
if ($('#game_tile_' + newId).length < 1) {
updateSelectGameMeta({}, newId);
}
} }
const reverseList = $('#cb_reverse').data('gameList'); const reverseList = $('#cb_reverse').data('gameList');
@ -685,11 +710,6 @@ $(function (){
$('#cb_reverse').prop('checked', doReverse); $('#cb_reverse').prop('checked', doReverse);
arrangeBoard(doReverse); arrangeBoard(doReverse);
/* Ensure that the selected game is in the list (for new games). */
if ($('#game_tile_' + newId).length < 1) {
updateSelectGameMeta({}, newId);
}
/* This is in case the old tile no longer qualifies to be in the list. */ /* This is in case the old tile no longer qualifies to be in the list. */
const oldGameTile = $('#game_tile_' + gameId); const oldGameTile = $('#game_tile_' + gameId);
if (oldGameTile.length >= 1) { if (oldGameTile.length >= 1) {
@ -1273,8 +1293,6 @@ $(function (){
selectBox.setContent(gameSelectContent); selectBox.setContent(gameSelectContent);
} }
IO.onMetaUpdate(updateSelectGameMeta);
const lastNotifyState = {}; const lastNotifyState = {};
function notifyForGame(meta, gameId) { function notifyForGame(meta, gameId) {
@ -1343,20 +1361,39 @@ $(function (){
} }
} }
IO.onMetaUpdate(notifyForGame); window.onhashchange = function(/*event*/){
window.onpopstate = function(/*event*/){
const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/); const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/);
if (foundId) { if (foundId) {
switchGameId(foundId[1]); switchGameId(foundId[1]);
} }
}; };
const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/); const foundPrivate = location.hash.match(/^#\/private((\/(.*)?)?)$/);
if (foundId) { if (foundPrivate) {
switchGameId(foundId[1]); switchGameId('private');
$('#page').addClass('cb-private');
let state;
try {
const decoded = Buffer.from(foundPrivate[1].slice(1), 'base64');
const inflated = pako.inflate(decoded);
state = JSON.parse(Buffer.from(inflated).toString());
} catch(err) {/*ignore*/}
if (state) {
setCurrentGame(new PS.Game(JSON.stringify(state)));
}
} else { } else {
switchGameId(randomId()); const foundId = location.hash.match(/^#\/([0-9a-f]{16}\b)/);
if (foundId) {
switchGameId(foundId[1]);
} else {
switchGameId(randomId());
}
IO.onMetaUpdate(async (meta, gameId) => {
await updateQueue.idle;
updateSelectGameMeta(meta, gameId);
notifyForGame(meta, gameId);
});
} }
function adjustBoardSize() { function adjustBoardSize() {

View File

@ -44,6 +44,7 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"pako": "^1.0.11",
"svgo": "^1.3.2", "svgo": "^1.3.2",
"svgo-loader": "^2.2.1", "svgo-loader": "^2.2.1",
"webpack": "^4.43.0", "webpack": "^4.43.0",