From: Benjamin Auder Date: Fri, 18 Jan 2019 18:24:27 +0000 (+0100) Subject: Start server implementation for correspondance play (early debug stage) X-Git-Url: https://git.auder.net/doc/html/%7B%7B%20path%28%27fos_user_security_logout%27%29%20%7D%7D?a=commitdiff_plain;h=ab4f4bf258ed68d8292b64d81babde03cddbae3c;p=vchess.git Start server implementation for correspondance play (early debug stage) --- diff --git a/_tmp/TODO b/_tmp/TODO index 868d4d3e..0717e496 100644 --- a/_tmp/TODO +++ b/_tmp/TODO @@ -1,6 +1,17 @@ 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...) @@ -73,3 +84,7 @@ Mode contre ordinateur : seulement accessible depuis onglet "Rules" (son princip 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 diff --git a/db/create.sql b/db/create.sql index 75e1b1cd..936992ce 100644 --- a/db/create.sql +++ b/db/create.sql @@ -37,6 +37,9 @@ create table Challenges ( uid integer, vid integer, nbPlayers integer, + fen varchar, + mainTime integer, + addTime integer, foreign key (uid) references Users(id), foreign key (vid) references Variants(id) ); @@ -52,8 +55,11 @@ create table WillPlay ( create table Games ( id integer primary key, vid integer, - fen varchar, --initial position - score varchar default '*', + fenStart varchar, --initial state + fen varchar, --current state + score varchar, + mainTime integer, + addTime integer, foreign key (vid) references Variants(id) ); @@ -62,6 +68,7 @@ create table Players ( gid integer, uid integer, color character, + rtime integer, --remaining time in milliseconds foreign key (gid) references Games(id), foreign key (uid) references Users(id) ); @@ -69,6 +76,7 @@ create table Players ( create table Moves ( gid integer, move varchar, + message varchar, played datetime, --when was this move played? idx integer, --index of the move in the game color character, --required for e.g. Marseillais Chess diff --git a/models/Challenge.js b/models/Challenge.js index adb40224..e945daa5 100644 --- a/models/Challenge.js +++ b/models/Challenge.js @@ -7,76 +7,113 @@ var db = require("../utils/database"); * uid: user id (int) * vid: variant id (int) * nbPlayers: integer + * fen: varchar (optional) + * mainTime: integer + * addTime: integer * * Structure table WillPlay: * cid: ref challenge id * uid: ref user id */ -exports.create = function(uid, vid, nbPlayers, cb) +const ChallengeModel = { - db.serialize({ - let query = - "INSERT INTO Challenges (added, uid, vid, nbPlayers) " + - "VALUES (" + Date.now() + "," + uid + "," + vid + "," + nbPlayers + ")"; - db.run(insertQuery, err => { - if (!!err) - return cb(err); - db.get("SELECT last_insert_rowid() AS rowid", (err2,lastId) => { + // fen cannot be undefined; TODO: generate fen on server instead + create: function(c, cb) + { + db.serialize(function() { + let query = + "INSERT INTO Challenges (added, uid, vid, nbPlayers, fen, mainTime, addTime) " + + "VALUES (" + Date.now() + "," + c.uid + "," + c.vid + "," + c.nbPlayers + "," + + c.fen + "," + c.mainTime + "," + c.increment + ")"; + db.run(query, err => { + if (!!err) + return cb(err); + db.get("SELECT last_insert_rowid() AS rowid", (err2,lastId) => { + query = + "INSERT INTO WillPlay VALUES " + + "(" + lastId["rowid"] + "," + uid + ")"; + db.run(query, (err,ret) => { + cb(err, lastId); //all we need is the challenge ID + }); + }); + }); + }); + }, + + getOne: function(id, cb) + { + db.serialize(function() { + let query = + "SELECT * " + + "FROM Challenges c " + + "JOIN Variants v " + + " ON c.vid = v.id " + "WHERE id = " + id; + db.get(query, (err,challengeInfo) => { + if (!!err) + return cb(err); query = - "INSERT INTO WillPlay VALUES " + - "(" + lastId["rowid"] + "," + uid + ")"; - db.run(query, cb); + "SELECT w.uid AS id, u.name " + + "FROM WillPlay w " + + "JOIN Users u " + + " ON w.uid = u.id " + + "WHERE w.cid = " + id; + db.run(query, (err2,players) => { + if (!!err2) + return cb(err2); + const challenge = { + id: id, + vname: challengeInfo.name, + added: challengeInfo.added, + nbPlayers: challengeInfo.nbPlayers, + players: players, //currently in + fen: challengeInfo.fen, + mainTime: challengeInfo.mainTime, + increment: challengeInfo.addTime, + }; + return cb(null, challenge); }); }); }); - }); -} + }, -exports.getOne = function(id, cb) -{ - db.serialize(function() { - let query = - "SELECT * " + - "FROM Challenges c " + - "JOIN Variants v " + - " ON c.vid = v.id " - "WHERE id = " + id; - db.get(query, (err,challengeInfo) => { - if (!!err) - return cb(err); - query = - "SELECT w.uid AS id, u.name " + - "FROM WillPlay w " + - "JOIN Users u " + - " ON w.uid = u.id " + - "WHERE w.cid = " + id; - db.run(query, (err2,players) => { - if (!!err2) - return cb(err2); - const challenge = { - id: id, - vname: challengeInfo.name, - added: challengeInfo.added, - nbPlayers: challengeInfo.nbPlayers, - players: players, //currently in - }; - return cb(null, challenge); + getByUser: function(uid, cb) + { + db.serialize(function() { + const query = + "SELECT cid " + + "FROM WillPlay " + + "WHERE uid = " + uid; + db.run(query, (err,challIds) => { + if (!!err) + return cb(err); + let challenges = []; + challIds.forEach(cidRow => { + ChallengeModel.getOne(cidRow["cid"], (err2,chall) => { + if (!!err2) + return cb(err2); + challenges.push(chall); + }); + }); + return cb(null, challenges); }); }); - }); -} + }, -exports.remove = function(id) -{ - db.parallelize(function() { - let query = - "DELETE FROM Challenges " + - "WHERE id = " + id; - db.run(query); - query = - "DELETE FROM WillPlay " + - "WHERE cid = " + id; - db.run(query); - }); + remove: function(id) + { + db.parallelize(function() { + let query = + "DELETE FROM Challenges " + + "WHERE id = " + id; + db.run(query); + query = + "DELETE FROM WillPlay " + + "WHERE cid = " + id; + db.run(query); + }); + }, } + +module.exports = ChallengeModel; diff --git a/models/Game.js b/models/Game.js index 72f1ea89..23a74e81 100644 --- a/models/Game.js +++ b/models/Game.js @@ -4,102 +4,137 @@ var db = require("../utils/database"); * Structure table Games: * id: game id (int) * vid: integer (variant id) - * fen: varchar (initial position) + * fenStart: varchar (initial position) + * fen: varchar (current position) * mainTime: integer - * increment: integer + * addTime: integer (increment) * score: varchar (result) * * Structure table Players: * gid: ref game id * uid: ref user id * color: character + * rtime: real (remaining time) * * Structure table Moves: + * gid: ref game id * move: varchar (description) + * message: text * played: datetime * idx: integer * color: character */ -exports.create = function(vid, fen, mainTime, increment, players, cb) +const GameModel = { - db.serialize({ - let query = - "INSERT INTO Games (vid, fen, mainTime, increment) " + - "VALUES (" + vid + ",'" + fen + "'," + mainTime + "," + increment + ")"; - db.run(insertQuery, err => { - if (!!err) - return cb(err); - db.get("SELECT last_insert_rowid() AS rowid", (err2,lastId) => { - players.forEach(p => { - query = - "INSERT INTO Players VALUES " + - "(" + lastId["rowid"] + "," + p.id + "," + p.color + ")"; - db.run(query, cb); + // mainTime and increment in milliseconds + create: function(vid, fen, mainTime, increment, players, cb) + { + db.serialize({ + let query = + "INSERT INTO Games (vid, fen, mainTime, addTime) " + + "VALUES (" + vid + ",'" + fen + "'," + mainTime + "," + increment + ")"; + db.run(insertQuery, err => { + if (!!err) + return cb(err); + db.get("SELECT last_insert_rowid() AS rowid", (err2,lastId) => { + players.forEach(p => { + query = + "INSERT INTO Players VALUES " + + "(" + lastId["rowid"] + "," + p.id + "," + p.color + "," + mainTime + ")"; + db.run(query, cb); + }); }); }); }); - }); -} + }, -// TODO: queries here could be async, and wait for all to complete -exports.getOne = function(id, cb) -{ - db.serialize(function() { - let query = - "SELECT v.name AS vname, g.fen, g.score " + - "FROM Games g " + - "JOIN Variants v " + - " ON g.vid = v.id " - "WHERE id = " + id; - db.get(query, (err,gameInfo) => { - if (!!err) - return cb(err); - query = - "SELECT p.uid AS id, p.color, u.name " + - "FROM Players p " + - "JOIN Users u " + - " ON p.uid = u.id " + - "WHERE p.gid = " + id; - db.run(query, (err2,players) => { - if (!!err2) - return cb(err2); + // TODO: queries here could be async, and wait for all to complete + getOne: function(id, cb) + { + db.serialize(function() { + let query = + "SELECT v.name AS vname, g.fen, g.fenStart, g.score " + + "FROM Games g " + + "JOIN Variants v " + + " ON g.vid = v.id " + "WHERE id = " + id; + db.get(query, (err,gameInfo) => { + if (!!err) + return cb(err); query = - "SELECT move AS desc, played, idx, color " + - "FROM Moves " + - "WHERE gid = " + id; - db.run(query, (err3,moves) => { - if (!!err3) - return cb(err3); - const game = { - id: id, - vname: gameInfo.vname, - fen: gameInfo.fen, - score: gameInfo.score, - players: players, - moves: moves, - }; - return cb(null, game); + "SELECT p.uid AS id, p.color, p.rtime, u.name " + + "FROM Players p " + + "JOIN Users u " + + " ON p.uid = u.id " + + "WHERE p.gid = " + id; + db.run(query, (err2,players) => { + if (!!err2) + return cb(err2); + query = + "SELECT move AS desc, message, played, idx, color " + + "FROM Moves " + + "WHERE gid = " + id; + db.run(query, (err3,moves) => { + if (!!err3) + return cb(err3); + const game = { + id: id, + vname: gameInfo.vname, + fenStart: gameInfo.fenStart, + fen: gameInfo.fen, + score: gameInfo.score, + players: players, + moves: moves, + }; + return cb(null, game); + }); }); }); }); - }); -} + }, -exports.remove = function(id) -{ - db.parallelize(function() { - let query = - "DELETE FROM Games " + - "WHERE id = " + id; - db.run(query); - query = - "DELETE FROM Players " + - "WHERE gid = " + id; - db.run(query); - query = - "DELETE FROM Moves " + - "WHERE gid = " + id; - db.run(query); - }); + getByUser: function(uid, cb) + { + db.serialize(function() { + // Next query is fine because a player appear at most once in a game + const query = + "SELECT gid " + + "FROM Players " + + "WHERE uid = " + uid; + db.run(query, (err,gameIds) => { + if (!!err) + return cb(err); + let gameArray = []; + gameIds.forEach(gidRow => { + GameModel.getOne(gidRow["gid"], (err2,game) => { + if (!!err2) + return cb(err2); + gameArray.push(game); + }); + }); + return cb(null, gameArray); + }); + }); + }, + + remove: function(id) + { + db.parallelize(function() { + let query = + "DELETE FROM Games " + + "WHERE id = " + id; + db.run(query); + query = + "DELETE FROM Players " + + "WHERE gid = " + id; + db.run(query); + query = + "DELETE FROM Moves " + + "WHERE gid = " + id; + db.run(query); + }); + }, } + +module.exports = GameModel; diff --git a/models/Problem.js b/models/Problem.js index 8f3a302c..a1f99031 100644 --- a/models/Problem.js +++ b/models/Problem.js @@ -10,67 +10,72 @@ var db = require("../utils/database"); * solution: text */ -exports.create = function(uid, vid, fen, instructions, solution, cb) +const ProblemModel = { - db.serialize(function() { - const insertQuery = - "INSERT INTO Problems (added, uid, vid, fen, instructions, solution) " + - "VALUES (" + Date.now() + "," + uid + "," + vid + ",'" + fen + "',?,?)"; - db.run(insertQuery, [instructions, solution], err => { - if (!!err) - return cb(err); - db.get("SELECT last_insert_rowid() AS rowid", cb); + create: function(uid, vid, fen, instructions, solution, cb) + { + db.serialize(function() { + const insertQuery = + "INSERT INTO Problems (added, uid, vid, fen, instructions, solution) " + + "VALUES (" + Date.now() + "," + uid + "," + vid + ",'" + fen + "',?,?)"; + db.run(insertQuery, [instructions, solution], err => { + if (!!err) + return cb(err); + db.get("SELECT last_insert_rowid() AS rowid", cb); + }); }); - }); -} + }, -exports.getOne = function(id, callback) -{ - db.serialize(function() { - const query = - "SELECT * " + - "FROM Problems " + - "WHERE id = " + id; - db.get(query, callback); - }); -} + getOne: function(id, callback) + { + db.serialize(function() { + const query = + "SELECT * " + + "FROM Problems " + + "WHERE id = " + id; + db.get(query, callback); + }); + }, -exports.fetchN = function(vid, uid, type, directionStr, lastDt, MaxNbProblems, callback) -{ - db.serialize(function() { - let typeLine = ""; - if (uid > 0) - typeLine = "AND uid " + (type=="others" ? "!=" : "=") + " " + uid; - const query = - "SELECT * FROM Problems " + - "WHERE vid = " + vid + - " AND added " + directionStr + " " + lastDt + " " + typeLine + " " + - "ORDER BY added " + (directionStr=="<" ? "DESC " : "") + - "LIMIT " + MaxNbProblems; - db.all(query, callback); - }); -} + fetchN: function(vid, uid, type, directionStr, lastDt, MaxNbProblems, callback) + { + db.serialize(function() { + let typeLine = ""; + if (uid > 0) + typeLine = "AND uid " + (type=="others" ? "!=" : "=") + " " + uid; + const query = + "SELECT * FROM Problems " + + "WHERE vid = " + vid + + " AND added " + directionStr + " " + lastDt + " " + typeLine + " " + + "ORDER BY added " + (directionStr=="<" ? "DESC " : "") + + "LIMIT " + MaxNbProblems; + db.all(query, callback); + }); + }, -// TODO: update fails (but insert is OK) -exports.update = function(id, uid, fen, instructions, solution, cb) -{ - db.serialize(function() { - const query = - "UPDATE Problems SET " + - "fen = '" + fen + "', " + - "instructions = ?, " + - "solution = ? " + - "WHERE id = " + id + " AND uid = " + uid; - db.run(query, [instructions,solution], cb); - }); -} + // TODO: update fails (but insert is OK) + update: function(id, uid, fen, instructions, solution, cb) + { + db.serialize(function() { + const query = + "UPDATE Problems SET " + + "fen = '" + fen + "', " + + "instructions = ?, " + + "solution = ? " + + "WHERE id = " + id + " AND uid = " + uid; + db.run(query, [instructions,solution], cb); + }); + }, -exports.remove = function(id, uid) -{ - db.serialize(function() { - const query = - "DELETE FROM Problems " + - "WHERE id = " + id + " AND uid = " + uid; - db.run(query); - }); + remove: function(id, uid) + { + db.serialize(function() { + const query = + "DELETE FROM Problems " + + "WHERE id = " + id + " AND uid = " + uid; + db.run(query); + }); + }, } + +module.exports = ProblemModel; diff --git a/models/User.js b/models/User.js index 4b5c840a..a36ab683 100644 --- a/models/User.js +++ b/models/User.js @@ -14,83 +14,88 @@ var params = require("../config/parameters"); * notify: boolean (send email notifications for corr games) */ -// NOTE: parameters are already cleaned (in controller), thus no sanitization here -exports.create = function(name, email, notify, callback) +const UserModel = { - db.serialize(function() { - const insertQuery = - "INSERT INTO Users " + - "(name, email, notify) VALUES " + - "('" + name + "', '" + email + "', " + notify + ")"; - db.run(insertQuery, err => { - if (!!err) - return callback(err); - db.get("SELECT last_insert_rowid() AS rowid", callback); + // NOTE: parameters are already cleaned (in controller), thus no sanitization here + create: function(name, email, notify, callback) + { + db.serialize(function() { + const insertQuery = + "INSERT INTO Users " + + "(name, email, notify) VALUES " + + "('" + name + "', '" + email + "', " + notify + ")"; + db.run(insertQuery, err => { + if (!!err) + return callback(err); + db.get("SELECT last_insert_rowid() AS rowid", callback); + }); }); - }); -} - -// Find one user (by id, name, email, or token) -exports.getOne = function(by, value, cb) -{ - const delimiter = (typeof value === "string" ? "'" : ""); - db.serialize(function() { - const query = - "SELECT * " + - "FROM Users " + - "WHERE " + by + " = " + delimiter + value + delimiter; - db.get(query, cb); - }); -} + }, -///////// -// MODIFY + // Find one user (by id, name, email, or token) + getOne: function(by, value, cb) + { + const delimiter = (typeof value === "string" ? "'" : ""); + db.serialize(function() { + const query = + "SELECT * " + + "FROM Users " + + "WHERE " + by + " = " + delimiter + value + delimiter; + db.get(query, cb); + }); + }, -exports.setLoginToken = function(token, uid, cb) -{ - db.serialize(function() { - const query = - "UPDATE Users " + - "SET loginToken = '" + token + "', loginTime = " + Date.now() + " " + - "WHERE id = " + uid; - db.run(query, cb); - }); -} + ///////// + // MODIFY -// Set session token only if empty (first login) -// TODO: weaker security (but avoid to re-login everywhere after each logout) -exports.trySetSessionToken = function(uid, cb) -{ - // Also empty the login token to invalidate future attempts - db.serialize(function() { - const querySessionToken = - "SELECT sessionToken " + - "FROM Users " + - "WHERE id = " + uid; - db.get(querySessionToken, (err,ret) => { - if (!!err) - return cb(err); - const token = ret.sessionToken || genToken(params.token.length); - const queryUpdate = + setLoginToken: function(token, uid, cb) + { + db.serialize(function() { + const query = "UPDATE Users " + - "SET loginToken = NULL" + - (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + + "SET loginToken = '" + token + "', loginTime = " + Date.now() + " " + "WHERE id = " + uid; - db.run(queryUpdate); - cb(null, token); + db.run(query, cb); }); - }); -} + }, -exports.updateSettings = function(user, cb) -{ - db.serialize(function() { - const query = - "UPDATE Users " + - "SET name = '" + user.name + "'" + - ", email = '" + user.email + "'" + - ", notify = " + user.notify + " " + - "WHERE id = " + user.id; - db.run(query, cb); - }); + // Set session token only if empty (first login) + // TODO: weaker security (but avoid to re-login everywhere after each logout) + trySetSessionToken: function(uid, cb) + { + // Also empty the login token to invalidate future attempts + db.serialize(function() { + const querySessionToken = + "SELECT sessionToken " + + "FROM Users " + + "WHERE id = " + uid; + db.get(querySessionToken, (err,ret) => { + if (!!err) + return cb(err); + const token = ret.sessionToken || genToken(params.token.length); + const queryUpdate = + "UPDATE Users " + + "SET loginToken = NULL" + + (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + + "WHERE id = " + uid; + db.run(queryUpdate); + cb(null, token); + }); + }); + }, + + updateSettings: function(user, cb) + { + db.serialize(function() { + const query = + "UPDATE Users " + + "SET name = '" + user.name + "'" + + ", email = '" + user.email + "'" + + ", notify = " + user.notify + " " + + "WHERE id = " + user.id; + db.run(query, cb); + }); + }, } + +module.exports = UserModel; diff --git a/models/Variant.js b/models/Variant.js index 8d7eba25..233d938b 100644 --- a/models/Variant.js +++ b/models/Variant.js @@ -7,25 +7,30 @@ var db = require("../utils/database"); * description: varchar */ -exports.getByName = function(name, callback) +const VariantModel = { - db.serialize(function() { - const query = - "SELECT * " + - "FROM Variants " + - "WHERE name='" + name + "'"; - db.get(query, callback); - }); -} + getByName: function(name, callback) + { + db.serialize(function() { + const query = + "SELECT * " + + "FROM Variants " + + "WHERE name='" + name + "'"; + db.get(query, callback); + }); + }, -exports.getAll = function(callback) -{ - db.serialize(function() { - const query = - "SELECT * " + - "FROM Variants"; - db.all(query, callback); - }); + getAll: function(callback) + { + db.serialize(function() { + const query = + "SELECT * " + + "FROM Variants"; + db.all(query, callback); + }); + }, + + //create, update, delete: directly in DB } -//create, update, delete: directly in DB +module.exports = VariantModel; diff --git a/public/javascripts/components/correspondance.js b/public/javascripts/components/correspondance.js index 87de3eb8..35659c3f 100644 --- a/public/javascripts/components/correspondance.js +++ b/public/javascripts/components/correspondance.js @@ -1,11 +1,123 @@ Vue.component("my-correspondance", { + data: function() { + return { + userId: user.id, + games: [], + challenges: [], + willPlay: [], //IDs of challenges in which I decide to play (>= 3 players) + newgameInfo: { + fen: "", + vid: 0, + nbPlayers: 0, + players: ["","",""], + mainTime: 0, + increment: 0, + }, + }; + }, template: `
-

TODO: load from server, show timeControl + players + link "play"

-

Also tab for current challenges + button "new game"

+ +
+
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + + +
+
+
+ + +
+ +
+
+

Correspondance play is reserved to registered users

+
+ + + + + +
`, + computed: { + // TODO: this is very artificial... + variants: function() { + return variantArray; + }, + }, created: function() { - //TODO + // use user.id to load challenges + games from server + }, + methods: { + translate: translate, + clickChallenge: function() { + // TODO: accepter un challenge peut lancer une partie, il + // faut alors supprimer challenge + creer partie + la retourner et l'ajouter ici + // autres actions: + // supprime mon défi + // accepte un défi + // annule l'acceptation d'un défi (si >= 3 joueurs) + // + // si pas le mien et FEN speciale :: (charger code variante et) + // montrer diagramme + couleur (orienté) + }, + showGame: function(g) { + // Redirect to /variant#game?id=... + location.href="/variant#game?id=" + g.id; + }, + newGame: function() { + // NOTE: side-effect = set FEN + // TODO: (to avoid any cheating option) separate the GenRandInitFen() functions + // in separate files, load on server and generate FEN on server. + const error = checkChallenge(this.newgameInfo); + if (!!error) + return alert(error); + // Possible (server) error if filled player does not exist + ajax( + "/challenges/" + this.newgameInfo.vid, + "POST", + this.newgameInfo, + response => { + this.challenges.push(response.challenge); + } + ); + }, + possibleNbplayers: function(nbp) { + if (this.newgameInfo.vid == 0) + return false; + const idxInVariants = variantArray.findIndex(v => v.id == this.newgameInfo.vid); + return NbPlayers[variantArray[idxInVariants].name].includes(nbp); + }, }, }); diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index 0d9322e2..534d9af4 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -120,7 +120,7 @@ Vue.component('my-game', { this.loadGame(); else if (!!this.fen) { - this.vr = new VariantRules(this.fen); + this.vr = new V(this.fen); this.fenStart = this.fen; } // TODO: if I'm one of the players in game, then: @@ -296,7 +296,7 @@ Vue.component('my-game', { }, translate: translate, newGameFromFen: function() { - this.vr = new VariantRules(this.fen); + this.vr = new V(this.fen); this.moves = []; this.cursor = -1; this.fenStart = this.fen; @@ -532,12 +532,12 @@ Vue.component('my-game', { this.moves.pop(); }, gotoMove: function(index) { - this.vr = new VariantRules(this.moves[index].fen); + this.vr = new V(this.moves[index].fen); this.cursor = index; this.lastMove = this.moves[index]; }, gotoBegin: function() { - this.vr = new VariantRules(this.fenStart); + this.vr = new V(this.fenStart); this.cursor = -1; this.lastMove = null; }, diff --git a/public/javascripts/components/gameList.js b/public/javascripts/components/gameList.js index 9eb1f7d0..9b669826 100644 --- a/public/javascripts/components/gameList.js +++ b/public/javascripts/components/gameList.js @@ -1,18 +1,23 @@ Vue.component("my-game-list", { props: ["games"], computed: { + showVariant: function() { + return this.games.length > 0 && !!this.games[0].vname; + }, showResult: function() { - this.games.length > 0 && this.games[0].score != "*"; + return this.games.length > 0 && this.games[0].score != "*"; }, }, template: ` + + diff --git a/public/javascripts/components/room.js b/public/javascripts/components/room.js index 93990ce1..fbf02ee1 100644 --- a/public/javascripts/components/room.js +++ b/public/javascripts/components/room.js @@ -144,7 +144,7 @@ Vue.component('my-room', { }, // user: last person to accept the challenge newGame: function(chall, user) { - const fen = chall.fen || VariantRules.GenRandInitFen(); + const fen = chall.fen || V.GenRandInitFen(); const game = {}; //TODO: fen, players, time ... //setStorage(game); //TODO game.players.forEach(p => { diff --git a/public/javascripts/playCompMove.js b/public/javascripts/playCompMove.js index 86769337..45da5113 100644 --- a/public/javascripts/playCompMove.js +++ b/public/javascripts/playCompMove.js @@ -9,7 +9,7 @@ onmessage = function(e) '/javascripts/base_rules.js', '/javascripts/utils/array.js', '/javascripts/variants/' + e.data[1] + '.js'); - self.V = VariantRules; + self.V = eval(e.data[1] + "Rules"); break; case "init": const fen = e.data[1]; diff --git a/public/javascripts/shared/challengeCheck.js b/public/javascripts/shared/challengeCheck.js new file mode 100644 index 00000000..a5833f62 --- /dev/null +++ b/public/javascripts/shared/challengeCheck.js @@ -0,0 +1,66 @@ +function checkChallenge(c) +{ + const vid = parseInt(c.vid); + if (isNaN(vid) || vid <= 0) + return "Please select a variant"; + + const mainTime = parseInt(c.mainTime); + const increment = parseInt(c.increment); + if (isNaN(mainTime) || mainTime <= 0) + return "Main time should be strictly positive"; + if (isNaN(increment) || increment < 0) + return "Increment must be positive"; + + // Basic alphanumeric check for players names + let playerCount = 0; + for (p of c.players) + { + if (p.length > 0) + { + if (!p.match(/^[\w]+$/)) + return "Wrong characters in players names"; + playerCount++; + } + } + + if (playerCount > 0 && playerCount != c.nbPlayers) + return "None, or all of the opponent names must be filled" + + if (!!document) //client side + { + const idxInVariants = variantArray.findIndex(v => v.id == c.vid); + const vname = variantArray[idxInVariants].name; + const scriptId = vname + "RulesScript"; + const afterRulesAreLoaded = () => { + const V = eval(vname + "Rules"); + // Allow custom FEN (and check it) only for individual challenges + if (c.fen.length > 0 && playerCount > 0) + { + if (!V.IsGoodFen(c.fen)) + return "Bad FEN string"; + } + else + { + // Generate a FEN + c.fen = V.GenRandInitFen(); + } + }; + if (!document.getElementById(scriptId)) + { + // Load variant rules (only once) + var script = document.createElement("script"); + script.id = scriptId; + script.src = "/javascripts/variants/" + vname + ".js"; + document.body.appendChild(script); + script.onload = afterRulesAreLoaded; + } + } + else + { + // Just characters check on server: + if (!c.fen.match(/^[a-zA-Z0-9, /-]*$/)) + return "Bad FEN string"; + } +} + +try { module.exports = checkChallenge; } catch(e) { } //for server diff --git a/public/javascripts/shared/nbPlayers.js b/public/javascripts/shared/nbPlayers.js new file mode 100644 index 00000000..8c0cc864 --- /dev/null +++ b/public/javascripts/shared/nbPlayers.js @@ -0,0 +1,23 @@ +const NbPlayers = +{ + "Alice": [2,3,4], + "Antiking": [2,3,4], + "Atomic": [2,3,4], + "Baroque": [2,3,4], + "Berolina": [2,4], + "Checkered": [2,3,4], + "Chess960": [2,3,4], + "Crazyhouse": [2,3,4], + "Dark": [2,3,4], + "Extinction": [2,3,4], + "Grand": [2], + "Losers": [2,3,4], + "Magnetic": [2], + "Marseille": [2], + "Switching": [2,3,4], + "Upsidedown": [2], + "Wildebeest": [2], + "Zen": [2,3,4], +}; + +try { module.exports = NbPlayers; } catch (e) { } //for server diff --git a/public/javascripts/utils/printDiagram.js b/public/javascripts/utils/printDiagram.js index 9f7a0203..ba7b1e97 100644 --- a/public/javascripts/utils/printDiagram.js +++ b/public/javascripts/utils/printDiagram.js @@ -76,7 +76,7 @@ function getShadowArray(shadow) function getDiagram(args) { // Obtain the array of pieces images names: - const board = VariantRules.GetBoard(args.position); + const board = V.GetBoard(args.position); const orientation = args.orientation || "w"; const markArray = getMarkArray(args.marks); const shadowArray = getShadowArray(args.shadow); diff --git a/public/javascripts/variants/Alice.js b/public/javascripts/variants/Alice.js index 3a613585..04b4a346 100644 --- a/public/javascripts/variants/Alice.js +++ b/public/javascripts/variants/Alice.js @@ -350,5 +350,3 @@ class AliceRules extends ChessRules return notation; } } - -const VariantRules = AliceRules; diff --git a/public/javascripts/variants/Antiking.js b/public/javascripts/variants/Antiking.js index 45daa62b..66eb9f0a 100644 --- a/public/javascripts/variants/Antiking.js +++ b/public/javascripts/variants/Antiking.js @@ -198,5 +198,3 @@ class AntikingRules extends ChessRules " w 1111 -"; } } - -const VariantRules = AntikingRules; diff --git a/public/javascripts/variants/Atomic.js b/public/javascripts/variants/Atomic.js index 255444f2..87c98097 100644 --- a/public/javascripts/variants/Atomic.js +++ b/public/javascripts/variants/Atomic.js @@ -140,5 +140,3 @@ class AtomicRules extends ChessRules return color == "w" ? "0-1" : "1-0"; //checkmate } } - -const VariantRules = AtomicRules; diff --git a/public/javascripts/variants/Baroque.js b/public/javascripts/variants/Baroque.js index 38d4012c..d06b99c3 100644 --- a/public/javascripts/variants/Baroque.js +++ b/public/javascripts/variants/Baroque.js @@ -614,5 +614,3 @@ class BaroqueRules extends ChessRules return notation; } } - -const VariantRules = BaroqueRules; diff --git a/public/javascripts/variants/Berolina.js b/public/javascripts/variants/Berolina.js index 31630ab5..517a93e5 100644 --- a/public/javascripts/variants/Berolina.js +++ b/public/javascripts/variants/Berolina.js @@ -133,5 +133,3 @@ class BerolinaRules extends ChessRules return super.getNotation(move); //all other pieces are orthodox } } - -const VariantRules = BerolinaRules; diff --git a/public/javascripts/variants/Checkered.js b/public/javascripts/variants/Checkered.js index 6864673c..9de46ff1 100644 --- a/public/javascripts/variants/Checkered.js +++ b/public/javascripts/variants/Checkered.js @@ -299,5 +299,3 @@ class CheckeredRules extends ChessRules } } } - -const VariantRules = CheckeredRules; diff --git a/public/javascripts/variants/Chess960.js b/public/javascripts/variants/Chess960.js index 529ad997..1c8292ce 100644 --- a/public/javascripts/variants/Chess960.js +++ b/public/javascripts/variants/Chess960.js @@ -2,5 +2,3 @@ class Chess960Rules extends ChessRules { // Standard rules } - -const VariantRules = Chess960Rules; diff --git a/public/javascripts/variants/Crazyhouse.js b/public/javascripts/variants/Crazyhouse.js index 3d9c743f..179fffa5 100644 --- a/public/javascripts/variants/Crazyhouse.js +++ b/public/javascripts/variants/Crazyhouse.js @@ -281,5 +281,3 @@ class CrazyhouseRules extends ChessRules return "@" + V.CoordsToSquare(move.end); } } - -const VariantRules = CrazyhouseRules; diff --git a/public/javascripts/variants/Dark.js b/public/javascripts/variants/Dark.js index e1ab30dd..3879d9df 100644 --- a/public/javascripts/variants/Dark.js +++ b/public/javascripts/variants/Dark.js @@ -285,5 +285,3 @@ class DarkRules extends ChessRules return moves[_.sample(candidates, 1)]; } } - -const VariantRules = DarkRules; diff --git a/public/javascripts/variants/Extinction.js b/public/javascripts/variants/Extinction.js index 2f57260a..aab359f8 100644 --- a/public/javascripts/variants/Extinction.js +++ b/public/javascripts/variants/Extinction.js @@ -135,5 +135,3 @@ class ExtinctionRules extends ChessRules return super.evalPosition(); } } - -const VariantRules = ExtinctionRules; diff --git a/public/javascripts/variants/Grand.js b/public/javascripts/variants/Grand.js index 8d06f710..64088471 100644 --- a/public/javascripts/variants/Grand.js +++ b/public/javascripts/variants/Grand.js @@ -387,5 +387,3 @@ class GrandRules extends ChessRules " w 1111 - 00000000000000"; } } - -const VariantRules = GrandRules; diff --git a/public/javascripts/variants/Losers.js b/public/javascripts/variants/Losers.js index 9af6776c..33c81093 100644 --- a/public/javascripts/variants/Losers.js +++ b/public/javascripts/variants/Losers.js @@ -183,5 +183,3 @@ class LosersRules extends ChessRules " w -"; //no en-passant } } - -const VariantRules = LosersRules; diff --git a/public/javascripts/variants/Magnetic.js b/public/javascripts/variants/Magnetic.js index 8adb1bab..32279905 100644 --- a/public/javascripts/variants/Magnetic.js +++ b/public/javascripts/variants/Magnetic.js @@ -202,5 +202,3 @@ class MagneticRules extends ChessRules return 500; //checkmates evals may be slightly below 1000 } } - -const VariantRules = MagneticRules; diff --git a/public/javascripts/variants/Marseille.js b/public/javascripts/variants/Marseille.js index 7aa73c01..7d83bd55 100644 --- a/public/javascripts/variants/Marseille.js +++ b/public/javascripts/variants/Marseille.js @@ -293,5 +293,3 @@ class MarseilleRules extends ChessRules return selected; } } - -const VariantRules = MarseilleRules; diff --git a/public/javascripts/variants/Switching.js b/public/javascripts/variants/Switching.js index d898ff63..04bb110c 100644 --- a/public/javascripts/variants/Switching.js +++ b/public/javascripts/variants/Switching.js @@ -133,5 +133,3 @@ class SwitchingRules extends ChessRules return "S" + V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end); } } - -const VariantRules = SwitchingRules; diff --git a/public/javascripts/variants/Upsidedown.js b/public/javascripts/variants/Upsidedown.js index 3e389d0d..ecd1ff51 100644 --- a/public/javascripts/variants/Upsidedown.js +++ b/public/javascripts/variants/Upsidedown.js @@ -68,5 +68,3 @@ class UpsidedownRules extends ChessRules " w"; //no castle, no en-passant } } - -const VariantRules = UpsidedownRules; diff --git a/public/javascripts/variants/Wildebeest.js b/public/javascripts/variants/Wildebeest.js index 293b3b15..cb95318b 100644 --- a/public/javascripts/variants/Wildebeest.js +++ b/public/javascripts/variants/Wildebeest.js @@ -285,5 +285,3 @@ class WildebeestRules extends ChessRules " w 1111 -"; } } - -const VariantRules = WildebeestRules; diff --git a/public/javascripts/variants/Zen.js b/public/javascripts/variants/Zen.js index 7713b848..6a568db9 100644 --- a/public/javascripts/variants/Zen.js +++ b/public/javascripts/variants/Zen.js @@ -225,5 +225,3 @@ class ZenRules extends ChessRules } } } - -const VariantRules = ZenRules; diff --git a/routes/all.js b/routes/all.js index 85815a3b..f901a025 100644 --- a/routes/all.js +++ b/routes/all.js @@ -4,7 +4,7 @@ router.use("/", require("./index")); router.use("/", require("./users")); router.use("/", require("./messages")); //router.use("/", require("./games")); -//router.use("/", require("./challenge")); +router.use("/", require("./challenge")); router.use("/", require("./problems")); router.use("/", require("./variant")); diff --git a/routes/challenge.js b/routes/challenge.js index 1a4d22b8..2131870f 100644 --- a/routes/challenge.js +++ b/routes/challenge.js @@ -1,77 +1,81 @@ -// TODO: adapt this (from Mongo to SQLite, and challenge format changed) for corr play +// AJAX methods to get, create, update or delete a challenge -var router = require("express").Router(); -var ObjectID = require("bson-objectid"); -var ChallengeModel = require('../models/Challenge'); -var UserModel = require('../models/User'); -var ObjectID = require("bson-objectid"); -var access = require("../utils/access"); +let router = require("express").Router(); +const access = require("../utils/access"); +const ChallengeModel = require("../models/Challenge"); +const checkChallenge = require("../public/javascripts/shared/challengeCheck.js"); -// variant page -router.get("/challengesbyvariant", access.logged, access.ajax, (req,res) => { - if (req.query["uid"] != req.user._id) - return res.json({errmsg: "Not your challenges"}); - let uid = ObjectID(req.query["uid"]); - let vid = ObjectID(req.query["vid"]); - ChallengeModel.getByVariant(uid, vid, (err, challengeArray) => { - res.json(err || {challenges: challengeArray}); +router.post("/challenges/:vid([0-9]+)", access.logged, access.ajax, (req,res) => { + const vid = req.params["vid"]; + const chall = { + uid: req.userId, + vid: vid, + fen: req.body["fen"], + mainTime: req.body["mainTime"], + increment: req.body["increment"], + nbPlayers: req.body["nbPlayers"], + players: req.body["players"], + }; + const error = checkChallenge(chall); + ChallengeModel.create(chall, (err,lastId) => { + res.json(err || {challenge: Object.assign(chall, {id: lastId["rowid"]})}); }); }); -// index -router.get("/challengesbyplayer", access.logged, access.ajax, (req,res) => { - if (req.query["uid"] != req.user._id) - return res.json({errmsg: "Not your challenges"}); - let uid = ObjectID(req.query["uid"]); - ChallengeModel.getByPlayer(uid, (err, challengeArray) => { - res.json(err || {challenges: challengeArray}); - }); -}); - -function createChallenge(vid, from, to, res) -{ - ChallengeModel.create(vid, from, to, (err, chall) => { - res.json(err || { - // A challenge can be sent using only name, thus 'to' is returned - to: chall.to, - cid: chall._id - }); - }); -} - -// from[, to][,nameTo] -router.post("/challenges", access.logged, access.ajax, (req,res) => { - if (req.body.from != req.user._id) - return res.json({errmsg: "Identity usurpation"}); - let from = ObjectID(req.body.from); - let to = !!req.body.to ? ObjectID(req.body.to) : undefined; - let nameTo = !!req.body.nameTo ? req.body.nameTo : undefined; - let vid = ObjectID(req.body.vid); - if (!to && !!nameTo) - { - UserModel.getByName(nameTo, (err,user) => { - access.checkRequest(res, err, user, "Opponent not found", () => { - createChallenge(vid, from, user._id, res); - }); - }); - } - else if (!!to) - createChallenge(vid, from, to, res); - else - createChallenge(vid, from, undefined, res); //automatch -}); - -router.delete("/challenges", access.logged, access.ajax, (req,res) => { - let cid = ObjectID(req.query.cid); - ChallengeModel.getById(cid, (err,chall) => { - access.checkRequest(res, err, chall, "Challenge not found", () => { - if (!chall.from.equals(req.user._id) && !!chall.to && !chall.to.equals(req.user._id)) - return res.json({errmsg: "Not your challenge"}); - ChallengeModel.remove(cid, err => { - res.json(err || {}); - }); - }); - }); -}); +//// index +//router.get("/challengesbyplayer", access.logged, access.ajax, (req,res) => { +// if (req.query["uid"] != req.user._id) +// return res.json({errmsg: "Not your challenges"}); +// let uid = ObjectID(req.query["uid"]); +// ChallengeModel.getByPlayer(uid, (err, challengeArray) => { +// res.json(err || {challenges: challengeArray}); +// }); +//}); +// +//function createChallenge(vid, from, to, res) +//{ +// ChallengeModel.create(vid, from, to, (err, chall) => { +// res.json(err || { +// // A challenge can be sent using only name, thus 'to' is returned +// to: chall.to, +// cid: chall._id +// }); +// }); +//} +// +//// from[, to][,nameTo] +//router.post("/challenges", access.logged, access.ajax, (req,res) => { +// if (req.body.from != req.user._id) +// return res.json({errmsg: "Identity usurpation"}); +// let from = ObjectID(req.body.from); +// let to = !!req.body.to ? ObjectID(req.body.to) : undefined; +// let nameTo = !!req.body.nameTo ? req.body.nameTo : undefined; +// let vid = ObjectID(req.body.vid); +// if (!to && !!nameTo) +// { +// UserModel.getByName(nameTo, (err,user) => { +// access.checkRequest(res, err, user, "Opponent not found", () => { +// createChallenge(vid, from, user._id, res); +// }); +// }); +// } +// else if (!!to) +// createChallenge(vid, from, to, res); +// else +// createChallenge(vid, from, undefined, res); //automatch +//}); +// +//router.delete("/challenges", access.logged, access.ajax, (req,res) => { +// let cid = ObjectID(req.query.cid); +// ChallengeModel.getById(cid, (err,chall) => { +// access.checkRequest(res, err, chall, "Challenge not found", () => { +// if (!chall.from.equals(req.user._id) && !!chall.to && !chall.to.equals(req.user._id)) +// return res.json({errmsg: "Not your challenge"}); +// ChallengeModel.remove(cid, err => { +// res.json(err || {}); +// }); +// }); +// }); +//}); module.exports = router; diff --git a/views/index.pug b/views/index.pug index cecca5cf..6ea45166 100644 --- a/views/index.pug +++ b/views/index.pug @@ -22,10 +22,10 @@ block content #flagMenu.clickable(onClick="doClick('modalLang')") img(src="/images/flags/" + lang + ".svg") include userMenu - a.right-menu(v-show="display=='variants'" href="#games") + a.right-menu(v-show="display=='variants'" href="#correspondance") .info-container p Correspondance - a.right-menu(v-show="display=='games'" href="#variants") + a.right-menu(v-show="display=='correspondance'" href="#variants") .info-container p Variants .row(v-show="display=='variants'") @@ -43,5 +43,6 @@ block javascripts script(src="/javascripts/socket_url.js") script(src="/javascripts/components/variantSummary.js") script(src="/javascripts/components/gameList.js") + script(src="/javascripts/components/challengeList.js") script(src="/javascripts/components/correspondance.js") script(src="/javascripts/index.js") diff --git a/views/layout.pug b/views/layout.pug index 1e0f1751..b2b1d526 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -46,12 +46,16 @@ html script(src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js") script(src="/javascripts/utils/misc.js") script(src="/javascripts/utils/ajax.js") + script(src="/javascripts/utils/array.js") + script(src="/javascripts/base_rules.js") script(src="/javascripts/layout.js") script(src="/javascripts/contactForm.js") if development script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js") else script(src="https://cdn.jsdelivr.net/npm/vue") + script(src="/javascripts/shared/nbPlayers.js") + script(src="/javascripts/shared/challengeCheck.js") script(src="/javascripts/shared/userCheck.js") script(src="/javascripts/components/upsertUser.js") script. diff --git a/views/variant.pug b/views/variant.pug index c6303f3f..7e57bbf5 100644 --- a/views/variant.pug +++ b/views/variant.pug @@ -37,16 +37,15 @@ block content :mode="mode" :settings="settings" @game-over="archiveGame") block javascripts - script(src="/javascripts/utils/array.js") script(src="/javascripts/utils/printDiagram.js") script(src="/javascripts/utils/datetime.js") script(src="/javascripts/utils/squareId.js") script(src="/javascripts/socket_url.js") - script(src="/javascripts/base_rules.js") script(src="/javascripts/variants/" + variant.name + ".js") script. - const V = VariantRules; //because this variable is often used const variant = !{JSON.stringify(variant)}; + // Just 'V' because this variable is often used: + const V = eval(variant.name + "Rules"); script(src="/javascripts/components/board.js") script(src="/javascripts/components/chat.js") script(src="/javascripts/components/gameList.js")
Variant Players names Cadence Result
{{ g.vname }} {{ p.name }}