X-Git-Url: https://git.auder.net/?p=xogo.git;a=blobdiff_plain;f=app.js;h=12c2b7ab65fea59a497ed1901a924889b0225491;hp=f89706233b24ef2fb380202f9cc0f59cceb24c7f;hb=HEAD;hpb=41534b92f0bcfc8ef5f58d8040706a5e7ce088c6 diff --git a/app.js b/app.js index f897062..c5d5c7f 100644 --- a/app.js +++ b/app.js @@ -4,11 +4,11 @@ let $ = document; //shortcut // Initialisations // https://stackoverflow.com/a/27747377/12660887 -function dec2hex (dec) { return dec.toString(16).padStart(2, "0") } function generateId (len) { - var arr = new Uint8Array((len || 40) / 2) - window.crypto.getRandomValues(arr) - return Array.from(arr, dec2hex).join('') + const dec2hex = (dec) => dec.toString(16).padStart(2, "0"); + let arr = new Uint8Array(len / 2); //len/2 because 2 chars per hex value + window.crypto.getRandomValues(arr); //fill with random integers + return Array.from(arr, dec2hex).join(''); } // Populate variants dropdown list @@ -30,164 +30,286 @@ if (!localStorage.getItem("name")) const sid = localStorage.getItem("sid"); $.getElementById("myName").value = localStorage.getItem("name"); +// "Material" input field name +let inputName = document.getElementById("myName"); +let formField = document.getElementById("ng-name"); +const setActive = (active) => { + if (active) + formField.classList.add("form-field--is-active"); + else { + formField.classList.remove("form-field--is-active"); + inputName.value == '' + ? formField.classList.remove("form-field--is-filled") + : formField.classList.add("form-field--is-filled"); + } +}; +setActive(true); +inputName.onblur = () => setActive(false); +inputName.onfocus = () => setActive(true); + ///////// // Utils function setName() { + // 'onChange' event on name input text field [HTML] localStorage.setItem("name", $.getElementById("myName").value); } // Turn a "tab" on, and "close" all others function toggleVisible(element) { - for (elt of document.querySelectorAll('body > div')) { - if (elt.id != element) elt.style.display = "none"; - else elt.style.display = "block"; + for (elt of document.querySelectorAll("main > div")) { + if (elt.id != element) + elt.style.display = "none"; + else + elt.style.display = "block"; + } + if (element == "boardContainer") { + // Avoid smartphone scrolling effects (TODO?) + document.querySelector("html").style.overflow = "hidden"; + document.body.style.overflow = "hidden"; + } + else { + document.querySelector("html").style.overflow = "visible"; + document.body.style.overflow = "visible"; + // Workaround "superposed texts" effect: + if (element == "newGame") + setActive(false); } } let seek_vname; function seekGame() { seek_vname = $.getElementById("selectVariant").value; - send("seekgame", {vname: seek_vname, name: localStorage.getItem("name")}); - toggleVisible("pendingSeek"); + if (send("seekgame", + {vname: seek_vname, name: localStorage.getItem("name")}) + ) { + toggleVisible("pendingSeek"); + } } function cancelSeek() { - send("cancelseek", {vname: seek_vname}); - toggleVisible("newGame"); + if (send("cancelseek", {vname: seek_vname})) + toggleVisible("newGame"); } -function sendRematch() { - send("rematch", { gid: gid }); - toggleVisible("pendingRematch"); +function sendRematch(random) { + if (send("rematch", {gid: gid, random: !!random})) + toggleVisible("pendingRematch"); } function cancelRematch() { - send("norematch", { gid: gid }); - toggleVisible("newGame"); + if (send("norematch", {gid: gid})) + toggleVisible("newGame"); } // Play with a friend (or not ^^) function showNewGameForm() { const vname = $.getElementById("selectVariant").value; - if (vname == "_random") alert("Select a variant first"); + if (vname == "_random") + alert("Select a variant first"); else { $.getElementById("gameLink").innerHTML = ""; $.getElementById("selectColor").selectedIndex = 0; toggleVisible("newGameForm"); import(`/variants/${vname}/class.js`).then(module => { - const Rules = module.default; - prepareOptions(Rules); + window.V = module.default; + for (const [k, v] of Object.entries(V.Aliases)) + window[k] = v; + prepareOptions(); }); } } -function backToNormalSeek() { toggleVisible("newGame"); } +function backToNormalSeek() { + toggleVisible("newGame"); +} -function toggleStyle(e, word) { +function toggleStyle(event, obj) { + const word = obj.innerHTML; options[word] = !options[word]; - e.target.classList.toggle("highlight-word"); + event.target.classList.toggle("highlight-word"); } let options; -function prepareOptions(Rules) { +function prepareOptions() { options = {}; let optHtml = ""; - for (let select of Rules.Options.select) { - optHtml += ` - - '; + if (V.Options.select) { + optHtml += V.Options.select.map(select => { return ` +
+ +
+ + +
+
`; + }).join(""); } - for (let check of Rules.Options.check) { - optHtml += ` - - { return ` +
+ +
`; + }).join(""); } - if (Rules.Options.styles.length >= 1) optHtml += "

"; - for (let style of Rules.Options.styles) { - optHtml += ` - - ${style} - `; + if (V.Options.styles) { + optHtml += '

'; + let i = 0; + const stylesLength = V.Options.styles.length; + while (i < stylesLength) { + optHtml += '
'; + for (let j=i; j${style}`; + } + optHtml += "
"; + i += 4; + } + optHtml += "
"; } - if (Rules.Options.styles.length >= 1) optHtml += "

"; $.getElementById("gameOptions").innerHTML = optHtml; } function getGameLink() { const vname = $.getElementById("selectVariant").value; const color = $.getElementById("selectColor").value; - for (const select of $.querySelectorAll("#gameOptions > select")) { - let value = select.value; - if (select.attributes["data-numeric"]) value = parseInt(value, 10); + for (const select of $.querySelectorAll("#gameOptions select")) { + let value = parseInt(select.value, 10); + if (isNaN(value)) //not an integer + value = select.value; options[ select.id.split("_")[1] ] = value; } - for (const check of $.querySelectorAll("#gameOptions > input")) - options[ check.id.split("_")[1] ] = check.checked; + for (const input of $.querySelectorAll("#gameOptions input")) { + const variable = input.id.split("_")[1]; + if (input.type == "number") + options[variable] = parseInt(input.value, 10); //TODO: real numbers? + else if (input.type == "checkbox") + options[variable] = input.checked; + } send("creategame", { vname: vname, - player: { sid: sid, name: localStorage.getItem("name"), color: color }, + player: {sid: sid, name: localStorage.getItem("name"), color: color}, options: options }); } -const fillGameInfos = (gameInfos, oppIndex) => { +function fillGameInfos(gameInfos, oppIndex) { fetch(`/variants/${gameInfos.vname}/rules.html`) .then(res => res.text()) .then(txt => { let htmlContent = ` -

- ${gameInfos.vdisp} - vs. ${gameInfos.players[oppIndex].name} -

-
-

`; - htmlContent += - Object.entries(gameInfos.options).map(opt => { - return ( - '' + - (opt[1] === true ? opt[0] : `${opt[0]}:${opt[1]}`) + - '' - ); - }) - .join(", "); +

+

+ ${gameInfos.vdisp} + vs. ${gameInfos.players[oppIndex].name} +

+
`; + const options = Object.entries(gameInfos.options); + if (options.length > 0) { + htmlContent += '
'; + let i = 0; + while (i < options.length) { + htmlContent += '
'; + for (let j=i; j' + + (opt[1] === true ? opt[0] : `${opt[0]}:${opt[1]}`) + " " + + ""; + } + htmlContent += "
"; + i += 4; + } + htmlContent += "
"; + } htmlContent += ` -

-
-
- ${txt} -
- `; +
${txt}
+
+ +
`; $.getElementById("gameInfos").innerHTML = htmlContent; }); -}; +} //////////////// // Communication -let socket, gid, attempt = 0; +let socket, gid, recoAttempt = 0; const autoReconnectDelay = () => { - return [100, 200, 500, 1000, 3000, 10000, 30000][Math.min(attempt, 6)]; + return [100, 200, 500, 1000, 3000, 10000, 30000][Math.min(recoAttempt, 6)]; }; -function copyClipboard(msg) { navigator.clipboard.writeText(msg); } +function send(code, data, opts) { + opts = opts || {}; + const trySend = () => { + if (socket.readyState == 1) { + socket.send(JSON.stringify(Object.assign({code: code}, data))); + if (opts.success) + opts.success(); + return true; + } + return false; + }; + const firstTry = trySend(); + if (!firstTry) { + if (opts.retry) { + // Retry for a few seconds (sending move) + let sendAttempt = 1; + const retryLoop = setInterval( + () => { + if (trySend() || ++sendAttempt >= 3) + clearInterval(retryLoop); + if (sendAttempt >= 3 && opts.error) + opts.error(); + }, + 1000 + ); + } + else if (opts.error) + opts.error(); + } + return firstTry; +} + +function copyClipboard(msg) { + navigator.clipboard.writeText(msg); +} function getWhatsApp(msg) { return `https://api.whatsapp.com/send?text=${encodeURIComponent(msg)}`; } const tryResumeGame = () => { - attempt = 0; + recoAttempt = 0; // If a game is found, resume it: if (localStorage.getItem("gid")) { gid = localStorage.getItem("gid"); - send("getgame", { gid: gid }); + send("getgame", + {gid: gid}, + { + retry: true, + error: () => alert("Cannot load game: no connection") + }); } else { // If URL indicates "play with a friend", start game: @@ -195,9 +317,14 @@ const tryResumeGame = () => { if (hashIdx >= 0) { const urlParts = $.URL.split('#'); gid = urlParts[1]; - send("joingame", { gid: gid, name: localStorage.getItem("name") }); localStorage.setItem("gid", gid); - history.replaceState(null, '', urlParts[0]); + history.replaceState(null, '', urlParts[0]); //hide game ID + send("joingame", + {gid: gid, name: localStorage.getItem("name")}, + { + retry: true, + error: () => alert("Cannot load game: no connection") + }); } } }; @@ -207,7 +334,8 @@ const messageCenter = (msg) => { switch (obj.code) { // Start new game: case "gamestart": { - if (!$.hasFocus()) notifyMe("game"); + if (document.hidden) + notifyMe("game"); gid = obj.gid; initializeGame(obj); break; @@ -219,7 +347,7 @@ const messageCenter = (msg) => {

WhatsApp / - ToClipboard + ToClipboard

${link}

`; @@ -239,13 +367,18 @@ const messageCenter = (msg) => { break; // Receive opponent's move: case "newmove": - if (!$.hasFocus()) notifyMe("move"); + // Basic check: was it really opponent's turn? + if (vr.turn == playerColor) + break; + if (document.hidden) + notifyMe("move"); vr.playReceivedMove(obj.moves, () => { - if (vr.getCurrentScore(obj.moves[obj.moves.length-1]) != "*") { + if (vr.getCurrentScore(obj.moves) != "*") { localStorage.removeItem("gid"); setTimeout( () => toggleVisible("gameStopped"), 2000 ); } - else toggleTurnIndicator(true); + else + toggleTurnIndicator(true); }); break; // Opponent stopped game (draw, abort, resign...) @@ -257,11 +390,15 @@ const messageCenter = (msg) => { case "closerematch": toggleVisible("newGame"); break; + case "filechange": + // TODO?: could be more subtle + setTimeout(() => location.reload(), 100); + break; } }; const handleError = (err) => { - if (err.code === 'ECONNREFUSED') { + if (err.code === "ECONNREFUSED") { removeAllListeners(); alert("Server refused connection. Please reload page later"); } @@ -275,35 +412,34 @@ const handleClose = () => { }, autoReconnectDelay()); }; -const removeAllListeners = () => { +function removeAllListeners() { socket.removeEventListener("open", tryResumeGame); socket.removeEventListener("message", messageCenter); socket.removeEventListener("error", handleError); socket.removeEventListener("close", handleClose); -}; +} -const connectToWSS = () => { +function connectToWSS() { socket = new WebSocket(`${Params.socket_server}${Params.socket_path}?sid=${sid}`); socket.addEventListener("open", tryResumeGame); socket.addEventListener("message", messageCenter); socket.addEventListener("error", handleError); socket.addEventListener("close", handleClose); - attempt++; -}; + recoAttempt++; +} connectToWSS(); -const send = (code, data) => { - socket.send(JSON.stringify(Object.assign({code: code}, data))); -}; - /////////// // Playing function toggleTurnIndicator(myTurn) { - let indicator = $.getElementById("chessboard"); - if (myTurn) indicator.style.outline = "thick solid green"; - else indicator.style.outline = "thick solid lightgrey"; + let indicator = + $.getElementById("boardContainer").querySelector(".chessboard"); + if (myTurn) + indicator.style.outline = "thick solid green"; + else + indicator.style.outline = "thick solid lightgrey"; } function notifyMe(code) { @@ -312,77 +448,104 @@ function notifyMe(code) { new Notification("New " + code, { vibrate: [200, 100, 200] }); new Audio("/assets/new_" + code + ".mp3").play(); } - if (Notification.permission === 'granted') doNotify(); - else if (Notification.permission !== 'denied') { - Notification.requestPermission().then((permission) => { - if (permission === 'granted') doNotify(); + if (Notification.permission === "granted") + doNotify(); + else if (Notification.permission !== "denied") { + Notification.requestPermission().then(permission => { + if (permission === "granted") + doNotify(); }); } } -let curMoves = []; -const afterPlay = (move) => { //pack into one moves array, then send - curMoves.push({ - appear: move.appear, - vanish: move.vanish, - start: move.start, - end: move.end - }); - if (vr.turn != color) { - toggleTurnIndicator(false); - send("newmove", { gid: gid, moves: curMoves, fen: vr.getFen() }); +let curMoves = [], + lastFen; +const afterPlay = (move_s, newTurn, ops) => { + if (ops.send) { + // Pack into one moves array, then send (if turn changed) + if (Array.isArray(move_s)) + // Array of simple moves (e.g. Chakart) + Array.prototype.push.apply(curMoves, move_s); + else + // Usual case + curMoves.push(move_s); + if (newTurn != playerColor) { + send("newmove", + {gid: gid, moves: curMoves, fen: vr.getFen()}, + { + retry: true, + error: () => alert("Move not sent: reload page") + } + ); + } + } + if (ops.res && newTurn != playerColor) { + toggleTurnIndicator(false); //now all moves are sent and animated + const result = vr.getCurrentScore(curMoves); curMoves = []; - const result = vr.getCurrentScore(move); if (result != "*") { - setTimeout( () => { + setTimeout(() => { toggleVisible("gameStopped"); - send("gameover", { gid: gid }); + send("gameover", {gid: gid}); }, 2000); } } }; -// Avoid loading twice the same stylesheet: -const conditionalLoadCSS = (vname) => { - const allIds = [].slice.call($.styleSheets).map(s => s.id); - const newId = vname + "_css"; - if (!allIds.includes(newId)) { - $.getElementsByTagName("head")[0].insertAdjacentHTML( - "beforeend", - ``); - } -}; - -let vr, color; +let vr = null, playerColor, lastVname = undefined; function initializeGame(obj) { const options = obj.options || {}; import(`/variants/${obj.vname}/class.js`).then(module => { - const Rules = module.default; - conditionalLoadCSS(obj.vname); - color = (sid == obj.players[0].sid ? "w" : "b"); + window.V = module.default; + for (const [k, v] of Object.entries(V.Aliases)) + window[k] = v; + if (lastVname != obj.vname) { + // Load CSS + unload potential previous one. + if (lastVname) + document.getElementById(lastVname + "_css").remove(); + $.getElementsByTagName("head")[0].insertAdjacentHTML( + "beforeend", + ``); + lastVname = obj.vname; + } + playerColor = (sid == obj.players[0].sid ? "w" : "b"); // Init + remove potential extra DOM elements from a previous game: document.getElementById("boardContainer").innerHTML = `
- + + + + +
- + + + + +
-
`; - vr = new Rules({ +
`; + if (vr) + // Avoid interferences: + vr.removeListeners(); + vr = new V({ seed: obj.seed, //may be null if FEN already exists (running game) fen: obj.fen, - element: "chessboard", - color: color, + element: "boardContainer", + color: playerColor, afterPlay: afterPlay, options: options }); - if (!obj.fen) { - // Game creation - if (color == "w") send("setfen", {gid: obj.gid, fen: vr.getFen()}); + const gameCreation = !obj.fen; + if (gameCreation) { + // Both players set FEN, in case of one is offline + send("setfen", {gid: obj.gid, fen: vr.getFen()}); localStorage.setItem("gid", obj.gid); } const select = $.getElementById("selectVariant"); @@ -393,16 +556,18 @@ function initializeGame(obj) { break; } } - fillGameInfos(obj, color == "w" ? 1 : 0); - if (obj.randvar) toggleVisible("gameInfos"); - else toggleVisible("boardContainer"); - toggleTurnIndicator(vr.turn == color); + const playerIndex = (playerColor == "w" ? 0 : 1); + fillGameInfos(obj, 1 - playerIndex); + if (obj.players[playerIndex].randvar && gameCreation) + toggleVisible("gameInfos"); + else + toggleVisible("boardContainer"); + toggleTurnIndicator(vr.turn == playerColor); }); } function confirmStopGame() { - if (confirm("Stop game?")) { - send("gameover", { gid: gid, relay: true }); + if (confirm("Stop game?") && send("gameover", {gid: gid, relay: true})) { localStorage.removeItem("gid"); toggleVisible("gameStopped"); } @@ -411,12 +576,15 @@ function confirmStopGame() { function toggleGameInfos() { if ($.getElementById("gameInfos").style.display == "none") toggleVisible("gameInfos"); - else toggleVisible("boardContainer"); + else + toggleVisible("boardContainer"); } $.body.addEventListener("keydown", (e) => { - if (!localStorage.getItem("gid")) return; - if (e.keyCode == 27) confirmStopGame(); + if (!localStorage.getItem("gid")) + return; + if (e.keyCode == 27) + confirmStopGame(); else if (e.keyCode == 32) { e.preventDefault(); toggleGameInfos();