-styling: connection indicator, names, put movesList + chat in better positions
-complete translations, stylesheets, variants rules ...
+au moins l'échange des coups en P2P ? et game chat ?
+surligner "hall" (menu) si nouveau défi perso (reçu) et pas affichage courant
+de même surligner "my games" si c'est à nous de jouer dans une partie (corr)
+
+Click elsewhere make modal disappear (for now: Esc key works...)
+
+Use better-sqlite3 instead of node-sqlite3:
+https://www.npmjs.com/package/better-sqlite3
+
+Canvas for hexagonal board Vue reactivity :
+https://stackoverflow.com/questions/40177493/drawing-onto-a-canvas-with-vue-js
+custom directives ?
+
+Desktop notifications:
+https://developer.mozilla.org/fr/docs/Web/API/notification
+
+Think about this:
+https://alligator.io/vuejs/component-communication/
+https://alligator.io/vuejs/global-event-bus/
+
+Dans variant page, "mes parties" peut toujours contenir corr + importées (deux onglets)
+En fin de partie (observée ou non), bouton "import game" en + de "download game" ==> directement dans indexedDB
+les parties par correspondance survivent 7 jours après la fin de partie
+
+mat en 2 échiqueté : brnkr3/pppp1p1p/4ps2/8/2P2P2/P1qP4/2c1s1PP/R1K5
+(Bb3+ Kb1 Ba2#)
+
+Importer des parties : nécessite de parser le PGN produit (possible, un peu pénible)
+
+espagnol : jugada ou movimiento ?
+fin de la partida au lieu de final de partida ?
+
+Mode new game contre un ami comme sur lichess ?
+
+Coordonnées sur échiquier: sur cases, à gauche (verticale) ou en bas (horizontale)
+
+Import game : en local dans indexedDb, affichage dans "Games --> Imported"
+
+Hexachess: McCooey et Shafran (deux tailles, randomisation OK)
+http://www.math.bas.bg/~iad/tyalie/shegra/shegrax.html
+http://www.quadibloc.com/chess/ch0401.htm
+
+Inspiration for refactor:
+https://github.com/triestpa/Vue-Chess/blob/master/src/components/chessboard/chessboard.js
+https://github.com/gustaYo/vue-chess
+++ /dev/null
---> correspondance: stocker sur serveur lastMove + uid + color + movesCount + gameId + variant + timeleft
-fin de partie corr: supprimer partie du serveur au bout de 7 jours (arbitraire)
-// TODO: au moins l'échange des coups en P2P ? et game chat ?
-
-// TODO: surligner "hall" (menu) si nouveau défi perso (reçu) et pas affichage courant
-// de même surligner "my games" si c'est à nous de jouer dans une partie (corr)
-// ==> myGames componentn + Game component must listen for "new move" events
-
-Hall + problems : similar pages, with "New game[problem]" button
-with a list of variants.
---> but display all challenges (and all problems)
-Possible filter: write a few variant names, to keep only these.
---> In settings !
-
-Use better-sqlite3 instead of node-sqlite3:
-https://www.npmjs.com/package/better-sqlite3
-
-Canvas for hexagonal board Vue reactivity :
-https://stackoverflow.com/questions/40177493/drawing-onto-a-canvas-with-vue-js
-custom directives ?
-
-Desktop notifications:
-https://developer.mozilla.org/fr/docs/Web/API/notification
-
-Think about this:
-https://alligator.io/vuejs/component-communication/
-https://alligator.io/vuejs/global-event-bus/
-
-CRON task remove unlogged users, finished corr games after 7 days, individual challenges older than 7 days
-
-tell opponent that I got the move, for him to start timer (and lose...)
- --> no, not needed and impossible if everybody is offline
- ==> just store this time locally (cheating possible but...)
-board2, board3, board4
-VariantRules2, 3 et 4 aussi
-fetch challenges and corr games from server at startup (room)
-but forbid anonymous to start corr games or accept challenges
-
-Dans variant page, "mes parties" peut toujours contenir corr + importées (deux onglets)
-En fin de partie (observée ou non), bouton "import game" en + de "download game" ==> directement dans indexedDB
---> sursis de 7 jours pour les parties par correspondance, qui sont encore chargées depuis le serveur
-
-mat en 2 échiqueté : brnkr3/pppp1p1p/4ps2/8/2P2P2/P1qP4/2c1s1PP/R1K5
-(Bb3+ Kb1 Ba2#)
-
-// TODO: decodeURIComponent() for GET/DELETE parameters
-
-2) Integrate computer play into rules tab
-3) Allow correspondance play (no need for P2P: online moves through the server (which also store them))
-4) Write my-games tab (included current/finished/imported)
- Use Dexie.js, or anything to store games locally
-5) Write room tab
- Use this: https://github.com/feross/simple-peer for online games+challenges+chat
-6) Test... and publish
-
-Finish rules translation in Spanish + improve existing ones
-Design: final touch (gain extra space on top, using space on the right)
-Crazyhouse: center reserves, grey if zero available, numbers superimposed
-Promotions: increase pieces sizes, better background.
-Code: use two spaces instead of tabs, everywhere.
-Increase code line length to 100 or more?
-(http://katafrakt.me/2017/09/16/80-characters-line-length-limit/)
-Chat button should be more apparent after game ends (color ?)
-Reinforce security for problems upload (how ?)
-
-Later:
-Let choice of time control, allow correspondance play, several corr games at the same time
-==> need to use indexedDB instead of localStorage. Maybe with Dexie https://dexie.org/
-Each user would have a unique identifier stored in the client DB.
-Allow to cancel games (if opponent doesn't connect again)
-Live games storage would be browser-based: different games on smartphone, home computer, work computer... (why not ?)
-==> (at most 1) running, and finished (which can be deleted from local memory)
-Allow challenging a specific player (by his chosen name)
-But keep the random pairings as main playing way + always playing in ZEN mode
-
-style menu : surligner onglet courant
-
-Interface :
- - newGame: une modalBox à paramètres, timeControl, type d'adversaire ==> "new Game")
-
-Importer des parties : nécessite de parser le PGN produit (possible, un peu pénible)
-mais permettrait mode analyse (avec bouton "analyse", comme sur ancien site).
-
-espagnol : jugada ou movimiento ?
-fin de la partida au lieu de final de partida ?
-
-Bouton new game ==> human only. Indiquer adversaire (éventuellement), cadence (ou "infini")
-Mode analyse : accessible à tout moment d'une partie (HH, ou computer) terminée + bouton "analyze from here" (sur parties observées)
-
-Coordonnées sur échiquier: sur cases, à gauche (verticale) ou en bas (horizontale)
-
-Import game : en local dans indexedDb, affichage dans "Games --> Imported"
-
-Checkered : si intervention d'un 3eme joueur, initialiser son temps à la moyenne des temps restants des deux autres... ?
-
-Mode contre ordinateur : seulement accessible depuis onglet "Rules" (son principal intérêt)
-
-Hexachess: McCooey et Shafran (deux tailles, randomisation OK)
-http://www.math.bas.bg/~iad/tyalie/shegra/shegrax.html
-http://www.quadibloc.com/chess/ch0401.htm
-
-Inspiration for refactor:
-https://github.com/triestpa/Vue-Chess/blob/master/src/components/chessboard/chessboard.js
-https://github.com/gustaYo/vue-chess
h3#eogMessage.section {{ endgameMessage }}
.row
#boardContainer.col-sm-12.col-md-9
- Board(:vr="vr" :last-move="lastMove" :analyze="analyze"
+ Board(:vr="vr" :last-move="lastMove" :analyze="game.mode=='analyze'"
:user-color="game.mycolor" :orientation="orientation"
:vname="game.vname" @play-move="play")
#controls
button(@click="flip") ⇅
button(@click="() => play()") >
button(@click="gotoEnd") >>
- #fenDiv(v-if="showFen && !!vr")
- p(@click="gotoFenContent") {{ vr.getFen() }}
#pgnDiv
a#download(href="#")
button(@click="download") {{ st.tr["Download PGN"] }}
+ button(v-if="game.mode!='analyze'" @click="analyzePosition")
+ | {{ st.tr["Analyze"] }}
.col-sm-12.col-md-3
MoveList(v-if="showMoves" :score="game.score" :message="game.scoreMsg"
:moves="moves" :cursor="cursor" @goto-move="gotoMove")
this.re_setVariables();
},
// Received a new move to play:
- "game.moveToPlay": function() {
- this.play(this.game.moveToPlay, "receive", this.game.vname=="Dark");
+ "game.moveToPlay": function(newMove) {
+ if (!!newMove) //if stop + launch new game, get undefined move
+ this.play(newMove, "receive");
},
},
computed: {
showMoves: function() {
- return true;
- //return window.innerWidth >= 768;
- },
- showFen: function() {
- return this.game.vname != "Dark" || this.game.score != "*";
- },
- analyze: function() {
- return this.game.mode == "analyze" || this.game.score != "*";
+ return this.game.vname != "Dark" || this.game.mode=="analyze";
},
},
created: function() {
this.cursor = L-1;
this.lastMove = (L > 0 ? this.moves[L-1] : null);
},
- gotoFenContent: function(event) {
- const newUrl = "#/analyze/" + this.game.vname +
- "/?fen=" + event.target.innerText.replace(/ /g, "_");
- window.open(newUrl); //to open in a new tab
+ analyzePosition: function() {
+ const newUrl = "/analyze/" + this.game.vname +
+ "/?fen=" + this.vr.getFen().replace(/ /g, "_");
+ //window.open("#" + newUrl); //to open in a new tab
+ this.$router.push(newUrl); //better
},
download: function() {
const content = this.getPgn();
modalBox.checked = true;
setTimeout(() => { modalBox.checked = false; }, 2000);
},
- animateMove: function(move) {
+ animateMove: function(move, callback) {
let startSquare = document.getElementById(getSquareId(move.start));
let endSquare = document.getElementById(getSquareId(move.end));
let rectStart = startSquare.getBoundingClientRect();
for (let i=0; i<squares.length; i++)
squares.item(i).style.zIndex = "auto";
movingPiece.style = {}; //required e.g. for 0-0 with KR swap
- this.play(move);
+ callback();
}, 250);
},
- play: function(move, receive, noanimate) {
+ play: function(move, receive) {
+ // NOTE: navigate and receive are mutually exclusive
const navigate = !move;
- // Forbid playing outside analyze mode when cursor isn't at moves.length-1
- // (except if we receive opponent's move, human or computer)
- if (!navigate && !this.analyze && !receive
+ // Forbid playing outside analyze mode, except if move is received.
+ // Sufficient condition because Board already knows which turn it is.
+ if (!navigate && this.game.mode!="analyze" && !receive
&& this.cursor < this.moves.length-1)
{
return;
}
- if (navigate)
- {
- if (this.cursor == this.moves.length-1)
- return; //no more moves
- move = this.moves[this.cursor+1];
- }
- if (!!receive && !noanimate) //opponent move, variant != "Dark"
- {
- if (this.cursor < this.moves.length-1)
+ const doPlayMove = () => {
+ if (!!receive && this.cursor < this.moves.length-1)
this.gotoEnd(); //required to play the move
- return this.animateMove(move);
- }
- if (!navigate)
- {
- move.color = this.vr.turn;
- move.notation = this.vr.getNotation(move);
- }
- // Not programmatic, or animation is over
- this.vr.play(move);
- this.cursor++;
- this.lastMove = move;
- if (this.st.settings.sound == 2)
- new Audio("/sounds/move.mp3").play().catch(err => {});
- if (!navigate)
- {
- move.fen = this.vr.getFen();
- if (this.game.score == "*" || this.analyze)
+ if (navigate)
+ {
+ if (this.cursor == this.moves.length-1)
+ return; //no more moves
+ move = this.moves[this.cursor+1];
+ }
+ else
{
+ move.color = this.vr.turn;
+ move.notation = this.vr.getNotation(move);
+ }
+ this.vr.play(move);
+ this.cursor++;
+ this.lastMove = move;
+ if (this.st.settings.sound == 2)
+ new Audio("/sounds/move.mp3").play().catch(err => {});
+ if (!navigate)
+ {
+ move.fen = this.vr.getFen();
// Stack move on movesList at current cursor
if (this.cursor == this.moves.length)
this.moves.push(move);
else
this.moves = this.moves.slice(0,this.cursor).concat([move]);
}
- }
- if (!this.analyze)
- this.$emit("newmove", move); //post-processing (e.g. computer play)
- // Is opponent in check?
- this.incheck = this.vr.getCheckSquares(this.vr.turn);
- const score = this.vr.getCurrentScore();
- if (score != "*")
- {
- const message = this.getScoreMessage(score);
- if (!this.analyze)
- this.$emit("gameover", score, message);
- else //just show score on screen (allow undo)
- this.showEndgameMsg(score + " . " + message);
- }
+ if (this.game.mode != "analyze")
+ this.$emit("newmove", move); //post-processing (e.g. computer play)
+ // Is opponent in check?
+ this.incheck = this.vr.getCheckSquares(this.vr.turn);
+ const score = this.vr.getCurrentScore();
+ if (score != "*")
+ {
+ const message = this.getScoreMessage(score);
+ if (this.game.mode != "analyze")
+ this.$emit("gameover", score, message);
+ else //just show score on screen (allow undo)
+ this.showEndgameMsg(score + " . " + message);
+ }
+ };
+ if (!!receive && this.game.vname != "Dark")
+ this.animateMove(move, doPlayMove);
+ else
+ doPlayMove();
},
undo: function(move) {
const navigate = !move;
<style lang="sass">
#modal-eog+div .card
overflow: hidden
-#pgnDiv, #fenDiv
+#pgnDiv
text-align: center
margin-left: auto
margin-right: auto
chatInput.value = "";
const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous",
sid:this.st.user.sid};
+ this.$emit("newchat", chat); //useful for corr games
this.chats.unshift(chat);
this.st.conn.send(JSON.stringify({
code:"newchat", msg:chatTxt, name:chat.name}));
BaseGame,
},
// gameInfo: fen + mode + vname
- // mode: "auto" (game comp vs comp), "versus" (normal) or "analyze"
+ // mode: "auto" (game comp vs comp) or "versus" (normal)
props: ["gameInfo"],
data: function() {
return {
if (newScore != "*")
{
this.game.score = newScore; //user action
- this.game.mode = "analyze";
if (!this.compThink)
this.$emit("game-stopped"); //otherwise wait for comp
}
let moveIdx = 0;
let self = this;
(function executeMove() {
- self.$refs.basegame.play(compMove[moveIdx++], animate);
+ self.$set(self.game, "moveToPlay", compMove[moveIdx++]);
if (moveIdx >= compMove.length)
{
self.compThink = false;
gameOver: function(score, scoreMsg) {
this.game.score = score;
this.game.scoreMsg = scoreMsg;
- this.game.mode = "analyze";
this.$emit("game-over", score); //bubble up to Rules.vue
},
},
if (rows.length > 0)
{
rows[Math.floor(newValue/2)].scrollIntoView({
- behavior: 'smooth',
- block: 'center'
+ behavior: "auto",
+ block: "nearest",
});
}
});
},
created: function() {
window.doClick = (elemId) => { document.getElementById(elemId).click() };
+ document.addEventListener("keydown", (e) => {
+ if (e.code === "Escape")
+ {
+ let modalBoxes = document.querySelectorAll("[id^='modal']");
+ modalBoxes.forEach(m => {
+ if (m.checked)
+ m.checked = false;
+ });
+ }
+ });
// TODO: why is this wrong?
//store.initialize(this.$route.path);
store.initialize(window.location.href.split("#")[1]);
"Type here": "Type here",
"Send": "Send",
"Download PGN": "Download PGN",
+ "Analyze": "Analyze",
"Cancel": "Cancel",
// Game page:
},
// TODO: also option to takeback a move ?
- update: function(gameId, obj) //move, fen, clocks, score, initime, ...
+ update: function(gameId, obj) //chat, move, fen, clocks, score, initime, ...
{
if (Number.isInteger(gameId) || !isNaN(parseInt(gameId)))
{
gid: gameId,
newObj:
{
+ chat: obj.chat,
move: obj.move, //may be undefined...
fen: obj.fen,
- message: obj.message,
score: obj.score,
drawOffer: obj.drawOffer,
}
<template lang="pug">
main
+ .row
+ .col-sm-12
+ #fenDiv(v-if="!!vr") {{ vr.getFen() }}
.row
.col-sm-12.col-md-10.col-md-offset-1
BaseGame(:game="game" :vr="vr" ref="basegame")
main
.row
#chat.col-sm-12.col-md-4.col-md-offset-4
- Chat(:players="game.players")
+ Chat(:players="game.players" @newchat="processChat")
.row
.col-sm-12
#actions(v-if="game.mode!='analyze' && game.score=='*'")
button(@click="resign") Resign
div Names: {{ game.players[0].name }} - {{ game.players[1].name }}
div(v-if="game.score=='*'") Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }}
- div(v-if="game.type=='corr'") {{ game.corrMsg }}
- textarea(v-if="game.score=='*'" v-model="corrMsg")
BaseGame(:game="game" :vr="vr" ref="basegame"
@newmove="processMove" @gameover="gameOver")
</template>
rid: ""
},
game: {players:[{name:""},{name:""}]}, //passed to BaseGame
- corrMsg: "", //to send offline messages in corr games
virtualClocks: [0, 0], //initialized with true game.clocks
vr: null, //"variant rules" object initialized from FEN
drawOffer: "", //TODO: use for button style
game:myGame, target:data.from}));
break;
case "newmove":
- this.corrMsg = data.move.message; //may be empty
this.$set(this.game, "moveToPlay", data.move); //TODO: Vue3...
break;
case "lastate": //got opponent infos about last move
vanish: s.vanish,
start: s.start,
end: s.end,
- message: m.message,
};
});
}
addTime = this.game.increment - elapsed/1000;
}
let sendMove = Object.assign({}, filtered_move, {addTime: addTime});
- if (this.game.type == "corr")
- sendMove.message = this.corrMsg;
this.people.forEach(p => {
if (p.sid != this.st.user.sid)
{
GameStorage.update(this.gameRef.id,
{
fen: move.fen,
- message: this.corrMsg,
move:
{
squares: filtered_move,
if (this.repeat[repIdx] >= 3)
this.drawOffer = "received"; //TODO: will print "mutual agreement"...
},
+ processChat: function(chat) {
+ if (this.game.type == "corr")
+ GameStorage.update(this.gameRef.id, {chat: chat});
+ },
gameOver: function(score, scoreMsg) {
this.game.mode = "analyze";
this.game.score = score;
foreign key (vid) references Variants(id)
);
+create table Chats (
+ gid integer,
+ uid integer,
+);
+
-- Store informations about players in a corr game
create table Players (
gid integer,
create table Moves (
gid integer,
squares varchar, --description, appear/vanish/from/to
- message varchar,
played datetime, --when was this move played?
idx integer, --index of the move in the game
foreign key (gid) references Games(id)
});
},
- // obj can have fields move, fen, drawOffer and/or score
+ // obj can have fields move, message, fen, drawOffer and/or score
update: function(id, obj)
{
db.parallelize(function() {
let query =
"UPDATE Games " +
"SET ";
+ if (!!obj.message)
+ query += "message = message || ' ' || '" + obj.message + "',";
if (!!obj.drawOffer)
query += "drawOffer = " + obj.drawOffer + ",";
if (!!obj.fen)
{
const m = obj.move;
query =
- "INSERT INTO Moves (gid, squares, message, played, idx) VALUES " +
- "(" + id + ",'" + JSON.stringify(m.squares) + "','" + m.message +
- "'," + m.played + "," + m.idx + ")";
+ "INSERT INTO Moves (gid, squares, played, idx) VALUES " +
+ "(" + id + ",'" + JSON.stringify(m.squares) + "',"
+ + m.played + "," + m.idx + ")";
db.run(query);
}
});