From: Benjamin Auder <benjamin.auder@somewhere>
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/%7B%7B%20asset%28%27mixstore/images/pieces/current/R.css?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: `
 		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
-			<p>TODO: load from server, show timeControl + players + link "play"</p>
-			<p>Also tab for current challenges + button "new game"</p>
+			<input id="modalNewgame" type="checkbox" class="modal"/>
+			<div role="dialog" aria-labelledby="titleFenedit">
+				<div class="card smallpad">
+					<label id="closeNewgame" for="modalNewgame" class="modal-close">
+					</label>
+					<fieldset>
+						<label for="selectVariant">{{ translate("Variant") }}</label>
+						<select id="selectVariant" v-model="newgameInfo.vid">
+							<option v-for="v in variants" :value="v.id">{{ v.name }}</option>
+						</select>
+					</fieldset>
+					<fieldset>
+						<label for="selectNbPlayers">{{ translate("Number of players") }}</label>
+						<select id="selectNbPlayers" v-model="newgameInfo.nbPlayers">
+							<option v-show="possibleNbplayers(2)" value="2">2</option>
+							<option v-show="possibleNbplayers(3)" value="3">3</option>
+							<option v-show="possibleNbplayers(4)" value="4">4</option>
+						</select>
+					</fieldset>
+					<fieldset>
+						<label for="timeControl">Time control (in days)</label>
+						<div id="timeControl">
+							<input type="number" v-model="newgameInfo.mainTime" placeholder="Main time"/>
+							<input type="number" v-model="newgameInfo.increment" placeholder="Increment"/>
+						</div>
+					</fieldset>
+					<fieldset>
+						<label for="selectPlayers">{{ translate("Play with?") }}</label>
+						<div id="selectPlayers">
+							<input type="text" v-model="newgameInfo.players[0]"/>
+							<input v-show="newgameInfo.nbPlayers>=3" type="text"
+								v-model="newgameInfo.players[1]"/>
+							<input v-show="newgameInfo.nbPlayers==4" type="text"
+								v-model="newgameInfo.players[2]"/>
+						</div>
+					</fieldset>
+					<fieldset>
+						<label for="inputFen">{{ translate("FEN (ignored if players fields are blank)") }}</label>
+						<input id="inputFen" type="text" v-model="newgameInfo.fen"/>
+					</fieldset>
+					<button @click="newGame">Launch game</button>
+				</div>
+			</div>
+			<p v-if="!userId">Correspondance play is reserved to registered users</p>
+			<div v-if="!!userId">
+				<my-challenge-list :challenges="challenges" @click-challenge="clickChallenge">
+				</my-challenge-list>
+				<button onClick="doClick('modalNewgame')">New game</button>
+				<my-game-list :games="games" @show-game="showGame">
+				</my-game-list>
+			</div>
 		</div>
 	`,
+	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: `
 		<table>
 			<tr>
+				<th v-if="showVariant">Variant</th>
 				<th>Players names</th>
 				<th>Cadence</th>
 				<th v-if="showResult">Result</th>
 			</tr>
 			<tr v-for="g in games" @click="$emit('show-game',g)">
+				<td v-if="showVariant">{{ g.vname }}</td>
 				<td>
 					<span v-for="p in g.players">{{ p.name }}</span>
 				</td>
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")