From 582df3497b0f91dd4b645386a059eac9e98da1bb Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 15 Jan 2019 01:25:23 +0100
Subject: [PATCH] Computer mode in rules section almost OK

---
 TODO => _tmp/TODO                             | 34 ++++------
 .../hexaboard_test.html                       | 10 ++-
 models/Problem.js                             | 44 ++++---------
 public/javascripts/components/board.js        | 20 ++----
 public/javascripts/components/game.js         | 54 +++++++++++-----
 public/javascripts/components/problems.js     | 29 +++++++--
 public/javascripts/components/rules.js        | 30 ++++++++-
 public/javascripts/utils/squareId.js          | 12 ++++
 public/javascripts/variant.js                 | 64 +++++++++----------
 reflexions                                    |  7 --
 routes/all.js                                 |  4 +-
 routes/challenge.js                           |  4 +-
 routes/index.js                               |  2 +
 routes/messages.js                            |  2 +
 routes/playing.js                             |  2 +
 routes/problems.js                            | 55 ++++++++--------
 routes/users.js                               |  4 +-
 routes/variant.js                             |  6 +-
 views/modalSettings.pug                       | 37 +++++++----
 views/variant.pug                             |  7 +-
 20 files changed, 243 insertions(+), 184 deletions(-)
 rename TODO => _tmp/TODO (67%)
 rename hexaboard_test.html => _tmp/hexaboard_test.html (76%)
 create mode 100644 public/javascripts/utils/squareId.js
 delete mode 100644 reflexions

diff --git a/TODO b/_tmp/TODO
similarity index 67%
rename from TODO
rename to _tmp/TODO
index 7ea4813d..cb4b8a97 100644
--- a/TODO
+++ b/_tmp/TODO
@@ -1,4 +1,11 @@
-Sur index, introduction menu remplacé par "mes parties", montrant parties (corr) en cours toutes variantes confondues
+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
@@ -8,7 +15,6 @@ mat en 2 échiqueté : brnkr3/pppp1p1p/4ps2/8/2P2P2/P1qP4/2c1s1PP/R1K5
 
 // TODO: decodeURIComponent() for GET/DELETE parameters
 
-1) Finish problems tab
 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)
@@ -27,32 +33,20 @@ Increase code line length to 100 or more?
 Chat button should be more apparent after game ends (color ?)
 Reinforce security for problems upload (how ?)
 
-The mode switch between human/computer/friend (+ problem) is a mess
-(example: finished computer game, ongoing friend game, reload, friend game is unreachable)
-
 Later:
-Let choice of time control, allow correspondance play, several games at the same time
+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)
-Identity would be browser-based: different games on smartphone, home computer, work computer... (why not ?)
-Index might still look the same, and variant page would have another tab "Games"
-==> running, and finished (which can be deleted from local memory)
-(A true analysis mode could be implemented also, to navigate in completed games --> use a button)
+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,
-except when accepting an individual challenge.
+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")
- - friend-->renommé en 'analyse' et devenant un vrai mode analyse (on garde ces trois modes ?)
-
-problèmes : récupérer 20 ou 50 depuis le serveur, puis les afficher un par un en les analysant directement,
-comme sur le site de ProgramFOX ==> présentation unifiée échiquier avec instructions dessus et soluce cachée dessous
-
-==> il faut pouvoir faire "new Interface(variables)" pour lancer une analyse de problème sans repasser par le mode jeu...
 
 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).
@@ -61,13 +55,13 @@ 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.
+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...
+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)
 
diff --git a/hexaboard_test.html b/_tmp/hexaboard_test.html
similarity index 76%
rename from hexaboard_test.html
rename to _tmp/hexaboard_test.html
index 800e1896..86abc2e0 100644
--- a/hexaboard_test.html
+++ b/_tmp/hexaboard_test.html
@@ -37,13 +37,21 @@ for(var a=0;a<8;a++) {
 	}}
 }}
 
+let x=100, y=100, size=40;
+ctx.beginPath();
+ctx.moveTo(x + size * Math.cos(0), y + size * Math.sin(0));
+for (let side=0; side < 7; side++) {
+	ctx.lineTo(x + size * Math.cos(side * 2 * Math.PI / 6), y + size * Math.sin(side * 2 * Math.PI / 6));
+}
+ctx.fillStyle = "#333333";
+ctx.fill();
+
 var img = new Image();
 img.onload = function() {
 	    ctx.drawImage(img, 0, 0, 60, 60);
 }
 img.src = "public/images/pieces/wb.svg";
 
-	
 </script>
 
 </body>
diff --git a/models/Problem.js b/models/Problem.js
index 78586761..7ac92f78 100644
--- a/models/Problem.js
+++ b/models/Problem.js
@@ -11,50 +11,34 @@ var db = require("../utils/database");
  */
 
 // TODO: callback ?
-exports.create = function(vname, fen, instructions, solution)
-{
-	db.serialize(function() {
-		const vidQuery =
-			"SELECT id " +
-			"FROM Variants " +
-			"WHERE name = '" + vname + "'";
-		db.get(vidQuery, (err,variant) => {
-			const insertQuery =
-				"INSERT INTO Problems (added, vid, fen, instructions, solution) VALUES " +
-				"(" +
-					Date.now() + "," +
-					variant.id + "," +
-					fen + "," +
-					instructions + "," +
-					solution +
-				")";
-			db.run(insertQuery);
-		});
-	});
-}
-
-exports.getById = function(id, callback)
+exports.create = function(vid, fen, instructions, solution)
 {
 	db.serialize(function() {
 		const query =
-			"SELECT * FROM Problems " +
-			"WHERE id ='" + id + "'";
-		db.get(query, callback);
+			"INSERT INTO Problems (added, vid, fen, instructions, solution) VALUES " +
+			"(" +
+				Date.now() + "," +
+				vid + "," +
+				fen + "," +
+				instructions + "," +
+				solution +
+			")";
+		db.run(query);
 	});
 }
 
-exports.getOne = function(vname, pid, callback)
+exports.getOne = function(id, callback)
 {
 	db.serialize(function() {
 		const query =
 			"SELECT * " +
 			"FROM Problems " +
-			"WHERE id = " + pid;
+			"WHERE id = " + id;
 		db.get(query, callback);
 	});
 }
 
-exports.fetchN = function(vname, uid, type, directionStr, lastDt, MaxNbProblems, callback)
+exports.fetchN = function(vid, uid, type, directionStr, lastDt, MaxNbProblems, callback)
 {
 	db.serialize(function() {
 		let typeLine = "";
@@ -62,7 +46,7 @@ exports.fetchN = function(vname, uid, type, directionStr, lastDt, MaxNbProblems,
 			typeLine = "AND id " + (type=="others" ? "!=" : "=") + " " + uid;
 		const query =
 			"SELECT * FROM Problems " +
-			"WHERE vid = (SELECT id FROM Variants WHERE name = '" + vname + "') " +
+			"WHERE vid = " + vid +
 			"  AND added " + directionStr + " " + lastDt + " " + typeLine + " " +
 			"ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
 			"LIMIT " + MaxNbProblems;
diff --git a/public/javascripts/components/board.js b/public/javascripts/components/board.js
index 0399410f..5a75f298 100644
--- a/public/javascripts/components/board.js
+++ b/public/javascripts/components/board.js
@@ -142,7 +142,7 @@ Vue.component('my-board', {
 									'incheck': showLight && incheckSq[ci][cj],
 								},
 								attrs: {
-									id: this.getSquareId({x:ci,y:cj}),
+									id: getSquareId({x:ci,y:cj}),
 								},
 							},
 							elems
@@ -161,7 +161,7 @@ Vue.component('my-board', {
 				myReservePiecesArray.push(h('div',
 				{
 					'class': {'board':true, ['board'+sizeY]:true},
-					attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
+					attrs: { id: getSquareId({x:sizeX+shiftIdx,y:i}) }
 				},
 				[
 					h('img',
@@ -185,7 +185,7 @@ Vue.component('my-board', {
 				oppReservePiecesArray.push(h('div',
 				{
 					'class': {'board':true, ['board'+sizeY]:true},
-					attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
+					attrs: { id: getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
 				},
 				[
 					h('img',
@@ -251,16 +251,6 @@ Vue.component('my-board', {
 		);
 	},
 	methods: {
-		// Get the identifier of a HTML square from its numeric coordinates o.x,o.y.
-		getSquareId: function(o) {
-			// NOTE: a separator is required to allow any size of board
-			return  "sq-" + o.x + "-" + o.y;
-		},
-		// Inverse function
-		getSquareFromId: function(id) {
-			let idParts = id.split('-');
-			return [parseInt(idParts[1]), parseInt(idParts[2])];
-		},
 		mousedown: function(e) {
 			e = e || window.event;
 			let ingame = false;
@@ -291,7 +281,7 @@ Vue.component('my-board', {
 				this.selectedPiece.style.top = 0;
 				this.selectedPiece.style.display = "inline-block";
 				this.selectedPiece.style.zIndex = 3000;
-				const startSquare = this.getSquareFromId(e.target.parentNode.id);
+				const startSquare = getSquareFromId(e.target.parentNode.id);
 				this.possibleMoves = [];
 				const color = this.mode=="analyze" || this.gameOver
 					? this.vr.turn
@@ -337,7 +327,7 @@ Vue.component('my-board', {
 				return;
 			}
 			// OK: process move attempt
-			let endSquare = this.getSquareFromId(landing.id);
+			let endSquare = getSquareFromId(landing.id);
 			let moves = this.findMatchingMoves(endSquare);
 			this.possibleMoves = [];
 			if (moves.length > 1)
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index edd17c43..c411b096 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -4,12 +4,10 @@
 Vue.component('my-game', {
 	// gameId: to find the game in storage (assumption: it exists)
 	// fen: to start from a FEN without identifiers (analyze mode)
-	props: ["conn","gameId","fen","mode","allowChat","allowMovelist"],
+	props: ["conn","gameId","fen","mode","allowChat","allowMovelist","queryHash","settings"],
 	data: function() {
 		return {
 			oppConnected: false, //TODO?
-			// sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
-			sound: parseInt(localStorage["sound"] || "2"),
 			// Web worker to play computer moves without freezing interface:
 			compWorker: new Worker('/javascripts/playCompMove.js'),
 			timeStart: undefined, //time when computer starts thinking
@@ -17,10 +15,9 @@ Vue.component('my-game', {
 			endgameMessage: "",
 			orientation: "w",
 
-			// if oppid == "computer" then mode = "computer" (otherwise human)
 			oppid: "", //opponent ID in case of HH game
 			score: "*", //'*' means 'unfinished'
-			// userColor: given by gameId, or fen (if no game Id)
+			// userColor: given by gameId, or fen in problems mode (if no game Id)...
 			mycolor: "w",
 			fenStart: "",
 			moves: [], //TODO: initialize if gameId is defined...
@@ -31,10 +28,30 @@ Vue.component('my-game', {
 	watch: {
 		fen: function(newFen) {
 			this.vr = new VariantRules(newFen);
+			this.moves = [];
+			this.cursor = 0;
+			this.fenStart = newFen;
+			this.score = "*";
+			if (this.mode == "analyze")
+			{
+				this.mycolor = V.ParseFen(newFen).turn;
+				this.orientation = "w"; //convention (TODO?!)
+			}
+			else if (this.mode == "computer") //only other alternative (HH with gameId)
+			{
+				this.mycolor = (Math.random() < 0.5 ? "w" : "b");
+				this.orientation = this.mycolor;
+				this.compWorker.postMessage(["init",newFen]);
+			}
 		},
 		gameId: function() {
 			this.loadGame();
 		},
+		queryHash: function(newQhash) {
+			// New query hash = "id=42"; get 42 as gameId
+			this.gameId = parseInt(newQhash.substr(2));
+			this.loadGame();
+		},
 	},
 	computed: {
 		showChat: function() {
@@ -65,7 +82,9 @@ Vue.component('my-game', {
 			</div>
 			<my-chat v-if="showChat">
 			</my-chat>
-			<my-board v-bind:vr="vr" :last-move="lastMove" :mode="mode" :orientation="orientation" :user-color="mycolor" @play-move="play">
+			<my-board v-bind:vr="vr" :last-move="lastMove" :mode="mode"
+				:orientation="orientation" :user-color="mycolor" :settings="settings"
+				@play-move="play">
 			</my-board>
 			<div class="button-group">
 				<button @click="() => play()">Play</button>
@@ -175,8 +194,11 @@ Vue.component('my-game', {
 			this.conn.addEventListener('message', socketMessageListener);
 			this.conn.addEventListener('close', socketCloseListener);
 		};
-		this.conn.onmessage = socketMessageListener;
-		this.conn.onclose = socketCloseListener;
+		if (!!this.conn)
+		{
+			this.conn.onmessage = socketMessageListener;
+			this.conn.onclose = socketCloseListener;
+		}
 
 		// Computer moves web worker logic: (TODO: also for observers in HH games)
 		this.compWorker.postMessage(["scripts",variant.name]);
@@ -318,20 +340,20 @@ Vue.component('my-game', {
 			this.compWorker.postMessage(["askmove"]);
 		},
 		animateMove: function(move) {
-			let startSquare = document.getElementById(this.getSquareId(move.start));
-			let endSquare = document.getElementById(this.getSquareId(move.end));
+			let startSquare = document.getElementById(getSquareId(move.start));
+			let endSquare = document.getElementById(getSquareId(move.end));
 			let rectStart = startSquare.getBoundingClientRect();
 			let rectEnd = endSquare.getBoundingClientRect();
 			let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
 			let movingPiece =
-				document.querySelector("#" + this.getSquareId(move.start) + " > img.piece");
+				document.querySelector("#" + getSquareId(move.start) + " > img.piece");
 			// HACK for animation (with positive translate, image slides "under background")
 			// Possible improvement: just alter squares on the piece's way...
 			squares = document.getElementsByClassName("board");
 			for (let i=0; i<squares.length; i++)
 			{
 				let square = squares.item(i);
-				if (square.id != this.getSquareId(move.start))
+				if (square.id != getSquareId(move.start))
 					square.style.zIndex = "-1";
 			}
 			movingPiece.style.transform = "translate(" + translation.x + "px," +
@@ -368,12 +390,12 @@ Vue.component('my-game', {
 			this.lastMove = move;
 			if (!move.fen)
 				move.fen = this.vr.getFen();
-			if (this.sound == 2)
+			if (this.settings.sound == 2)
 				new Audio("/sounds/move.mp3").play().catch(err => {});
 			if (this.mode == "human")
 			{
 				updateStorage(move); //after our moves and opponent moves
-				if (this.vr.turn == this.userColor)
+				if (this.vr.turn == this.mycolor)
 					this.conn.send(JSON.stringify({code:"newmove", move:move, oppid:this.oppid}));
 			}
 			else if (this.mode == "computer")
@@ -400,7 +422,7 @@ Vue.component('my-game', {
 					this.showScoreMsg(score);
 				// TODO: notify end of game (give score)
 			}
-			else if (this.mode == "computer" && this.vr.turn != this.userColor)
+			else if (this.mode == "computer" && this.vr.turn != this.mycolor)
 				this.playComputerMove();
 			// https://vuejs.org/v2/guide/list.html#Caveats (also for undo)
 			if (navigate)
@@ -419,7 +441,7 @@ Vue.component('my-game', {
 			this.lastMove = (this.cursor > 0 ? this.moves[this.cursor-1] : undefined);
 			if (navigate)
 				this.$children[0].$forceUpdate(); //TODO!?
-			if (this.sound == 2)
+			if (this.settings.sound == 2)
 				new Audio("/sounds/undo.mp3").play().catch(err => {});
 			this.incheck = this.vr.getCheckSquares(this.vr.turn);
 			if (!navigate && this.mode == "analyze")
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index cf2cd074..00ea7692 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -1,4 +1,5 @@
 Vue.component('my-problems', {
+	props: ["queryHash","settings"],
 	data: function () {
 		return {
 			userId: user.id,
@@ -38,7 +39,7 @@ Vue.component('my-problems', {
 						{{ curProb.instructions }}
 					</p>
 				</div>
-				<my-game :fen="curProb.fen" :mode="analyze" :allowMovelist="true">
+				<my-game :fen="curProb.fen" :mode="analyze" :allowMovelist="true" :settings="settings">
 				</my-board>
 				<div id="solution-div" class="section-content">
 					<h3 class="clickable" @click="showSolution = !showSolution">
@@ -128,10 +129,24 @@ Vue.component('my-problems', {
 			</div>
 		</div>
 	`,
+	watch: {
+		queryHash: function(newQhash) {
+			if (!!newQhash)
+			{
+				// New query hash = "id=42"; get 42 as problem ID
+				const pid = parseInt(newQhash.substr(2));
+				this.showProblem(pid);
+			}
+			else
+				this.curProb = null; //(back to) list display
+		},
+	},
 	created: function() {
-		// TODO: adapt this, #problems:28 ? (for example)
-		if (location.hash.length > 0)
-			this.showProblem(location.hash.slice(1));
+		if (!!this.queryHash)
+		{
+			const pid = parseInt(this.queryHash.substr(2));
+			this.showProblem(pid);
+		}
 		else
 			this.firstFetch();
 	},
@@ -249,7 +264,7 @@ Vue.component('my-problems', {
 				}
 			}
 			ajax(
-				"/problems/" + variant.name, //TODO: use variant._id ?
+				"/problems/" + variant.id,
 				"GET",
 				{
 					type: type,
@@ -284,7 +299,7 @@ Vue.component('my-problems', {
 		},
 		deleteProblem: function(pid) {
 			ajax(
-				"/problems/" + variant.name + "/" + pid, //TODO: with variant.id ?
+				"/problems/" + variant.id + "/" + pid,
 				"DELETE",
 				response => {
 					// Delete problem from the list on client side
@@ -297,7 +312,7 @@ Vue.component('my-problems', {
 		sendProblem: function() {
 			// Send it to the server and close modal
 			ajax(
-				"/problems/" + variant.name, //TODO: with variant.id ?
+				"/problems/" + variant.id,
 				(this.modalProb.id > 0 ? "PUT" : "POST"),
 				this.modalProb,
 				response => {
diff --git a/public/javascripts/components/rules.js b/public/javascripts/components/rules.js
index 02d3a0ca..e9df1ecc 100644
--- a/public/javascripts/components/rules.js
+++ b/public/javascripts/components/rules.js
@@ -1,11 +1,33 @@
 // Load rules on variant page
 Vue.component('my-rules', {
+	props: ["settings"],
 	data: function() {
-		return { content: "" };
+		return {
+			content: "",
+			display: "rules",
+			mode: "computer",
+			mycolor: "w",
+			allowMovelist: true,
+			fen: "",
+		};
 	},
+	
+	// TODO: third button "see a sample game" (comp VS comp)
+	
 	template: `
 		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
-			<div v-html="content" class="section-content"></div>
+			<div class="button-group">
+				<button @click="display='rules'">
+					Read the rules
+				</button>
+				<button @click="startComputerGame()">
+					Beat the computer!
+				</button>
+			</div>
+			<div v-show="display=='rules'" v-html="content" class="section-content"></div>
+			<my-game v-show="display=='computer'" :mycolor="mycolor" :settings="settings"
+				:allow-movelist="allowMovelist" :mode="mode" :fen="fen">
+			</my-game>
 		</div>
 	`,
 	mounted: function() {
@@ -28,5 +50,9 @@ Vue.component('my-rules', {
 				shadow: fenParts[3],
 			};
 		},
+		startComputerGame: function() {
+			this.fen = V.GenRandInitFen();
+			this.display = "computer";
+		},
 	},
 })
diff --git a/public/javascripts/utils/squareId.js b/public/javascripts/utils/squareId.js
new file mode 100644
index 00000000..a3423f7b
--- /dev/null
+++ b/public/javascripts/utils/squareId.js
@@ -0,0 +1,12 @@
+// Get the identifier of a HTML square from its numeric coordinates o.x,o.y.
+function getSquareId(o)
+{
+	// NOTE: a separator is required to allow any size of board
+	return  "sq-" + o.x + "-" + o.y;
+}
+
+// Inverse function
+function getSquareFromId(id) {
+	let idParts = id.split('-');
+	return [parseInt(idParts[1]), parseInt(idParts[2])];
+}
diff --git a/public/javascripts/variant.js b/public/javascripts/variant.js
index c1ba8573..606c1b9a 100644
--- a/public/javascripts/variant.js
+++ b/public/javascripts/variant.js
@@ -3,23 +3,36 @@ new Vue({
 	data: {
 		display: "undefined", //default to main hall; see "created()" function
 		gameid: undefined, //...yet
-	
+		queryHash: "",
 		conn: null,
 
+		// Settings initialized with values from localStorage
+		settings:	{
+			bcolor: localStorage["bcolor"] || "lichess",
+			sound: parseInt(localStorage["sound"]) || 2,
+			hints: parseInt(localStorage["hints"]) || 1,
+			coords: !!eval(localStorage["coords"]),
+			highlight: !!eval(localStorage["highlight"]),
+			sqSize: parseInt(localStorage["sqSize"]),
+		},
+
 		// TEMPORARY: DEBUG
 		mode: "analyze",
 		orientation: "w",
 		userColor: "w",
-
 		allowChat: false,
 		allowMovelist: true,
 		fen: V.GenRandInitFen(),
 	},
 	created: function() {
-		// TODO: navigation becomes a little more complex
-		this.setDisplay();
+		if (!!localStorage["variant"])
+		{
+			location.hash = "#game?id=" + localStorage["gameId"];
+			this.display = location.hash.substr(1);
+		}
+		else
+			this.setDisplay();
 		window.onhashchange = this.setDisplay;
-
 		this.myid = "abcdefghij";
 //console.log(this.myid + " " + variant);
 			//myid: localStorage.getItem("myid"), //our ID, always set
@@ -33,45 +46,30 @@ new Vue({
 		//this.vr = new VariantRules( V.GenRandInitFen() );
 	},
 	methods: {
+		updateSettings: function(event) {
+			const propName =
+				event.target.id.substr(3).replace(/^\w/, c => c.toLowerCase())
+			localStorage[propName] = ["highlight","coords"].includes(propName)
+				? event.target.checked
+				: event.target.value;
+		},
 		setDisplay: function() {
-
-//TODO: prevent set display if there is a running game
-
+			// Prevent set display if there is a running game
+			if (!!localStorage["variant"])
+				return;
 			if (!location.hash)
 				location.hash = "#room"; //default
-			this.display = location.hash.substr(1);
+			const hashParts = location.hash.substr(1).split("?");
+			this.display = hashParts[0];
+			this.queryHash = hashParts[1]; //may be empty, undefined...
 			// Close menu on small screens:
 			let menuToggle = document.getElementById("drawer-control");
 			if (!!menuToggle)
 				menuToggle.checked = false;
 		},
-
-		// TEMPORARY: DEBUG (duplicate code)
-		play: function(move) {
-			// Not programmatic, or animation is over
-			if (!move.notation)
-				move.notation = this.vr.getNotation(move);
-			this.vr.play(move);
-			if (!move.fen)
-				move.fen = this.vr.getFen();
-			if (this.sound == 2)
-				new Audio("/sounds/move.mp3").play().catch(err => {});
-			// Is opponent in check?
-			this.incheck = this.vr.getCheckSquares(this.vr.turn);
-			const score = this.vr.getCurrentScore();
-		},
-		undo: function(move) {
-			this.vr.undo(move);
-			if (this.sound == 2)
-				new Audio("/sounds/undo.mp3").play().catch(err => {});
-			this.incheck = this.vr.getCheckSquares(this.vr.turn);
-		},
 	},
 });
 		
 //const continuation = (localStorage.getItem("variant") === variant.name);
 //			if (continuation) //game VS human has priority
 //				this.continueGame("human");
-
-// TODO:
-// si quand on arrive il y a une continuation "humaine" : display="game" et retour à la partie !
diff --git a/reflexions b/reflexions
deleted file mode 100644
index 82e59809..00000000
--- a/reflexions
+++ /dev/null
@@ -1,7 +0,0 @@
-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
diff --git a/routes/all.js b/routes/all.js
index 3e989f77..0989b3fb 100644
--- a/routes/all.js
+++ b/routes/all.js
@@ -2,10 +2,10 @@ var router = require("express").Router();
 
 router.use("/", require("./index"));
 router.use("/", require("./users"));
-router.use("/", require("./problems"));
 router.use("/", require("./messages"));
-//router.use("/", require("./challenge"));
 //router.use("/", require("./playing"));
+//router.use("/", require("./challenge"));
+router.use("/", require("./problems"));
 router.use("/", require("./variant"));
 
 module.exports = router;
diff --git a/routes/challenge.js b/routes/challenge.js
index 47aaad63..1a4d22b8 100644
--- a/routes/challenge.js
+++ b/routes/challenge.js
@@ -1,3 +1,5 @@
+// TODO: adapt this (from Mongo to SQLite, and challenge format changed) for corr play
+
 var router = require("express").Router();
 var ObjectID = require("bson-objectid");
 var ChallengeModel = require('../models/Challenge');
@@ -5,8 +7,6 @@ var UserModel = require('../models/User');
 var ObjectID = require("bson-objectid");
 var access = require("../utils/access");
 
-// Only AJAX requests here (from variant page and index)
-
 // variant page
 router.get("/challengesbyvariant", access.logged, access.ajax, (req,res) => {
 	if (req.query["uid"] != req.user._id)
diff --git a/routes/index.js b/routes/index.js
index 0ed5b07a..d510068e 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,3 +1,5 @@
+// Main index page
+
 let router = require("express").Router();
 const VariantModel = require("../models/Variant");
 const selectLanguage = require("../utils/language.js");
diff --git a/routes/messages.js b/routes/messages.js
index 9a72f5e4..74ec8bd4 100644
--- a/routes/messages.js
+++ b/routes/messages.js
@@ -1,3 +1,5 @@
+// Router for contact form sending
+
 let router = require("express").Router();
 const mailer = require(__dirname.replace("/routes", "/utils/mailer"));
 
diff --git a/routes/playing.js b/routes/playing.js
index 7af0a1bc..3bdfa35c 100644
--- a/routes/playing.js
+++ b/routes/playing.js
@@ -1,3 +1,5 @@
+// TODO: adapt for correspondance play
+
 var router = require("express").Router();
 var UserModel = require("../models/User");
 var GameModel = require('../models/Game');
diff --git a/routes/problems.js b/routes/problems.js
index adb75dae..3434f0cc 100644
--- a/routes/problems.js
+++ b/routes/problems.js
@@ -6,11 +6,27 @@ const ProblemModel = require("../models/Problem");
 const sanitizeHtml = require('sanitize-html');
 const MaxNbProblems = 20;
 
-// Get one problem
-router.get("/problems/:vname([a-zA-Z0-9]+)/:pnum([0-9]+)", access.ajax, (req,res) => {
-	const vname = req.params["vname"];
-	const pnum = req.params["pnum"];
-	ProblemModel.getOne(vname, pnum, (err,problem) => {
+function sanitizeUserInput(fen, instructions, solution)
+{
+	if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
+		return "Bad characters in FEN string";
+	instructions = sanitizeHtml(instructions);
+	solution = sanitizeHtml(solution);
+	if (instructions.length == 0)
+		return "Empty instructions";
+	if (solution.length == 0)
+		return "Empty solution";
+	return {
+		fen: fen,
+		instructions: instructions,
+		solution: solution
+	};
+}
+
+// Get one problem (TODO: vid unused, here for URL de-ambiguification)
+router.get("/problems/:vid([0-9]+)/:id([0-9]+)", access.ajax, (req,res) => {
+	const pid = req.params["id"];
+	ProblemModel.getOne(pid, (err,problem) => {
 		if (!!err)
 			return res.json(err);
 		return res.json({problem: problem});
@@ -18,8 +34,8 @@ router.get("/problems/:vname([a-zA-Z0-9]+)/:pnum([0-9]+)", access.ajax, (req,res
 });
 
 // Fetch N previous or next problems
-router.get("/problems/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
-	const vname = req.params["vname"];
+router.get("/problems/:vid([0-9]+)", access.ajax, (req,res) => {
+	const vid = req.params["vid"];
 	const directionStr = (req.query.direction == "forward" ? ">" : "<");
 	const lastDt = req.query.last_dt;
 	const type = req.query.type;
@@ -27,7 +43,7 @@ router.get("/problems/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
 		return res.json({errmsg: "Bad timestamp"});
 	if (!["others","mine"].includes(type))
 		return res.json({errmsg: "Bad type"});
-	ProblemModel.fetchN(vname, req.userId, type, directionStr, lastDt, MaxNbProblems,
+	ProblemModel.fetchN(vid, req.userId, type, directionStr, lastDt, MaxNbProblems,
 		(err,problems) => {
 			if (!!err)
 				return res.json(err);
@@ -36,30 +52,13 @@ router.get("/problems/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
 	);
 });
 
-function sanitizeUserInput(fen, instructions, solution)
-{
-	if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
-		return "Bad characters in FEN string";
-	instructions = sanitizeHtml(instructions);
-	solution = sanitizeHtml(solution);
-	if (instructions.length == 0)
-		return "Empty instructions";
-	if (solution.length == 0)
-		return "Empty solution";
-	return {
-		fen: fen,
-		instructions: instructions,
-		solution: solution
-	};
-}
-
 // Upload a problem (sanitize inputs)
-router.post("/problems/:vname([a-zA-Z0-9]+)", access.logged, access.ajax, (req,res) => {
-	const vname = req.params["vname"];
+router.post("/problems/:vid([0-9]+)", access.logged, access.ajax, (req,res) => {
+	const vid = req.params["vid"];
 	const s = sanitizeUserInput(req.body["fen"], req.body["instructions"], req.body["solution"]);
 	if (typeof s === "string")
 		return res.json({errmsg: s});
-  ProblemModel.create(vname, s.fen, s.instructions, s.solution);
+  ProblemModel.create(vid, s.fen, s.instructions, s.solution);
 	res.json({});
 });
 
diff --git a/routes/users.js b/routes/users.js
index 9639ad58..9c88d08c 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -1,3 +1,5 @@
+// AJAX methods to get, create, update or delete a user
+
 var router = require("express").Router();
 var UserModel = require('../models/User');
 var sendEmail = require('../utils/mailer');
@@ -25,8 +27,6 @@ function setAndSendLoginToken(subject, to, res)
 	});
 }
 
-// AJAX user life cycle...
-
 router.post('/register', access.unlogged, access.ajax, (req,res) => {
 	const name = req.body.name;
 	const email = req.body.email;
diff --git a/routes/variant.js b/routes/variant.js
index f45c9594..cfb63414 100644
--- a/routes/variant.js
+++ b/routes/variant.js
@@ -1,11 +1,13 @@
+// (any) variant page (with room, games, problems ...)
+
 let router = require("express").Router();
 const createError = require('http-errors');
 const VariantModel = require("../models/Variant");
 const selectLanguage = require("../utils/language.js");
 const access = require("../utils/access");
 
-router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => {
-	const vname = req.params["variant"];
+router.get("/:vname([a-zA-Z0-9]+)", (req,res,next) => {
+	const vname = req.params["vname"];
 	VariantModel.getByName(vname, (err,variant) => {
 		if (!!err)
 			return next(err);
diff --git a/views/modalSettings.pug b/views/modalSettings.pug
index 9398a773..3c22c359 100644
--- a/views/modalSettings.pug
+++ b/views/modalSettings.pug
@@ -1,26 +1,35 @@
 input#modalSettings.modal(type="checkbox")
 div(role="dialog" aria-labelledby="settingsTitle")
-	.card.smallpad(onChange="blabla(event)")
+	.card.smallpad(@change="updateSettings")
 		label.modal-close(for="modalSettings")
 		h3#settingsTitle.section= translations["Preferences"]
-		// taille echiquier : TODO
 		fieldset
-			label(for="setHints")= translations["Show hints?"]
-			// TODO: this.hints will not work. Idea: query storage in a generic way ?
-			// --> getValue("hints") par exemple
-			input#setHints(type="checkbox" checked=this.hints)
+			label(for="setSqSize")= translations["Square size (in pixels). 0 for 'adaptative'"]
+			input#setSqSize(type="number" v-model="settings.sqSize")
+		fieldset
+			label(for="selectHints")= translations["Show move hints?"]
+			select#setHints(v-model="settings.hints")
+				option(value="0")= translations["None"]
+				option(value="1")= translations["Moves from a square"]
+				option(value="2")= translations["Pieces which can move"]
+		fieldset
+			label(for="setHighlight")= translations["Highlight squares? (Last move & checks)"]
+			input#setHighlight(type="checkbox" v-model="settings.highlight")
+		fieldset
+			label(for="setCoords")= translations["Show board coordinates?"]
+			input#setCoords(type="checkbox" v-model="settings.coords")
 		fieldset
 			label(for="selectColor")= translations["Board colors"]
-			select#selectColor
-				option(value="lichess" selected="this.color=='lichess'")
+			select#setBcolor(v-model="settings.bcolor")
+				option(value="lichess")
 					= translations["brown"]
-				option(value="chesscom" selected="this.color=='chesscom'")
+				option(value="chesscom")
 					= translations["green"]
-				option(value="chesstempo" selected="this.color=='chesstempo'")
+				option(value="chesstempo")
 					= translations["blue"]
 		fieldset
 			label(for="selectSound")= translations["Play sounds?"]
-			select#selectSound
-				option(value="0" selected="this.sound==0")= translations["None"]
-				option(value="1" selected="this.sound==1")= translations["New game"]
-				option(value="2" selected="this.sound==2")= translations["All"]
+			select#setSound(v-model="settings.sound")
+				option(value="0")= translations["None"]
+				option(value="1")= translations["New game"]
+				option(value="2")= translations["All"]
diff --git a/views/variant.pug b/views/variant.pug
index 7d091909..89872ffb 100644
--- a/views/variant.pug
+++ b/views/variant.pug
@@ -30,11 +30,11 @@ block content
 		.row
 			//my-room(v-show="display=='room'")
 			//my-game-list(v-show="display=='gameList'")
-			my-rules(v-show="display=='rules'")
-			//my-problems(v-show="display=='problems'")
+			my-rules(v-show="display=='rules'" :settings="settings")
+			//my-problems(v-show="display=='problems'" :query-hash="queryHash")
 			my-game(v-show="display=='game'" :game-id="gameid" :conn="conn"
 				:allow-chat="allowChat" :allow-movelist="allowMovelist"
-				:mode="mode" :fen="fen")
+				:mode="mode" :fen="fen" :query-hash="queryHash")
 			//my-board(:vr="vr" :mode="mode" :orientation="orientation"
 				:user-color="userColor" v-on:play-move="play")
 
@@ -42,6 +42,7 @@ 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/settings.js")
-- 
2.44.0