From 28b32b4fc7c23b1c72bed68e1897576c5be46c3d Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Tue, 10 Mar 2020 20:54:10 +0100 Subject: [PATCH] Some fixes and enhancements as suggested on Discord --- TODO | 5 + client/public/images/icons/SOURCE | 1 + client/public/images/icons/delete.svg | 1 + client/src/components/BaseGame.vue | 1 + client/src/components/Board.vue | 143 ++++++++----- client/src/components/GameList.vue | 6 + client/src/components/MoveList.vue | 1 + client/src/translations/en.js | 4 + client/src/translations/es.js | 4 + client/src/translations/fr.js | 4 + client/src/variants/Eightpieces.js | 117 ++++++----- client/src/views/Game.vue | 2 +- client/src/views/Hall.vue | 290 ++++++++++++++++++-------- client/src/views/MyGames.vue | 26 +-- server/sockets.js | 6 +- 15 files changed, 402 insertions(+), 209 deletions(-) create mode 100644 client/public/images/icons/delete.svg diff --git a/TODO b/TODO index 76349413..8ac83a08 100644 --- a/TODO +++ b/TODO @@ -34,3 +34,8 @@ Possibles passes : soit à une pièce, soit sur une case. --> selon le mode de déplacement standard (donc tout droit pour les pions) Pas de notion d'échec ou de mat (?) Si une pièce est mat elle donne le ballon (?) + +Landing pieces from empty board: +https://www.chessvariants.com/diffsetup.dir/unachess.html + +Rugby http://www.echecspourtous.com/?page_id=7945 diff --git a/client/public/images/icons/SOURCE b/client/public/images/icons/SOURCE index b1c82815..eeb51d3e 100644 --- a/client/public/images/icons/SOURCE +++ b/client/public/images/icons/SOURCE @@ -12,3 +12,4 @@ https://www.flaticon.com/free-icon/right_565870?term=forward&page=1&position=31 https://www.flaticon.com/free-icon/download_724933?term=download&page=1&position=3 https://www.flaticon.com/free-icon/resize_512182?term=resize&page=1&position=49 https://www.flaticon.com/free-icon/clear_565313?term=delete&page=1&position=33 +https://www.flaticon.com/free-icon/clear_1632708?term=delete&page=1&position=3 diff --git a/client/public/images/icons/delete.svg b/client/public/images/icons/delete.svg new file mode 100644 index 00000000..875ce27c --- /dev/null +++ b/client/public/images/icons/delete.svg @@ -0,0 +1 @@ +<svg height="511.992pt" viewBox="0 0 511.992 511.992" width="511.992pt" xmlns="http://www.w3.org/2000/svg"><path d="m415.402344 495.421875-159.40625-159.410156-159.40625 159.410156c-22.097656 22.09375-57.921875 22.09375-80.019532 0-22.09375-22.097656-22.09375-57.921875 0-80.019531l159.410157-159.40625-159.410157-159.40625c-22.09375-22.097656-22.09375-57.921875 0-80.019532 22.097657-22.09375 57.921876-22.09375 80.019532 0l159.40625 159.410157 159.40625-159.410157c22.097656-22.09375 57.921875-22.09375 80.019531 0 22.09375 22.097657 22.09375 57.921876 0 80.019532l-159.410156 159.40625 159.410156 159.40625c22.09375 22.097656 22.09375 57.921875 0 80.019531-22.097656 22.09375-57.921875 22.09375-80.019531 0zm0 0" fill="#e76e54"/></svg> \ No newline at end of file diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 0b6fa055..0d5a3bbc 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -516,6 +516,7 @@ export default { display: inline-block #controls + user-select: none margin: 0 auto text-align: center display: flex diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 19a5e321..5080e846 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -23,7 +23,8 @@ export default { possibleMoves: [], //filled after each valid click/dragstart choices: [], //promotion pieces, or checkered captures... (as moves) selectedPiece: null, //moving piece (or clicked piece) - start: {}, //pixels coordinates + id of starting square (click or drag) + start: null, //pixels coordinates + id of starting square (click or drag) + click: "", settings: store.state.settings }; }, @@ -351,76 +352,101 @@ export default { }, methods: { mousedown: function(e) { - // Abort if a piece is already being processed, or target is not a piece. - // NOTE: just looking at classList[0] because piece is the first assigned class - if (!!this.selectedPiece || e.target.classList[0] != "piece") return; - e.preventDefault(); //disable native drag & drop - let parent = e.target.parentNode; //the surrounding square - // Next few lines to center the piece on mouse cursor - let rect = parent.getBoundingClientRect(); - this.start = { - x: rect.x + rect.width / 2, - y: rect.y + rect.width / 2, - id: parent.id - }; - this.selectedPiece = e.target.cloneNode(); - let spStyle = this.selectedPiece.style; - spStyle.position = "absolute"; - spStyle.top = 0; - spStyle.display = "inline-block"; - spStyle.zIndex = 3000; - const startSquare = getSquareFromId(parent.id); - this.possibleMoves = []; - const color = this.analyze ? this.vr.turn : this.userColor; - if (this.vr.canIplay(color, startSquare)) - this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare); - // Next line add moving piece just after current image - // (required for Crazyhouse reserve) - parent.insertBefore(this.selectedPiece, e.target.nextSibling); + e.preventDefault(); + if (!this.start) { + // Start square must contain a piece. + // NOTE: classList[0] is enough: 'piece' is the first assigned class + if (e.target.classList[0] != "piece") return; + let parent = e.target.parentNode; //surrounding square + // Show possible moves if current player allowed to play + const startSquare = getSquareFromId(parent.id); + this.possibleMoves = []; + const color = this.analyze ? this.vr.turn : this.userColor; + if (this.vr.canIplay(color, startSquare)) + this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare); + // For potential drag'n drop, remember start coordinates + // (to center the piece on mouse cursor) + let rect = parent.getBoundingClientRect(); + this.start = { + x: rect.x + rect.width / 2, + y: rect.y + rect.width / 2, + id: parent.id + }; + // Add the moving piece to the board, just after current image + this.selectedPiece = e.target.cloneNode(); + Object.assign( + this.selectedPiece.style, + { + position: "absolute", + top: 0, + display: "inline-block", + zIndex: 3000 + } + ); + parent.insertBefore(this.selectedPiece, e.target.nextSibling); + } else { + this.processMoveAttempt(e); + } }, mousemove: function(e) { if (!this.selectedPiece) return; + e.preventDefault(); // There is an active element: move it around const [offsetX, offsetY] = this.mobileBrowser ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY] : [e.clientX, e.clientY]; - this.selectedPiece.style.left = offsetX - this.start.x + "px"; - this.selectedPiece.style.top = offsetY - this.start.y + "px"; + Object.assign( + this.selectedPiece.style, + { + left: offsetX - this.start.x + "px", + top: offsetY - this.start.y + "px" + } + ); }, mouseup: function(e) { if (!this.selectedPiece) return; - // There is an active element: obtain the move from start and end squares - this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords + e.preventDefault(); + // Drag'n drop. Selected piece is no longer needed: + this.selectedPiece.parentNode.removeChild(this.selectedPiece); + delete this.selectedPiece; + this.selectedPiece = null; + this.processMoveAttempt(e); + }, + processMoveAttempt: function(e) { + // Obtain the move from start and end squares const [offsetX, offsetY] = this.mobileBrowser ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY] : [e.clientX, e.clientY]; let landing = document.elementFromPoint(offsetX, offsetY); - this.selectedPiece.style.zIndex = 3000; // Next condition: classList.contains(piece) fails because of marks while (landing.tagName == "IMG") landing = landing.parentNode; - if (this.start.id == landing.id) - // One or multi clicks on same piece + if (this.start.id == landing.id) { + if (this.click == landing.id) { + // Second click on same square: cancel current move + this.possibleMoves = []; + this.start = null; + this.click = ""; + } else this.click = landing.id; return; + } + this.start = null; // OK: process move attempt, landing is a square node let endSquare = getSquareFromId(landing.id); let moves = this.findMatchingMoves(endSquare); this.possibleMoves = []; if (moves.length > 1) this.choices = moves; else if (moves.length == 1) this.play(moves[0]); - // Else: impossible move - this.selectedPiece.parentNode.removeChild(this.selectedPiece); - delete this.selectedPiece; - this.selectedPiece = null; + // else: forbidden move attempt }, findMatchingMoves: function(endSquare) { // Run through moves list and return the matching set (if promotions...) - let moves = []; - this.possibleMoves.forEach(function(m) { - if (endSquare[0] == m.end.x && endSquare[1] == m.end.y) moves.push(m); - }); - return moves; + return ( + this.possibleMoves.filter(m => { + return (endSquare[0] == m.end.x && endSquare[1] == m.end.y); + }) + ); }, play: function(move) { this.$emit("play-move", move); @@ -442,12 +468,14 @@ export default { // NOTE: no variants with reserve of size != 8 .game + user-select: none width: 100% margin: 0 .board cursor: pointer #choices + user-select: none margin: 0 position: absolute z-index: 300 @@ -465,14 +493,9 @@ export default { img.ghost position: absolute - opacity: 0.4 + opacity: 0.5 top: 0 -.highlight-light - background-color: rgba(0, 204, 102, 0.7) !important -.highlight-dark - background-color: rgba(0, 204, 102, 0.9) !important - .incheck-light background-color: rgba(204, 51, 0, 0.7) !important .incheck-dark @@ -489,7 +512,25 @@ img.ghost background-color: #6f8f57; .light-square.chesstempo - background-color: #fdfdfd; + background-color: #dfdfdf; .dark-square.chesstempo - background-color: #88a0a8; + background-color: #7287b6; + +// TODO: no predefined highlight colors, but layers. How? + +.light-square.lichess.highlight-light + background-color: #cdd26a !important +.dark-square.lichess.highlight-dark + background-color: #aaa23a !important + +.light-square.chesscom.highlight-light + background-color: #f7f783 !important +.dark-square.chesscom.highlight-dark + background-color: #bacb44 !important + +.light-square.chesstempo.highlight-light + background-color: #9f9fff !important +.dark-square.chesstempo.highlight-dark + background-color: #557fff !important + </style> diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index 5396d13c..354cfa03 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -61,6 +61,12 @@ export default { remGames.forEach(g => { if (g.created < minCreated) minCreated = g.created; if (g.created > maxCreated) maxCreated = g.created; + g.priority = 0; + if (g.score == "*") { + g.priority++; + if (!!g.myColor) g.priority++; + if (!!g.myTurn) g.priority++; + } }); const deltaCreated = maxCreated - minCreated; return remGames.sort((g1, g2) => { diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index 6f2eb317..eb47dc90 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -160,6 +160,7 @@ export default { <style lang="sass" scoped> .moves-list + user-select: none cursor: pointer min-height: 1px max-height: 500px diff --git a/client/src/translations/en.js b/client/src/translations/en.js index f0a5942a..35779ee7 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -23,6 +23,7 @@ export const translations = { Cadence: "Cadence", Cancel: "Cancel", Challenge: "Challenge", + "Challenge already exists": "Challenge already exists", "Challenge declined": "Challenge declined", "Chat here": "Chat here", "Clear history": "Clear history", @@ -58,6 +59,7 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "Logout successful!", + "Memorize?": "Memorize?", "Mispelled variant name": "Mispelled variant name", "Missing email": "Missing email", "Missing FEN": "Missing FEN", @@ -95,6 +97,7 @@ export const translations = { "Please select a variant": "Please select a variant", Practice: "Practice", "Prefix?": "Prefix?", + "Preset challenges": "Preset challenges", Previous: "Previous", "Processing... Please wait": "Processing... Please wait", Problems: "Problems", @@ -105,6 +108,7 @@ export const translations = { Register: "Register", "Registration complete! Please check your emails now": "Registration complete! Please check your emails now", Rematch: "Rematch", + "Rematch in progress:": "Rematch in progress", "Remove game?": "Remove game?", Resign: "Resign", "Resign the game?": "Resign the game?", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index b79df7e8..2095f923 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -23,6 +23,7 @@ export const translations = { Cadence: "Cadencia", Cancel: "Anular", Challenge: "Desafiar", + "Challenge already exists": "El desafÃo ya existe", "Challenge declined": "DesafÃo rechazado", "Chat here": "Chat aquÃ", "Clear history": "Clara historia", @@ -58,6 +59,7 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "¡Desconexión exitosa!", + "Memorize?": "¿Memorizar?", "Mispelled variant name": "Variante mal escrita", "Missing email": "Email falta", "Missing FEN": "FEN falta", @@ -95,6 +97,7 @@ export const translations = { "Please select a variant": "Por favor seleccione una variante", Practice: "Práctica", "Prefix?": "¿Prefijo?", + "Preset challenges": "DesafÃos registrados", Previous: "Anterior", "Processing... Please wait": "Procesando... por favor espere", Problems: "Problemas", @@ -105,6 +108,7 @@ export const translations = { Register: "Registrarse", "Registration complete! Please check your emails now": "¡Registro completo! Revise sus correos electrónicos ahora", Rematch: "Revancha", + "Rematch in progress:": "Revancha en progreso:", "Remove game?": "¿Eliminar la partida?", Resign: "Abandonar", "Resign the game?": "¿Abandonar la partida?", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 2cbc6d28..7030c5a6 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -23,6 +23,7 @@ export const translations = { Cadence: "Cadence", Cancel: "Annuler", Challenge: "Défier", + "Challenge already exists": "Le défi existe déjà ", "Challenge declined": "Défi refusé", "Chat here": "Chattez ici", "Clear history": "Effacer l'historique", @@ -58,6 +59,7 @@ export const translations = { Login: "Login", Logout: "Logout", "Logout successful!": "Déconnection réussie !", + "Memorize?": "Mémoriser ?", "Mispelled variant name": "Variante mal orthographiée", "Missing email": "Email manquant", "Missing FEN": "FEN manquante", @@ -95,6 +97,7 @@ export const translations = { "Please select a variant": "Sélectionnez une variante SVP", Practice: "Pratiquer", "Prefix?": "Préfixe ?", + "Preset challenges": "Défis enregistrés", Previous: "Précédent", "Processing... Please wait": "Traitement en cours... Attendez SVP", Problems: "Problèmes", @@ -105,6 +108,7 @@ export const translations = { Register: "S'enregistrer", "Registration complete! Please check your emails now": "Enregistrement terminé ! Allez voir vos emails maintenant", Rematch: "Rejouer", + "Rematch in progress:": "Revanche en cours :", "Remove game?": "Supprimer la partie ?", Resign: "Abandonner", "Resign the game?": "Abandonner la partie ?", diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index 06ece2a6..8fe89359 100644 --- a/client/src/variants/Eightpieces.js +++ b/client/src/variants/Eightpieces.js @@ -17,83 +17,100 @@ export const VariantRules = class EightpiecesRules extends ChessRules { return ChessRules.PIECES.concat([V.JAILER, V.SENTRY, V.LANCER]); } + static get LANCER_DIRS() { + return { + 'c': [-1, 0], //north + 'd': [-1, 1], //N-E + 'e': [0, 1], //east + 'f': [1, 1], //S-E + 'g': [1, 0], //south + 'h': [1, -1], //S-W + 'm': [0, -1], //west + 'o': [-1, -1] //N-W + }; + } + + getPiece(i, j) { + const piece = this.board[i][j].charAt(1); + // Special lancer case: 8 possible orientations + if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER; + return piece; + } + getPpath(b) { - // TODO: more subtle, path depends on the orientations - // lancerOrientations should probably be a 8x8 array, for speed. return ( ([V.JAILER, V.SENTRY, V.LANCER].includes(b[1]) ? "Eightpieces/" : "") + b ); } - static IsGoodFen(fen) { - if (!ChessRules.IsGoodFen(fen)) return false; - const fenParsed = V.ParseFen(fen); - // 5) Check lancers orientations (if there are any left) - if ( - !fenParsed.lancers || - ( - fenParsed.lancers != "-" && - !fenParsed.lancers.match(/^([a-h][1-8][0-7],?)+$/) - ) - ) { - return false; - } - return true; + setOtherVariables(fen) { + super.setOtherVariables(fen); + // subTurn == 2 only when a sentry moved, and is about to push something + this.subTurn = 1; + // Stack pieces' forbidden squares after a sentry move at each turn + this.sentryPath = []; } - static ParseFen(fen) { - const fenParts = fen.split(" "); - return Object.assign(ChessRules.ParseFen(fen), { - lancers: fenParts[5], - }); + canTake([x1,y1], [x2, y2]) { + if (this.subTurn == 2) + // Sentry push: pieces can capture own color (only) + return this.getColor(x1, y1) == this.getColor(x2, y2); + return super.canTake([x1,y1], [x2, y2]); } static GenRandInitFen(randomness) { // TODO: special conditions } - getFen() { - return ( - super.getFen() + " " + this.getLancersFen() - ); + // TODO: rook + jailer + scanKingsRooks(fen) { + this.kingPos = { w: [-1, -1], b: [-1, -1] }; + const fenRows = V.ParseFen(fen).position.split("/"); + for (let i = 0; i < fenRows.length; i++) { + let k = 0; //column index on board + for (let j = 0; j < fenRows[i].length; j++) { + switch (fenRows[i].charAt(j)) { + case "k": + case "l": + this.kingPos["b"] = [i, k]; + break; + case "K": + case "L": + this.kingPos["w"] = [i, k]; + break; + default: { + const num = parseInt(fenRows[i].charAt(j)); + if (!isNaN(num)) k += num - 1; + } + } + k++; + } + } } - getFenForRepeat() { - return ( - this.getBaseFen() + "_" + - this.getTurnFen() + "_" + - this.getFlagsFen() + "_" + - this.getEnpassantFen() + "_" + - this.getLancersFen() - ); + getPotentialMovesFrom([x,y]) { + // if subTurn == 2, allow only } - getLancersFen() { - let res = ""; - this.lancerOrientations.forEach(o => { - res += V.CoordsToSquare(o.sq) + o.dir + ","; - }); - res = res.slice(0, -1); - return res || "-"; + // getPotentialMoves, isAttacked: TODO + getPotentialCastleMoves(sq) { //TODO: adapt, with jailer } - setOtherVariables(fen) { - super.setOtherVariables(fen); - const fenParsed = V.ParseFen(fen); - // Also init lancer orientations (from FEN): - this.lancerOrientations = 32; // TODO + updateVariables(move) { + // TODO: stack sentryPath if subTurn == 2 --> all squares between move.start et move.end, sauf si c'est un pion } - // getPotentialMoves, isAttacked: TODO - - // updatedVariables: update lancers' orientations + // TODO: special pass move: take jailer with king - // subTurn : if sentry moved to some enemy piece. + // subTurn : if sentry moved to some enemy piece --> enregistrer déplacement sentry, subTurn == 2, puis déplacer pièce adverse --> 1st 1/2 of turn, vanish sentry tout simplement. + // --> le turn ne change pas ! + // 2nd half: move only + // FEN flag: sentryPath from init pushing to final enemy square --> forbid some moves (getPotentialMoves) static get VALUES() { return Object.assign( - { l: 5, s: 4, j: 5 }, //experimental + { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations ChessRules.VALUES ); } diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index b39e7d2c..df5ef6f3 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -89,7 +89,7 @@ main ) img(src="/images/icons/resign.svg") button.tooltip( - v-else-if="!!game.mycolor" + v-else @click="clickRematch()" :class="{['rematch-' + rematchOffer]: true}" :aria-label="st.tr['Rematch']" diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 92030e74..14ea54fa 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -34,7 +34,7 @@ main ) .card label#closeNewgame.modal-close(for="modalNewgame") - div(@keyup.enter="newChallenge()") + div(@keyup.enter="issueNewChallenge()") fieldset label(for="selectVariant") {{ st.tr["Variant"] }} * select#selectVariant( @@ -65,6 +65,12 @@ main option(value="0") {{ st.tr["Deterministic"] }} option(value="1") {{ st.tr["Symmetric random"] }} option(value="2") {{ st.tr["Asymmetric random"] }} + fieldset + label(for="memorizeChall") {{ st.tr["Memorize?"] }} + input#memorizeChall( + type="checkbox" + v-model="newchallenge.memorize" + ) fieldset(v-if="st.user.id > 0") label(for="selectPlayers") {{ st.tr["Play with?"] }} input#selectPlayers( @@ -79,7 +85,7 @@ main v-model="newchallenge.fen" ) .diagram(v-html="newchallenge.diag") - button(@click="newChallenge()") {{ st.tr["Send challenge"] }} + button(@click="issueNewChallenge()") {{ st.tr["Send challenge"] }} input#modalPeople.modal( type="checkbox" @click="resetSocialColor()" @@ -122,6 +128,26 @@ main | {{ st.tr["Who's there?"] }} button(@click="showNewchallengeForm()") | {{ st.tr["New game"] }} + .row(v-if="presetChalls.length > 0") + .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + h4.text-center {{ st.tr["Preset challenges"] }} + table + thead + tr + th {{ st.tr["Variant"] }} + th {{ st.tr["Cadence"] }} + th {{ st.tr["Random?"] }} + th + tbody + tr( + v-for="pc in presetChalls" + @click="newChallFromPreset(pc)" + ) + td {{ pc.vname }} + td {{ pc.cadence }} + td(:class="getRandomnessClass(pc)") + td.remove-preset(@click="removePresetChall($event, pc)") + img(src="/images/icons/delete.svg") .row .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 div#div2 @@ -199,10 +225,12 @@ export default { // diagrams of targetted challenges: V: null, vname: "", - diag: "" //visualizing FEN + diag: "", //visualizing FEN + memorize: false //put settings in localStorage }, tchallDiag: "", curChallToAccept: {from: {}}, + presetChalls: JSON.parse(localStorage.getItem("presetChalls") || "[]"), newChat: "", conn: null, connexionString: "", @@ -261,8 +289,7 @@ export default { g, { type: type, - vname: vname, - priority: g.score == "*" ? 1 : 0 //for display + vname: vname } ); }) @@ -368,6 +395,11 @@ export default { this.send("disconnect"); }, methods: { + getRandomnessClass: function(pc) { + return { + ["random-" + pc.randomness]: true + }; + }, visibilityChange: function() { // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 this.send( @@ -381,11 +413,37 @@ export default { this.newchallenge.to = ""; this.newchallenge.fen = ""; this.newchallenge.diag = ""; + this.newchallenge.memorize = false; }, showNewchallengeForm: function() { this.partialResetNewchallenge(); window.doClick("modalNewgame"); }, + addPresetChall: function(chall) { + // Add only if not already existing: + if (this.presetChalls.some(c => + c.vid == chall.vid && + c.cadence == chall.cadence && + c.randomness == chall.randomness + )) { + return; + } + const L = this.presetChalls.length; + this.presetChalls.push({ + index: L, + vid: chall.vid, + vname: chall.vname, //redundant, but easier + cadence: chall.cadence, + randomness: chall.randomness + }); + localStorage.setItem("presetChalls", JSON.stringify(this.presetChalls)); + }, + removePresetChall: function(e, pchall) { + e.stopPropagation(); + const pchallIdx = this.presetChalls.findIndex(pc => pc.index == pchall.index); + this.presetChalls.splice(pchallIdx, 1); + localStorage.setItem("presetChalls", JSON.stringify(this.presetChalls)); + }, tchallButtonsMargin: function() { if (!!this.curChallToAccept.fen) return { "margin-top": "10px" }; return {}; @@ -411,7 +469,7 @@ export default { return this.games.filter(g => g.type == type); }, classifyObject: function(o) { - //challenge or game + // o: challenge or game return o.cadence.indexOf("d") === -1 ? "live" : "corr"; }, setDisplay: function(letter, type, e) { @@ -445,6 +503,7 @@ export default { this.partialResetNewchallenge(); // Available, in Hall this.newchallenge.to = this.people[sid].name; + // TODO: also store target sid to not re-search for it document.getElementById("modalPeople").checked = false; window.doClick("modalNewgame"); }, @@ -509,7 +568,7 @@ export default { this.people[s.sid].pages.push({ path: page, focus: true }); if (!s.page) // Peer is in Hall - this.send("askchallenge", { target: s.sid }); + this.send("askchallenges", { target: s.sid }); // Peer is in Game else this.send("askgame", { target: s.sid, page: page }); }); @@ -518,11 +577,11 @@ export default { case "connect": case "gconnect": { const page = data.page || "/"; - // Only ask game / challenge if first connexion: + // Only ask game / challenges if first connexion: if (!this.people[data.from]) { this.people[data.from] = { pages: [{ path: page, focus: true }] }; if (data.code == "connect") - this.send("askchallenge", { target: data.from }); + this.send("askchallenges", { target: data.from }); else this.send("askgame", { target: data.from, page: page }); } else { // Append page if not already in list @@ -544,10 +603,11 @@ export default { if (!this.people[data.from]) return; // Disconnect means no more tmpIds: if (data.code == "disconnect") { - // Remove the live challenge sent by this player: + // Remove the live challenges sent by this player: ArrayFun.remove( this.challenges, - c => c.type == "live" && c.from.sid == data.from + c => c.type == "live" && c.from.sid == data.from, + "all" ); } else { // Remove the matching live game if now unreachable @@ -625,69 +685,56 @@ export default { } break; } - case "askchallenge": { - // Send my current live challenge (if any) - const cIdx = this.challenges.findIndex( - c => c.from.sid == this.st.user.sid && c.type == "live" - ); - if (cIdx >= 0) { - const c = this.challenges[cIdx]; - // NOTE: in principle, should only send targeted challenge to the target. - // But we may not know yet the identity of the target (just name), - // so cannot decide if data.from is the target or not. - const myChallenge = { - id: c.id, - from: this.st.user.sid, - to: c.to, - randomness: c.randomness, - fen: c.fen, - vid: c.vid, - cadence: c.cadence, - added: c.added - }; - this.send("challenge", { data: myChallenge, target: data.from }); - } + case "askchallenges": { + // Send my current live challenges (if any) + const myChallenges = this.challenges + .filter(c => + c.from.sid == this.st.user.sid && c.type == "live" + ) + .map(c => { + // NOTE: in principle, should only send targeted challenge to the target. + // But we may not know yet the identity of the target (just name), + // so cannot decide if data.from is the target or not. + return { + id: c.id, + from: this.st.user.sid, + to: c.to, + randomness: c.randomness, + fen: c.fen, + vid: c.vid, + cadence: c.cadence, + added: c.added + }; + }); + if (myChallenges.length > 0) + this.send("challenges", { data: myChallenges, target: data.from }); break; } - case "challenge": //after "askchallenge" - case "newchallenge": { - // NOTE about next condition: see "askchallenge" case. - const chall = data.data; - if ( - !chall.to || - (this.people[chall.from].id > 0 && - (chall.from == this.st.user.sid || chall.to == this.st.user.name)) - ) { - let newChall = Object.assign({}, chall); - newChall.type = this.classifyObject(chall); - newChall.randomness = chall.randomness; - newChall.added = Date.now(); - let fromValues = Object.assign({}, this.people[chall.from]); - delete fromValues["pages"]; //irrelevant in this context - newChall.from = Object.assign({ sid: chall.from }, fromValues); - newChall.vname = this.getVname(newChall.vid); - this.challenges.push(newChall); - if ( - (newChall.type == "live" && this.cdisplay == "corr") || - (newChall.type == "corr" && this.cdisplay == "live") - ) { - document - .getElementById("btnC" + newChall.type) - .classList.add("somethingnew"); - } - } + case "challenges": //after "askchallenges" + data.data.forEach(this.addChallenge); + break; + case "newchallenge": + this.addChallenge(data.data); break; - } case "refusechallenge": { const cid = data.data; ArrayFun.remove(this.challenges, c => c.id == cid); alert(this.st.tr["Challenge declined"]); break; } - case "deletechallenge": { - // NOTE: the challenge may be already removed - const cid = data.data; - ArrayFun.remove(this.challenges, c => c.id == cid); + case "deletechallenge_s": { + // NOTE: the challenge(s) may be already removed + const cref = data.data; + if (!!cref.cid) ArrayFun.remove(this.challenges, c => c.id == cref.cid); + else if (!!cref.sids) { + cref.sids.forEach(s => { + ArrayFun.remove( + this.challenges, + c => c.type == "live" && c.from.sid == s, + "all" + ); + }); + } break; } case "game": //individual request @@ -702,11 +749,9 @@ export default { let newGame = game; newGame.type = this.classifyObject(game); newGame.vname = this.getVname(game.vid); - newGame.priority = 0; if (!game.score) // New game from Hall newGame.score = "*"; - if (newGame.score == "*") newGame.priority++; newGame.rids = [game.rid]; delete newGame["rid"]; this.games.push(newGame); @@ -727,10 +772,7 @@ export default { } case "result": { let g = this.games.find(g => g.id == data.gid); - if (!!g) { - g.score = data.score; - g.priority = 0; - } + if (!!g) g.score = data.score; break; } case "startgame": { @@ -765,6 +807,32 @@ export default { this.conn.addEventListener("close", this.socketCloseListener); }, // Challenge lifecycle: + addChallenge: function(chall) { + // NOTE about next condition: see "askchallenges" case. + if ( + !chall.to || + (this.people[chall.from].id > 0 && + (chall.from == this.st.user.sid || chall.to == this.st.user.name)) + ) { + let newChall = Object.assign({}, chall); + newChall.type = this.classifyObject(chall); + newChall.randomness = chall.randomness; + newChall.added = Date.now(); + let fromValues = Object.assign({}, this.people[chall.from]); + delete fromValues["pages"]; //irrelevant in this context + newChall.from = Object.assign({ sid: chall.from }, fromValues); + newChall.vname = this.getVname(newChall.vid); + this.challenges.push(newChall); + if ( + (newChall.type == "live" && this.cdisplay == "corr") || + (newChall.type == "corr" && this.cdisplay == "live") + ) { + document + .getElementById("btnC" + newChall.type) + .classList.add("somethingnew"); + } + } + }, loadNewchallVariant: async function(cb) { const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); @@ -792,7 +860,14 @@ export default { }); } }, - newChallenge: async function() { + newChallFromPreset(pchall) { + this.partialResetNewchallenge(); + this.newchallenge.vid = pchall.vid; + this.newchallenge.cadence = pchall.cadence; + this.newchallenge.randomness = pchall.randomness; + this.issueNewChallenge(); + }, + issueNewChallenge: async function() { if (!!(this.newchallenge.cadence.match(/^[0-9]+$/))) this.newchallenge.cadence += "+0"; //assume minutes, no increment const ctype = this.classifyObject(this.newchallenge); @@ -823,27 +898,52 @@ export default { } // NOTE: "from" information is not required here let chall = Object.assign({}, this.newchallenge); + // Add only if not already issued (not counting target or FEN): + if (this.challenges.some(c => + (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id) && + c.vid == chall.vid && + c.cadence == chall.cadence && + c.randomness == chall.randomness + )) { + alert(this.st.tr["Challenge already exists"]); + return; + } + if (this.newchallenge.memorize) this.addPresetChall(this.newchallenge); delete chall["V"]; delete chall["diag"]; const finishAddChallenge = cid => { chall.id = cid || "c" + getRandString(); - // Remove old challenge if any (only one at a time of a given type): - const cIdx = this.challenges.findIndex( - c => - (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id) && - c.type == ctype - ); - if (cIdx >= 0) { - // Delete current challenge (will be replaced now) - this.send("deletechallenge", { data: this.challenges[cIdx].id }); + const MAX_ALLOWED_CHALLS = 3; + // Remove oldest challenge if 3 found: only 3 at a time of a given type + let countMyChalls = 0; + let challToDelIdx = 0; + let oldestAdded = Number.MAX_SAFE_INTEGER; + for (let i=0; i<this.challenges.length; i++) { + const c = this.challenges[i]; + if ( + c.type == ctype && + (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id) + ) { + countMyChalls++; + if (c.added < oldestAdded) { + challToDelIdx = i; + oldestAdded = c.added; + } + } + } + if (countMyChalls >= MAX_ALLOWED_CHALLS) { + this.send( + "deletechallenge_s", + { data: { cid: this.challenges[challToDelIdx].id } } + ); if (ctype == "corr") { ajax( "/challenges", "DELETE", - { data: { id: this.challenges[cIdx].id } } + { data: { id: this.challenges[challToDelIdx].id } } ); } - this.challenges.splice(cIdx, 1); + this.challenges.splice(challToDelIdx, 1); } this.send("newchallenge", { data: Object.assign({ from: this.st.user.sid }, chall) @@ -906,6 +1006,12 @@ export default { name: this.st.user.name }; this.launchGame(c); + if (c.type == "live") + // Remove all live challenges of both players + this.send("deletechallenge_s", { data: { sids: [c.from.sid, c.seat.sid] } }); + else + // Corr challenge: just remove the challenge + this.send("deletechallenge_s", { data: { cid: c.id } }); } else { const oppsid = this.getOppsid(c); if (!!oppsid) @@ -917,8 +1023,8 @@ export default { { data: { id: c.id } } ); } + this.send("deletechallenge_s", { data: { cid: c.id } }); } - this.send("deletechallenge", { data: c.id }); }, // TODO: if several players click same challenge at the same time: problem clickChallenge: async function(c) { @@ -957,7 +1063,7 @@ export default { { data: { id: c.id } } ); } - this.send("deletechallenge", { data: c.id }); + this.send("deletechallenge_s", { data: { cid: c.id } }); } // In all cases, the challenge is consumed: ArrayFun.remove(this.challenges, ch => ch.id == c.id); @@ -1150,4 +1256,18 @@ button.refuseBtn @media screen and (max-width: 767px) #div2, #div3 margin-top: 0 + +tr > td + &.random-0 + background-color: #FF5733 + &.random-1 + background-color: #2B63B4 + &.random-2 + background-color: #33B42B + +td.remove-preset + background-color: lightgrey + text-align: center + & > img + height: 1em </style> diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 88b69065..5faae8a5 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -114,21 +114,19 @@ export default { .classList.add("somethingnew"); } }, - // Called at loading to augment games with priority + myTurn infos + // Called at loading to augment games with myColor + myTurn infos decorate: function(games) { games.forEach(g => { - g.priority = 0; + // If game is over, myColor and myTurn are ignored: if (g.score == "*") { - g.priority++; - const myColor = + g.myColor = (g.type == "corr" && g.players[0].uid == this.st.user.id) || (g.type == "live" && g.players[0].sid == this.st.user.sid) ? 'w' : 'b'; const rem = g.movesCount % 2; - if ((rem == 0 && myColor == 'w') || (rem == 1 && myColor == 'b')) { + if ((rem == 0 && g.myColor == 'w') || (rem == 1 && g.myColor == 'b')) { g.myTurn = true; - g.priority++; } } }); @@ -148,11 +146,7 @@ export default { // "notifything" --> "thing": const thing = data.code.substr(6); game[thing] = info[thing]; - if (thing == "score") game.priority = 0; - else { - game.priority = 3 - game.priority; //toggle turn - game.myTurn = !game.myTurn; - } + if (thing == "turn") game.myTurn = !game.myTurn; this.$forceUpdate(); this.tryShowNewsIndicator(type); break; @@ -173,15 +167,9 @@ export default { }, gameInfo ); - // Compute priority: - game.priority = 1; //at least: my running game - if ( + game.myTurn = (type == "corr" && game.players[0].uid == this.st.user.id) || - (type == "live" && game.players[0].sid == this.st.user.sid) - ) { - game.priority++; - game.myTurn = true; - } + (type == "live" && game.players[0].sid == this.st.user.sid); gamesArrays[type].push(game); this.$forceUpdate(); this.tryShowNewsIndicator(type); diff --git a/server/sockets.js b/server/sockets.js index d96b6967..c8f3c7f8 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -165,7 +165,7 @@ module.exports = function(wss) { // but the requested resource can be from any tmpId (except current!) case "askidentity": case "asklastate": - case "askchallenge": + case "askchallenges": case "askgame": case "askfullgame": { const pg = obj.page || page; //required for askidentity and askgame @@ -201,7 +201,7 @@ module.exports = function(wss) { // Notify all room: mostly game events case "newchat": case "newchallenge": - case "deletechallenge": + case "deletechallenge_s": case "newgame": case "resign": case "abort": @@ -297,7 +297,7 @@ module.exports = function(wss) { // Passing, relaying something: from isn't needed, // but target is fully identified (sid + tmpId) - case "challenge": + case "challenges": case "fullgame": case "game": case "identity": -- 2.44.0