From 81da2786f2f497b4416e0488c34a48fb794c28df Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 8 Jan 2019 01:44:19 +0100
Subject: [PATCH] Refactoring: split files into components (not working, broken
 state)

---
 TODO                                          |   15 +
 public/javascripts/components/board.js        |  385 +++++
 public/javascripts/components/chat.js         |  106 ++
 public/javascripts/components/game.js         | 1256 +----------------
 public/javascripts/components/moveList.js     |    1 +
 .../javascripts/components/problemPreview.js  |   25 +
 .../javascripts/components/problemSummary.js  |   37 -
 public/javascripts/components/problems.js     |  111 +-
 public/javascripts/components/room.js         |  222 +++
 public/javascripts/settings.js                |   20 +
 public/javascripts/utils/storage.js           |   55 +
 public/javascripts/variant.js                 |    4 +
 views/variant.pug                             |    7 +-
 13 files changed, 993 insertions(+), 1251 deletions(-)
 create mode 100644 public/javascripts/components/board.js
 create mode 100644 public/javascripts/components/chat.js
 create mode 100644 public/javascripts/components/moveList.js
 create mode 100644 public/javascripts/components/problemPreview.js
 delete mode 100644 public/javascripts/components/problemSummary.js
 create mode 100644 public/javascripts/utils/storage.js

diff --git a/TODO b/TODO
index 071c476f..52fb8bbf 100644
--- a/TODO
+++ b/TODO
@@ -40,3 +40,18 @@ mais permettrait mode analyse (avec bouton "analyse", comme sur ancien site).
 
 espagnol : jugada ou movimiento ?
 fin de la partida au lieu de final de partida ?
+
+Bouton new game ==> human only. Indiquer adversaire (éventuellement), cadence (ou "infini")
+Mode analyse : accessible à tout moment d'une partie (HH, ou computer) terminée.
+
+Coordonnées sur échiquier: sur cases, à gauche (verticale) ou en bas (horizontale)
+
+Import game : en local dans indexedDb, affichage dans "Games --> Imported"
+
+Checkered : si intervention d'un 3eme joueur, initialiser son temps à la moyenne des temps restants des deux autres...
+
+Mode contre ordinateur : seulement accessible depuis onglet "Rules" (son principal intérêt)
+
+Hexachess: McCooey et Shafran (deux tailles, randomisation OK)
+http://www.math.bas.bg/~iad/tyalie/shegra/shegrax.html
+http://www.quadibloc.com/chess/ch0401.htm
diff --git a/public/javascripts/components/board.js b/public/javascripts/components/board.js
new file mode 100644
index 00000000..4cadb0bd
--- /dev/null
+++ b/public/javascripts/components/board.js
@@ -0,0 +1,385 @@
+			hints: (!localStorage["hints"] ? true : localStorage["hints"] === "1"),
+			bcolor: localStorage["bcolor"] || "lichess", //lichess, chesscom or chesstempo
+			possibleMoves: [], //filled after each valid click/dragstart
+			choices: [], //promotion pieces, or checkered captures... (as moves)
+			selectedPiece: null, //moving piece (or clicked piece)
+			incheck: [],
+			start: {}, //pixels coordinates + id of starting square (click or drag)
+			vr: null, //object to check moves, store them, FEN..
+	orientation: "w", //useful if click on "flip board"	
+	
+	
+	
+	const [sizeX,sizeY] = [V.size.x,V.size.y];
+		// Precompute hints squares to facilitate rendering
+		let hintSquares = doubleArray(sizeX, sizeY, false);
+		this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
+		// Also precompute in-check squares
+		let incheckSq = doubleArray(sizeX, sizeY, false);
+		this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; });
+			const choices = h('div',
+				{
+					attrs: { "id": "choices" },
+					'class': { 'row': true },
+					style: {
+						"display": this.choices.length>0?"block":"none",
+						"top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
+						"width": (this.choices.length * squareWidth) + "px",
+						"height": squareWidth + "px",
+					},
+				},
+				this.choices.map( m => { //a "choice" is a move
+					return h('div',
+						{
+							'class': {
+								'board': true,
+								['board'+sizeY]: true,
+							},
+							style: {
+								'width': (100/this.choices.length) + "%",
+								'padding-bottom': (100/this.choices.length) + "%",
+							},
+						},
+						[h('img',
+							{
+								attrs: { "src": '/images/pieces/' +
+									VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
+								'class': { 'choice-piece': true },
+								on: {
+									"click": e => { this.play(m); this.choices=[]; },
+									// NOTE: add 'touchstart' event to fix a problem on smartphones
+									"touchstart": e => { this.play(m); this.choices=[]; },
+								},
+							})
+						]
+					);
+				})
+			);
+			// Create board element (+ reserves if needed by variant or mode)
+			const lm = this.vr.lastMove;
+			const showLight = this.hints && variant!="Dark" &&
+				(this.mode != "idle" ||
+					(this.vr.moves.length > 0 && this.cursor==this.vr.moves.length));
+			const gameDiv = h('div',
+				{
+					'class': {
+						'game': true,
+						'clearer': true,
+					},
+				},
+				[_.range(sizeX).map(i => {
+					let ci = (this.mycolor=='w' ? i : sizeX-i-1);
+					return h(
+						'div',
+						{
+							'class': {
+								'row': true,
+							},
+							style: { 'opacity': this.choices.length>0?"0.5":"1" },
+						},
+						_.range(sizeY).map(j => {
+							let cj = (this.mycolor=='w' ? j : sizeY-j-1);
+							let elems = [];
+							if (this.vr.board[ci][cj] != VariantRules.EMPTY && (variant!="Dark"
+								|| this.score!="*" || this.vr.enlightened[this.mycolor][ci][cj]))
+							{
+								elems.push(
+									h(
+										'img',
+										{
+											'class': {
+												'piece': true,
+												'ghost': !!this.selectedPiece
+													&& this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
+											},
+											attrs: {
+												src: "/images/pieces/" +
+													VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
+											},
+										}
+									)
+								);
+							}
+							if (this.hints && hintSquares[ci][cj])
+							{
+								elems.push(
+									h(
+										'img',
+										{
+											'class': {
+												'mark-square': true,
+											},
+											attrs: {
+												src: "/images/mark.svg",
+											},
+										}
+									)
+								);
+							}
+							return h(
+								'div',
+								{
+									'class': {
+										'board': true,
+										['board'+sizeY]: true,
+										'light-square': (i+j)%2==0,
+										'dark-square': (i+j)%2==1,
+										[this.bcolor]: true,
+										'in-shadow': variant=="Dark" && this.score=="*"
+											&& !this.vr.enlightened[this.mycolor][ci][cj],
+										'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
+										'incheck': showLight && incheckSq[ci][cj],
+									},
+									attrs: {
+										id: this.getSquareId({x:ci,y:cj}),
+									},
+								},
+								elems
+							);
+						})
+					);
+				}), choices]
+			);
+			if (!!this.vr.reserve)
+			{
+				const shiftIdx = (this.mycolor=="w" ? 0 : 1);
+				let myReservePiecesArray = [];
+				for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
+				{
+					myReservePiecesArray.push(h('div',
+					{
+						'class': {'board':true, ['board'+sizeY]:true},
+						attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
+					},
+					[
+						h('img',
+						{
+							'class': {"piece":true, "reserve":true},
+							attrs: {
+								"src": "/images/pieces/" +
+									this.vr.getReservePpath(this.mycolor,i) + ".svg",
+							}
+						}),
+						h('sup',
+							{"class": { "reserve-count": true } },
+							[ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ]
+						)
+					]));
+				}
+				let oppReservePiecesArray = [];
+				const oppCol = this.vr.getOppCol(this.mycolor);
+				for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
+				{
+					oppReservePiecesArray.push(h('div',
+					{
+						'class': {'board':true, ['board'+sizeY]:true},
+						attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
+					},
+					[
+						h('img',
+						{
+							'class': {"piece":true, "reserve":true},
+							attrs: {
+								"src": "/images/pieces/" +
+									this.vr.getReservePpath(oppCol,i) + ".svg",
+							}
+						}),
+						h('sup',
+							{"class": { "reserve-count": true } },
+							[ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ]
+						)
+					]));
+				}
+				let reserves = h('div',
+					{
+						'class':{
+							'game': true,
+							"reserve-div": true,
+						},
+					},
+					[
+						h('div',
+							{
+								'class': {
+									'row': true,
+									"reserve-row-1": true,
+								},
+							},
+							myReservePiecesArray
+						),
+						h('div',
+							{ 'class': { 'row': true }},
+							oppReservePiecesArray
+						)
+					]
+				);
+				elementArray.push(reserves);
+			}
+				// Show current FEN (just below board, lower right corner)
+// (if mode != Dark ...)
+				elementArray.push(
+					h('div',
+						{
+							attrs: { id: "fen-div" },
+							"class": { "section-content": true },
+						},
+						[
+							h('p',
+								{
+									attrs: { id: "fen-string" },
+									domProps: { innerHTML: this.vr.getBaseFen() },
+									"class": { "text-center": true },
+								}
+							)
+						]
+					)
+				);
+				on: {
+					mousedown: this.mousedown,
+					mousemove: this.mousemove,
+					mouseup: this.mouseup,
+					touchstart: this.mousedown,
+					touchmove: this.mousemove,
+					touchend: this.mouseup,
+				},
+
+
+		// TODO: "chessground-like" component
+		// Get the identifier of a HTML table cell 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;
+			let elem = e.target;
+			while (!ingame && elem !== null)
+			{
+				if (elem.classList.contains("game"))
+				{
+					ingame = true;
+					break;
+				}
+				elem = elem.parentElement;
+			}
+			if (!ingame) //let default behavior (click on button...)
+				return;
+			e.preventDefault(); //disable native drag & drop
+			if (!this.selectedPiece && e.target.classList.contains("piece"))
+			{
+				// Next few lines to center the piece on mouse cursor
+				let rect = e.target.parentNode.getBoundingClientRect();
+				this.start = {
+					x: rect.x + rect.width/2,
+					y: rect.y + rect.width/2,
+					id: e.target.parentNode.id
+				};
+				this.selectedPiece = e.target.cloneNode();
+				this.selectedPiece.style.position = "absolute";
+				this.selectedPiece.style.top = 0;
+				this.selectedPiece.style.display = "inline-block";
+				this.selectedPiece.style.zIndex = 3000;
+				const startSquare = this.getSquareFromId(e.target.parentNode.id);
+				this.possibleMoves = [];
+				if (this.score == "*")
+				{
+					const color = ["friend","problem"].includes(this.mode)
+						? this.vr.turn
+						: this.mycolor;
+					if (this.vr.canIplay(color,startSquare))
+						this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
+				}
+				// Next line add moving piece just after current image
+				// (required for Crazyhouse reserve)
+				e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling);
+			}
+		},
+		mousemove: function(e) {
+			if (!this.selectedPiece)
+				return;
+			e = e || window.event;
+			// If there is an active element, move it around
+			if (!!this.selectedPiece)
+			{
+				const [offsetX,offsetY] = !!e.clientX
+					? [e.clientX,e.clientY] //desktop browser
+					: [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
+				this.selectedPiece.style.left = (offsetX-this.start.x) + "px";
+				this.selectedPiece.style.top = (offsetY-this.start.y) + "px";
+			}
+		},
+		mouseup: function(e) {
+			if (!this.selectedPiece)
+				return;
+			e = e || window.event;
+			// Read drop target (or parentElement, parentNode... if type == "img")
+			this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
+			const [offsetX,offsetY] = !!e.clientX
+				? [e.clientX,e.clientY]
+				: [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
+			let landing = document.elementFromPoint(offsetX, offsetY);
+			this.selectedPiece.style.zIndex = 3000;
+			// Next condition: classList.contains(piece) fails because of marks
+			while (landing.tagName == "IMG")
+				landing = landing.parentNode;
+			if (this.start.id == landing.id)
+			{
+				// A click: selectedPiece and possibleMoves are already filled
+				return;
+			}
+			// OK: process move attempt
+			let endSquare = this.getSquareFromId(landing.id);
+			let moves = this.findMatchingMoves(endSquare);
+			this.possibleMoves = [];
+			if (moves.length > 1)
+				this.choices = moves;
+			else if (moves.length==1)
+				this.play(moves[0]);
+			// Else: impossible move
+			this.selectedPiece.parentNode.removeChild(this.selectedPiece);
+			delete this.selectedPiece;
+			this.selectedPiece = null;
+		},
+		findMatchingMoves: function(endSquare) {
+			// Run through moves list and return the matching set (if promotions...)
+			let moves = [];
+			this.possibleMoves.forEach(function(m) {
+				if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
+					moves.push(m);
+			});
+			return moves;
+		},
+		animateMove: function(move) {
+			let startSquare = document.getElementById(this.getSquareId(move.start));
+			let endSquare = document.getElementById(this.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");
+			// 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))
+					square.style.zIndex = "-1";
+			}
+			movingPiece.style.transform = "translate(" + translation.x + "px," +
+				translation.y + "px)";
+			movingPiece.style.transitionDuration = "0.2s";
+			movingPiece.style.zIndex = "3000";
+			setTimeout( () => {
+				for (let i=0; i<squares.length; i++)
+					squares.item(i).style.zIndex = "auto";
+				movingPiece.style = {}; //required e.g. for 0-0 with KR swap
+				this.play(move);
+			}, 250);
+		},
diff --git a/public/javascripts/components/chat.js b/public/javascripts/components/chat.js
new file mode 100644
index 00000000..a1489909
--- /dev/null
+++ b/public/javascripts/components/chat.js
@@ -0,0 +1,106 @@
+			myname: localStorage["username"] || "anonymous",
+			oppName: "anonymous", //opponent name, revealed after a game (if provided)
+			chats: [], //chat messages after human game
+		
+	
+	
+		let chatEltsArray =
+		[
+			h('label',
+				{
+					attrs: { "id": "close-chat", "for": "modal-chat" },
+					"class": { "modal-close": true },
+				}
+			),
+			h('h3',
+				{
+					attrs: { "id": "titleChat" },
+					"class": { "section": true },
+					domProps: { innerHTML: translations["Chat with "] + this.oppName },
+				}
+			)
+		];
+		for (let chat of this.chats)
+		{
+			chatEltsArray.push(
+				h('p',
+					{
+						"class": {
+							"my-chatmsg": chat.author==this.myid,
+							"opp-chatmsg": chat.author==this.oppid,
+						},
+						domProps: { innerHTML: chat.msg }
+					}
+				)
+			);
+		}
+		chatEltsArray = chatEltsArray.concat([
+			h('input',
+				{
+					attrs: {
+						"id": "input-chat",
+						type: "text",
+						placeholder: translations["Type here"],
+					},
+					on: { keyup: this.trySendChat }, //if key is 'enter'
+				}
+			),
+			h('button',
+				{
+					attrs: { id: "sendChatBtn"},
+					on: { click: this.sendChat },
+					domProps: { innerHTML: translations["Send"] },
+				}
+			)
+		]);
+		const modalChat = [
+			h('input',
+				{
+					attrs: { "id": "modal-chat", type: "checkbox" },
+					"class": { "modal": true },
+				}),
+			h('div',
+				{
+					attrs: { "role": "dialog", "aria-labelledby": "titleChat" },
+				},
+				[
+					h('div',
+						{
+							"class": { "card": true, "smallpad": true },
+						},
+						chatEltsArray
+					)
+				]
+			)
+		];
+		elementArray = elementArray.concat(modalChat);
+	
+	
+				case "newchat":
+					// Receive new chat
+					this.chats.push({msg:data.msg, author:this.oppid});
+					break;
+				case "oppname":
+					// Receive opponent's name
+					this.oppName = data.name;
+					break;
+	
+	
+	// TODO: complete this component
+		trySendChat: function(e) {
+			if (e.keyCode == 13) //'enter' key
+				this.sendChat();
+		},
+		sendChat: function() {
+			let chatInput = document.getElementById("input-chat");
+			const chatTxt = chatInput.value;
+			chatInput.value = "";
+			this.chats.push({msg:chatTxt, author:this.myid});
+			this.conn.send(JSON.stringify({
+				code:"newchat", oppid: this.oppid, msg: chatTxt}));
+		},
+		startChat: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			document.getElementById("modal-chat").checked = true;
+		},
+
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index c2cce62c..8009edd7 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -1,30 +1,23 @@
 // Game logic on a variant page
 Vue.component('my-game', {
-	props: ["problem"],
+	props: ["gameId"], //to find the game in storage (assumption: it exists)
 	data: function() {
 		return {
-			vr: null, //object to check moves, store them, FEN..
-			mycolor: "w",
-			possibleMoves: [], //filled after each valid click/dragstart
-			choices: [], //promotion pieces, or checkered captures... (as moves)
-			start: {}, //pixels coordinates + id of starting square (click or drag)
-			selectedPiece: null, //moving piece (or clicked piece)
-			conn: null, //socket connection
-			score: "*", //'*' means 'unfinished'
-			mode: "idle", //human, friend, problem, computer or idle (if not playing)
+			
+			// TODO: merge next variables into "game"
+			// if oppid == "computer" then mode = "computer" (otherwise human)
 			myid: "", //our ID, always set
+		//this.myid = localStorage.getItem("myid")
 			oppid: "", //opponent ID in case of HH game
-			gameId: "", //useful if opponent started other human games after we disconnected
-			myname: localStorage["username"] || "anonymous",
-			oppName: "anonymous", //opponent name, revealed after a game (if provided)
-			chats: [], //chat messages after human game
+			score: "*", //'*' means 'unfinished'
+			mycolor: "w",
+			fromChallenge: false, //if true, show chat during game
+			
+			conn: null, //socket connection
 			oppConnected: false,
 			seek: false,
 			fenStart: "",
-			incheck: [],
 			pgnTxt: "",
-			hints: (!localStorage["hints"] ? true : localStorage["hints"] === "1"),
-			bcolor: localStorage["bcolor"] || "lichess", //lichess, chesscom or chesstempo
 			// 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:
@@ -32,757 +25,47 @@ Vue.component('my-game', {
 			timeStart: undefined, //time when computer starts thinking
 		};
 	},
-	watch: {
-		problem: function(p) {
-			// 'problem' prop changed: update board state
-			this.newGame("problem", p.fen, V.ParseFen(p.fen).turn);
+	computed: {
+		mode: function() {
+			return (this.game.oppid == "computer" ? "computer" ? "human");
+		},
+		showChat: function() {
+			return this.mode=='human' &&
+				(this.game.score != '*' || this.game.fromChallenge);
+		},
+		showMoves: function() {
+			return window.innerWidth >= 768;
 		},
 	},
-	// TODO: split the rendering in other components ?
-	// At least divide in parts, it's too big now.
-	render(h) {
-		const [sizeX,sizeY] = [V.size.x,V.size.y];
-		// Precompute hints squares to facilitate rendering
-		let hintSquares = doubleArray(sizeX, sizeY, false);
-		this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
-		// Also precompute in-check squares
-		let incheckSq = doubleArray(sizeX, sizeY, false);
-		this.incheck.forEach(sq => { incheckSq[sq[0]][sq[1]] = true; });
-		let elementArray = [];
-		let actionArray = [];
-		actionArray.push(
-			h('button',
-			{
-				on: { click: this.clickGameSeek },
-				attrs: { "aria-label": translations['New live game'] },
-				'class': {
-					"tooltip": true,
-					"play": true,
-					"seek": this.seek,
-					"playing": this.mode == "human" && this.score == "*",
-				},
-			},
-			[h('i', { 'class': { "material-icons": true } }, "accessibility")])
-		);
-		if (["idle","computer","friend"].includes(this.mode)
-			|| (this.mode == "human" && this.score != "*"))
-		{
-			actionArray.push(
-				h('button',
-				{
-					on: { click: this.clickComputerGame },
-					attrs: { "aria-label": translations['New game versus computer'] },
-					'class': {
-						"tooltip":true,
-						"play": true,
-						"playing": this.mode == "computer" && this.score == "*",
-						"spaceleft": true,
-					},
-				},
-				[h('i', { 'class': { "material-icons": true } }, "computer")])
-			);
-		}
-		if (variant != "Dark" && (["idle","friend"].includes(this.mode)
-			|| (["computer","human"].includes(this.mode) && this.score != "*")))
-		{
-			actionArray.push(
-				h('button',
-				{
-					on: { click: this.clickFriendGame },
-					attrs: { "aria-label": translations['Analysis mode'] },
-					'class': {
-						"tooltip":true,
-						"play": true,
-						"playing": this.mode == "friend",
-						"spaceleft": true,
-					},
-				},
-				[h('i', { 'class': { "material-icons": true } }, "people")])
-			);
-		}
-		if (!!this.vr)
-		{
-			const square00 = document.getElementById("sq-0-0");
-			const squareWidth = !!square00
-				? parseFloat(window.getComputedStyle(square00).width.slice(0,-2))
-				: 0;
-			const settingsBtnElt = document.getElementById("settingsBtn");
-			const settingsStyle = !!settingsBtnElt
-				? window.getComputedStyle(settingsBtnElt)
-				: {width:"46px", height:"26px"};
-			const [indicWidth,indicHeight] = //[44,24];
-			[
-				// NOTE: -2 for border
-				parseFloat(settingsStyle.width.slice(0,-2)) - 2,
-				parseFloat(settingsStyle.height.slice(0,-2)) - 2
-			];
-			let aboveBoardElts = [];
-			if (this.mode == "human")
-			{
-				const connectedIndic = h(
-					'div',
-					{
-						"class": {
-							"indic-left": true,
-							"connected": this.oppConnected,
-							"disconnected": !this.oppConnected,
-						},
-						style: {
-							"width": indicWidth + "px",
-							"height": indicHeight + "px",
-						},
-					}
-				);
-				aboveBoardElts.push(connectedIndic);
-			}
-			if (this.mode == "human" && this.score != "*")
-			{
-				const chatButton = h(
-					'button',
-					{
-						on: { click: this.startChat },
-						attrs: {
-							"aria-label": translations['Start chat'],
-							"id": "chatBtn",
-						},
-						'class': {
-							"tooltip": true,
-							"play": true,
-							"above-board": true,
-							"indic-left": true,
-						},
-					},
-					[h('i', { 'class': { "material-icons": true } }, "chat")]
-				);
-				aboveBoardElts.push(chatButton);
-			}
-			if (["human","computer","friend"].includes(this.mode))
-			{
-				const clearButton = h(
-					'button',
-					{
-						on: { click: this.clearCurrentGame },
-						attrs: {
-							"aria-label": translations['Clear current game'],
-							"id": "clearBtn",
-						},
-						'class': {
-							"tooltip": true,
-							"play": true,
-							"above-board": true,
-							"indic-left": true,
-						},
-					},
-					[h('i', { 'class': { "material-icons": true } }, "clear")]
-				);
-				aboveBoardElts.push(clearButton);
-			}
-			const turnIndic = h(
-				'div',
-				{
-					"class": {
-						"indic-right": true,
-						"white-turn": this.vr.turn=="w",
-						"black-turn": this.vr.turn=="b",
-					},
-					style: {
-						"width": indicWidth + "px",
-						"height": indicHeight + "px",
-					},
-				}
-			);
-			aboveBoardElts.push(turnIndic);
-			elementArray.push(
-				h('div',
-					{ "class": { "aboveboard-wrapper": true } },
-					aboveBoardElts
-				)
-			);
-			if (this.mode == "problem")
-			{
-				// Show problem instructions
-				elementArray.push(
-					h('div',
-						{
-							attrs: { id: "instructions-div" },
-							"class": {
-								"clearer": true,
-								"section-content": true,
-							},
-						},
-						[
-							h('p',
-								{
-									attrs: { id: "problem-instructions" },
-									domProps: { innerHTML: this.problem.instructions }
-								}
-							)
-						]
-					)
-				);
-			}
-			const choices = h('div',
-				{
-					attrs: { "id": "choices" },
-					'class': { 'row': true },
-					style: {
-						"display": this.choices.length>0?"block":"none",
-						"top": "-" + ((sizeY/2)*squareWidth+squareWidth/2) + "px",
-						"width": (this.choices.length * squareWidth) + "px",
-						"height": squareWidth + "px",
-					},
-				},
-				this.choices.map( m => { //a "choice" is a move
-					return h('div',
-						{
-							'class': {
-								'board': true,
-								['board'+sizeY]: true,
-							},
-							style: {
-								'width': (100/this.choices.length) + "%",
-								'padding-bottom': (100/this.choices.length) + "%",
-							},
-						},
-						[h('img',
-							{
-								attrs: { "src": '/images/pieces/' +
-									VariantRules.getPpath(m.appear[0].c+m.appear[0].p) + '.svg' },
-								'class': { 'choice-piece': true },
-								on: {
-									"click": e => { this.play(m); this.choices=[]; },
-									// NOTE: add 'touchstart' event to fix a problem on smartphones
-									"touchstart": e => { this.play(m); this.choices=[]; },
-								},
-							})
-						]
-					);
-				})
-			);
-			// Create board element (+ reserves if needed by variant or mode)
-			const lm = this.vr.lastMove;
-			const showLight = this.hints && variant!="Dark" &&
-				(this.mode != "idle" ||
-					(this.vr.moves.length > 0 && this.cursor==this.vr.moves.length));
-			const gameDiv = h('div',
-				{
-					'class': {
-						'game': true,
-						'clearer': true,
-					},
-				},
-				[_.range(sizeX).map(i => {
-					let ci = (this.mycolor=='w' ? i : sizeX-i-1);
-					return h(
-						'div',
-						{
-							'class': {
-								'row': true,
-							},
-							style: { 'opacity': this.choices.length>0?"0.5":"1" },
-						},
-						_.range(sizeY).map(j => {
-							let cj = (this.mycolor=='w' ? j : sizeY-j-1);
-							let elems = [];
-							if (this.vr.board[ci][cj] != VariantRules.EMPTY && (variant!="Dark"
-								|| this.score!="*" || this.vr.enlightened[this.mycolor][ci][cj]))
-							{
-								elems.push(
-									h(
-										'img',
-										{
-											'class': {
-												'piece': true,
-												'ghost': !!this.selectedPiece
-													&& this.selectedPiece.parentNode.id == "sq-"+ci+"-"+cj,
-											},
-											attrs: {
-												src: "/images/pieces/" +
-													VariantRules.getPpath(this.vr.board[ci][cj]) + ".svg",
-											},
-										}
-									)
-								);
-							}
-							if (this.hints && hintSquares[ci][cj])
-							{
-								elems.push(
-									h(
-										'img',
-										{
-											'class': {
-												'mark-square': true,
-											},
-											attrs: {
-												src: "/images/mark.svg",
-											},
-										}
-									)
-								);
-							}
-							return h(
-								'div',
-								{
-									'class': {
-										'board': true,
-										['board'+sizeY]: true,
-										'light-square': (i+j)%2==0,
-										'dark-square': (i+j)%2==1,
-										[this.bcolor]: true,
-										'in-shadow': variant=="Dark" && this.score=="*"
-											&& !this.vr.enlightened[this.mycolor][ci][cj],
-										'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
-										'incheck': showLight && incheckSq[ci][cj],
-									},
-									attrs: {
-										id: this.getSquareId({x:ci,y:cj}),
-									},
-								},
-								elems
-							);
-						})
-					);
-				}), choices]
-			);
-			if (["human","computer"].includes(this.mode))
-			{
-				if (this.score == "*")
-				{
-					actionArray.push(
-						h('button',
-							{
-								on: { click: this.resign },
-								attrs: { "aria-label": translations['Resign'] },
-								'class': {
-									"tooltip":true,
-									"play": true,
-									"spaceleft": true,
-								},
-							},
-							[h('i', { 'class': { "material-icons": true } }, "flag")])
-					);
-				}
-				else
-				{
-					// A game finished, and another is not started yet: allow navigation
-					actionArray = actionArray.concat([
-						h('button',
-							{
-								on: { click: e => this.undo() },
-								attrs: { "aria-label": translations['Undo'] },
-								"class": {
-									"play": true,
-									"big-spaceleft": true,
-								},
-							},
-							[h('i', { 'class': { "material-icons": true } }, "fast_rewind")]),
-						h('button',
-							{
-								on: { click: e => this.play() },
-								attrs: { "aria-label": translations['Play'] },
-								"class": {
-									"play": true,
-									"spaceleft": true,
-								},
-							},
-							[h('i', { 'class': { "material-icons": true } }, "fast_forward")]),
-						]
-					);
-				}
-			}
-			if (["friend","problem"].includes(this.mode))
-			{
-				actionArray = actionArray.concat(
-				[
-					h('button',
-						{
-							on: { click: this.undoInGame },
-							attrs: { "aria-label": translations['Undo'] },
-							"class": {
-								"play": true,
-								"big-spaceleft": true,
-							},
-						},
-						[h('i', { 'class': { "material-icons": true } }, "undo")]
-					),
-					h('button',
-						{
-							on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } },
-							attrs: { "aria-label": translations['Flip board'] },
-							"class": {
-								"play": true,
-								"spaceleft": true,
-							},
-						},
-						[h('i', { 'class': { "material-icons": true } }, "cached")]
-					),
-				]);
-			}
-			elementArray.push(gameDiv);
-			if (!!this.vr.reserve)
-			{
-				const shiftIdx = (this.mycolor=="w" ? 0 : 1);
-				let myReservePiecesArray = [];
-				for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
-				{
-					myReservePiecesArray.push(h('div',
-					{
-						'class': {'board':true, ['board'+sizeY]:true},
-						attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
-					},
-					[
-						h('img',
-						{
-							'class': {"piece":true, "reserve":true},
-							attrs: {
-								"src": "/images/pieces/" +
-									this.vr.getReservePpath(this.mycolor,i) + ".svg",
-							}
-						}),
-						h('sup',
-							{"class": { "reserve-count": true } },
-							[ this.vr.reserve[this.mycolor][VariantRules.RESERVE_PIECES[i]] ]
-						)
-					]));
-				}
-				let oppReservePiecesArray = [];
-				const oppCol = this.vr.getOppCol(this.mycolor);
-				for (let i=0; i<VariantRules.RESERVE_PIECES.length; i++)
-				{
-					oppReservePiecesArray.push(h('div',
-					{
-						'class': {'board':true, ['board'+sizeY]:true},
-						attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
-					},
-					[
-						h('img',
-						{
-							'class': {"piece":true, "reserve":true},
-							attrs: {
-								"src": "/images/pieces/" +
-									this.vr.getReservePpath(oppCol,i) + ".svg",
-							}
-						}),
-						h('sup',
-							{"class": { "reserve-count": true } },
-							[ this.vr.reserve[oppCol][VariantRules.RESERVE_PIECES[i]] ]
-						)
-					]));
-				}
-				let reserves = h('div',
-					{
-						'class':{
-							'game': true,
-							"reserve-div": true,
-						},
-					},
-					[
-						h('div',
-							{
-								'class': {
-									'row': true,
-									"reserve-row-1": true,
-								},
-							},
-							myReservePiecesArray
-						),
-						h('div',
-							{ 'class': { 'row': true }},
-							oppReservePiecesArray
-						)
-					]
-				);
-				elementArray.push(reserves);
-			}
-			const modalEog = [
-				h('input',
-					{
-						attrs: { "id": "modal-eog", type: "checkbox" },
-						"class": { "modal": true },
-					}),
-				h('div',
-					{
-						attrs: { "role": "dialog", "aria-labelledby": "eogMessage" },
-					},
-					[
-						h('div',
-							{
-								"class": {
-									"card": true,
-									"smallpad": true,
-									"small-modal": true,
-									"text-center": true,
-								},
-							},
-							[
-								h('label',
-									{
-										attrs: { "for": "modal-eog" },
-										"class": { "modal-close": true },
-									}
-								),
-								h('h3',
-									{
-										attrs: { "id": "eogMessage" },
-										"class": { "section": true },
-										domProps: { innerHTML: this.endgameMessage },
-									}
-								)
-							]
-						)
-					]
-				)
-			];
-			elementArray = elementArray.concat(modalEog);
-		}
-		const modalFenEdit = [
-			h('input',
-				{
-					attrs: { "id": "modal-fenedit", type: "checkbox" },
-					"class": { "modal": true },
-				}),
-			h('div',
-				{
-					attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" },
-				},
-				[
-					h('div',
-						{
-							"class": { "card": true, "smallpad": true },
-						},
-						[
-							h('label',
-								{
-									attrs: { "id": "close-fenedit", "for": "modal-fenedit" },
-									"class": { "modal-close": true },
-								}
-							),
-							h('h3',
-								{
-									attrs: { "id": "titleFenedit" },
-									"class": { "section": true },
-									domProps: { innerHTML: translations["Game state (FEN):"] },
-								}
-							),
-							h('input',
-								{
-									attrs: {
-										"id": "input-fen",
-										type: "text",
-										value: VariantRules.GenRandInitFen(),
-									},
-								}
-							),
-							h('button',
-								{
-									on: { click:
-										() => {
-											const fen = document.getElementById("input-fen").value;
-											document.getElementById("modal-fenedit").checked = false;
-											this.newGame("friend", fen);
-										}
-									},
-									domProps: { innerHTML: translations["Ok"] },
-								}
-							),
-							h('button',
-								{
-									on: { click:
-										() => {
-											document.getElementById("input-fen").value =
-												VariantRules.GenRandInitFen();
-										}
-									},
-									domProps: { innerHTML: translations["Random"] },
-								}
-							),
-						]
-					)
-				]
-			)
-		];
-		elementArray = elementArray.concat(modalFenEdit);
-		let chatEltsArray =
-		[
-			h('label',
-				{
-					attrs: { "id": "close-chat", "for": "modal-chat" },
-					"class": { "modal-close": true },
-				}
-			),
-			h('h3',
-				{
-					attrs: { "id": "titleChat" },
-					"class": { "section": true },
-					domProps: { innerHTML: translations["Chat with "] + this.oppName },
-				}
-			)
-		];
-		for (let chat of this.chats)
-		{
-			chatEltsArray.push(
-				h('p',
-					{
-						"class": {
-							"my-chatmsg": chat.author==this.myid,
-							"opp-chatmsg": chat.author==this.oppid,
-						},
-						domProps: { innerHTML: chat.msg }
-					}
-				)
-			);
-		}
-		chatEltsArray = chatEltsArray.concat([
-			h('input',
-				{
-					attrs: {
-						"id": "input-chat",
-						type: "text",
-						placeholder: translations["Type here"],
-					},
-					on: { keyup: this.trySendChat }, //if key is 'enter'
-				}
-			),
-			h('button',
-				{
-					attrs: { id: "sendChatBtn"},
-					on: { click: this.sendChat },
-					domProps: { innerHTML: translations["Send"] },
-				}
-			)
-		]);
-		const modalChat = [
-			h('input',
-				{
-					attrs: { "id": "modal-chat", type: "checkbox" },
-					"class": { "modal": true },
-				}),
-			h('div',
-				{
-					attrs: { "role": "dialog", "aria-labelledby": "titleChat" },
-				},
-				[
-					h('div',
-						{
-							"class": { "card": true, "smallpad": true },
-						},
-						chatEltsArray
-					)
-				]
-			)
-		];
-		elementArray = elementArray.concat(modalChat);
-		const actions = h('div',
-			{
-				attrs: { "id": "actions" },
-				'class': { 'text-center': true },
-			},
-			actionArray
-		);
-		elementArray.push(actions);
-		if (!!this.vr)
-		{
-			if (this.mode == "problem")
-			{
-				// Show problem solution (on click)
-				elementArray.push(
-					h('div',
-						{
-							attrs: { id: "solution-div" },
-							"class": { "section-content": true },
-						},
-						[
-							h('h3',
-								{
-									"class": { clickable: true },
-									domProps: { innerHTML: translations["Show solution"] },
-									on: { click: this.toggleShowSolution },
-								}
-							),
-							h('p',
-								{
-									attrs: { id: "problem-solution" },
-									domProps: { innerHTML: this.problem.solution }
-								}
-							)
-						]
-					)
-				);
-			}
-			if (variant != "Dark" || this.score!="*")
-			{
-				// Show current FEN
-				elementArray.push(
-					h('div',
-						{
-							attrs: { id: "fen-div" },
-							"class": { "section-content": true },
-						},
-						[
-							h('p',
-								{
-									attrs: { id: "fen-string" },
-									domProps: { innerHTML: this.vr.getBaseFen() },
-									"class": { "text-center": true },
-								}
-							)
-						]
-					)
-				);
-			}
-			elementArray.push(
-				h('div',
-					{
-						attrs: { id: "pgn-div" },
-						"class": { "section-content": true },
-					},
-					[
-						h('a',
-							{
-								attrs: {
-									id: "download",
-									href: "#",
-								}
-							}
-						),
-						h('button',
-							{
-								attrs: { "id": "downloadBtn" },
-								on: { click: this.download },
-								domProps: { innerHTML: translations["Download PGN"] },
-							}
-						),
-					]
-				)
-			);
-		}
-		return h(
-			'div',
-			{
-				'class': {
-					"col-sm-12":true,
-					"col-md-10":true,
-					"col-md-offset-1":true,
-					"col-lg-8":true,
-					"col-lg-offset-2":true,
-				},
-				// NOTE: click = mousedown + mouseup
-				on: {
-					mousedown: this.mousedown,
-					mousemove: this.mousemove,
-					mouseup: this.mouseup,
-					touchstart: this.mousedown,
-					touchmove: this.mousemove,
-					touchend: this.mouseup,
-				},
-			},
-			elementArray
-		);
-	},
+	// Modal end of game, and then sub-components
+	template: `
+		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
+			<input id="modal-eog" type="checkbox" class="modal"/>
+			<div role="dialog" aria-labelledby="eogMessage">
+				<div class="card smallpad small-modal text-center">
+					<label for="modal-eog" class="modal-close"></label>
+					<h3 id="eogMessage" class="section">{{ endgameMessage }}</h3>
+
+			<my-chat v-if="showChat"></my-chat>
+			//TODO: connection + turn indicators en haut à droite (superposé au menu)
+			<my-board></my-board>
+			// TODO: controls: abort, clear, resign, draw (avec confirm box)
+			// et si partie terminée : (mode analyse) just clear, back / play
+			// + flip button toujours disponible
+			
+			<div id="pgn-div" class="section-content">
+				<a id="download" href: "#"></a>
+				<button id="downloadBtn" @click="download">
+					{{ translations["Download PGN"] }}
+				</button>
+			
+			<my-move-list v-if="showMoves"></my-move-list>
+		</div>
+	`,
 	computed: {
 		endgameMessage: function() {
 			let eogMessage = "Unfinished";
-			switch (this.score)
+			switch (this.game.score)
 			{
 				case "1-0":
 					eogMessage = translations["White win"];
@@ -799,19 +82,11 @@ Vue.component('my-game', {
 	},
 	created: function() {
 		const url = socketUrl;
-		const humanContinuation = (localStorage.getItem("variant") === variant);
-		const computerContinuation = (localStorage.getItem("comp-variant") === variant);
-		const friendContinuation = (localStorage.getItem("anlz-variant") === variant);
-		this.myid = (humanContinuation ? localStorage.getItem("myid") : getRandString());
 		this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
-		const socketOpenListener = () => {
-			if (humanContinuation) //game VS human has priority
-				this.continueGame("human");
-			else if (computerContinuation)
-				this.continueGame("computer");
-			else if (friendContinuation)
-				this.continueGame("friend");
-		};
+//		const socketOpenListener = () => {
+//		};
+
+// TODO: after game, archive in indexedDB
 
 		// TODO: this events listener is central. Refactor ? How ?
 		const socketMessageListener = msg => {
@@ -819,25 +94,6 @@ Vue.component('my-game', {
 			let L = undefined;
 			switch (data.code)
 			{
-				case "oppname":
-					// Receive opponent's name
-					this.oppName = data.name;
-					break;
-				case "newchat":
-					// Receive new chat
-					this.chats.push({msg:data.msg, author:this.oppid});
-					break;
-				case "duplicate":
-					// We opened another tab on the same game
-					this.mode = "idle";
-					this.vr = null;
-					alert(translations[
-						"Already playing a game in this variant on another tab!"]);
-					break;
-				case "newgame": //opponent found
-					// oppid: opponent socket ID
-					this.newGame("human", data.fen, data.color, data.oppid, data.gameid);
-					break;
 				case "newmove": //..he played!
 					this.play(data.move, (variant!="Dark" ? "animate" : null));
 					break;
@@ -906,14 +162,17 @@ Vue.component('my-game', {
 
 		const socketCloseListener = () => {
 			this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
-			this.conn.addEventListener('open', socketOpenListener);
+			//this.conn.addEventListener('open', socketOpenListener);
 			this.conn.addEventListener('message', socketMessageListener);
 			this.conn.addEventListener('close', socketCloseListener);
 		};
-		this.conn.onopen = socketOpenListener;
+		//this.conn.onopen = socketOpenListener;
 		this.conn.onmessage = socketMessageListener;
 		this.conn.onclose = socketCloseListener;
+		
+		
 		// Listen to keyboard left/right to navigate in game
+		// TODO: also mouse wheel !
 		document.onkeydown = event => {
 			if (["human","computer"].includes(this.mode) &&
 				!!this.vr && this.vr.moves.length > 0 && [37,39].includes(event.keyCode))
@@ -925,6 +184,8 @@ Vue.component('my-game', {
 					this.play();
 			}
 		};
+
+
 		// Computer moves web worker logic: (TODO: also for observers in HH games)
 		this.compWorker.postMessage(["scripts",variant]);
 		const self = this;
@@ -951,59 +212,12 @@ Vue.component('my-game', {
 			}, delay);
 		}
 	},
-	methods: {
-		// TODO: in settings.js
-		setMyname: function(e) {
-			this.myname = e.target.value;
-			localStorage["username"] = this.myname;
-		},
-		showSettings: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			document.getElementById("modal-settings").checked = true;
-		},
-		toggleHints: function() {
-			this.hints = !this.hints;
-			localStorage["hints"] = (this.hints ? "1" : "0");
-		},
-		setBoardColor: function(e) {
-			this.bcolor = e.target.options[e.target.selectedIndex].value;
-			localStorage["bcolor"] = this.bcolor;
-		},
-		setSound: function(e) {
-			this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
-			localStorage["sound"] = this.sound;
-		},
-
-		// TODO: in another component
-		trySendChat: function(e) {
-			if (e.keyCode == 13) //'enter' key
-				this.sendChat();
-		},
-		sendChat: function() {
-			let chatInput = document.getElementById("input-chat");
-			const chatTxt = chatInput.value;
-			chatInput.value = "";
-			this.chats.push({msg:chatTxt, author:this.myid});
-			this.conn.send(JSON.stringify({
-				code:"newchat", oppid: this.oppid, msg: chatTxt}));
-		},
-		startChat: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			document.getElementById("modal-chat").checked = true;
-		},
 
-		// TODO: in  problems component
-		toggleShowSolution: function() {
-			let problemSolution = document.getElementById("problem-solution");
-			problemSolution.style.display =
-				!problemSolution.style.display || problemSolution.style.display == "none"
-					? "block"
-					: "none";
-		},
 
+	methods: {
 		download: function() {
 			// Variants may have special PGN structure (so next function isn't defined here)
-			const content = this.vr.getPGN(this.mycolor, this.score, this.fenStart, this.mode);
+			const content = V.GetPGN(this.moves, this.mycolor, this.score, this.fenStart, this.mode);
 			// Prepare and trigger download link
 			let downloadAnchor = document.getElementById("download");
 			downloadAnchor.setAttribute("download", "game.pgn");
@@ -1031,217 +245,6 @@ Vue.component('my-game', {
 			}
 			this.cursor = this.vr.moves.length; //to navigate in finished game
 		},
-
-		// TODO: elsewhere (general methods to access/retrieve from storage, to be generalized)
-		// https://developer.mozilla.org/fr/docs/Web/API/API_IndexedDB
-		// https://dexie.org/
-		getStoragePrefix: function(mode) {
-			let prefix = "";
-			if (mode == "computer")
-				prefix = "comp-";
-			else if (mode == "friend")
-				prefix = "anlz-";
-			return prefix;
-		},
-		setStorage: function() {
-			if (this.mode=="human")
-			{
-				localStorage.setItem("myid", this.myid);
-				localStorage.setItem("oppid", this.oppid);
-				localStorage.setItem("gameId", this.gameId);
-			}
-			const prefix = this.getStoragePrefix(this.mode);
-			localStorage.setItem(prefix+"variant", variant);
-			localStorage.setItem(prefix+"mycolor", this.mycolor);
-			localStorage.setItem(prefix+"fenStart", this.fenStart);
-			localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
-			localStorage.setItem(prefix+"fen", this.vr.getFen());
-			localStorage.setItem(prefix+"score", "*");
-		},
-		updateStorage: function() {
-			const prefix = this.getStoragePrefix(this.mode);
-			localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
-			localStorage.setItem(prefix+"fen", this.vr.getFen());
-			if (this.score != "*")
-				localStorage.setItem(prefix+"score", this.score);
-		},
-		// "computer mode" clearing is done through the menu
-		clearStorage: function() {
-			if (this.mode == "human")
-			{
-				delete localStorage["myid"];
-				delete localStorage["oppid"];
-				delete localStorage["gameId"];
-			}
-			const prefix = this.getStoragePrefix(this.mode);
-			delete localStorage[prefix+"variant"];
-			delete localStorage[prefix+"mycolor"];
-			delete localStorage[prefix+"fenStart"];
-			delete localStorage[prefix+"moves"];
-			delete localStorage[prefix+"fen"];
-			delete localStorage[prefix+"score"];
-		},
-		clearCurrentGame: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			this.clearStorage();
-			location.reload(); //to see clearing effects
-		},
-
-		// HACK because mini-css tooltips are persistent after click...
-		// NOTE: seems to work only in chrome/chromium. TODO...
-		getRidOfTooltip: function(elt) {
-			elt.style.visibility = "hidden";
-			setTimeout(() => { elt.style.visibility="visible"; }, 100);
-		},
-
-		// TODO: elsewhere, probably (new game button)
-		clickGameSeek: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			if (this.mode == "human" && this.score == "*")
-				return; //no newgame while playing
-			if (this.seek)
-			{
-				this.conn.send(JSON.stringify({code:"cancelnewgame"}));
-				this.seek = false;
-			}
-			else
-				this.newGame("human");
-		},
-		clickComputerGame: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			if (this.mode == "computer" && this.score == "*"
-				&& this.vr.turn != this.mycolor)
-			{
-				// Wait for computer reply first (avoid potential "ghost move" bug)
-				return;
-			}
-			this.newGame("computer");
-		},
-		clickFriendGame: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			document.getElementById("modal-fenedit").checked = true;
-		},
-		// In main hall :
-		newGame: function(mode, fenInit, color, oppId, gameId) {
-			const fen = fenInit || VariantRules.GenRandInitFen();
-			console.log(fen); //DEBUG
-			if (mode=="human" && !oppId)
-			{
-				const storageVariant = localStorage.getItem("variant");
-				if (!!storageVariant && storageVariant !== variant
-					&& localStorage["score"] == "*")
-				{
-					return alert(translations["Finish your "] +
-						storageVariant + translations[" game first!"]);
-				}
-				// Send game request and wait..
-				try {
-					this.conn.send(JSON.stringify({code:"newgame", fen:fen, gameid: getRandString() }));
-				} catch (INVALID_STATE_ERR) {
-					return; //nothing achieved
-				}
-				this.seek = true;
-				let modalBox = document.getElementById("modal-newgame");
-				modalBox.checked = true;
-				setTimeout(() => { modalBox.checked = false; }, 2000);
-				return;
-			}
-			const prefix = this.getStoragePrefix(mode);
-			if (mode == "computer")
-			{
-				const storageVariant = localStorage.getItem(prefix+"variant");
-				if (!!storageVariant)
-				{
-					const score = localStorage.getItem(prefix+"score");
-					if (storageVariant !== variant && score == "*")
-					{
-						if (!confirm(storageVariant +
-							translations[": unfinished computer game will be erased"]))
-						{
-							return;
-						}
-					}
-				}
-			}
-			else if (mode == "friend")
-			{
-				const storageVariant = localStorage.getItem(prefix+"variant");
-				if (!!storageVariant)
-				{
-					const score = localStorage.getItem(prefix+"score");
-					if (storageVariant !== variant && score == "*")
-					{
-						if (!confirm(storageVariant +
-							translations[": current analysis will be erased"]))
-						{
-							return;
-						}
-					}
-				}
-			}
-			this.vr = new VariantRules(fen, []);
-			this.score = "*";
-			this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
-			this.mode = mode;
-			this.incheck = [];
-			this.fenStart = V.ParseFen(fen).position; //this is enough
-			if (mode=="human")
-			{
-				// Opponent found!
-				this.gameId = gameId;
-				this.oppid = oppId;
-				this.oppConnected = true;
-				this.mycolor = color;
-				this.seek = false;
-				if (this.sound >= 1)
-					new Audio("/sounds/newgame.mp3").play().catch(err => {});
-				document.getElementById("modal-newgame").checked = false;
-			}
-			else if (mode == "computer")
-			{
-				this.compWorker.postMessage(["init",this.vr.getFen()]);
-				this.mycolor = (Math.random() < 0.5 ? 'w' : 'b');
-				if (this.mycolor != this.vr.turn)
-					this.playComputerMove();
-			}
-			else if (mode == "friend")
-				this.mycolor = "w"; //convention...
-			//else: problem solving: nothing more to do
-			if (mode != "problem")
-				this.setStorage(); //store game state in case of interruptions
-		},
-		continueGame: function(mode) {
-			this.mode = mode;
-			this.oppid = (mode=="human" ? localStorage.getItem("oppid") : undefined);
-			const prefix = this.getStoragePrefix(mode);
-			this.mycolor = localStorage.getItem(prefix+"mycolor");
-			const moves = JSON.parse(localStorage.getItem(prefix+"moves"));
-			const fen = localStorage.getItem(prefix+"fen");
-			const score = localStorage.getItem(prefix+"score"); //set in "endGame()"
-			this.fenStart = localStorage.getItem(prefix+"fenStart");
-			this.vr = new VariantRules(fen, moves);
-			this.incheck = this.vr.getCheckSquares(this.vr.turn);
-			if (mode == "human")
-			{
-				this.gameId = localStorage.getItem("gameId");
-				// Send ping to server (answer pong if opponent is connected)
-				this.conn.send(JSON.stringify({
-					code:"ping",oppid:this.oppid,gameId:this.gameId}));
-			}
-			else if (mode == "computer")
-			{
-				this.compWorker.postMessage(["init",fen]);
-				if (score == "*" && this.mycolor != this.vr.turn)
-					this.playComputerMove();
-			}
-			//else: nothing special to do in friend mode
-			if (score != "*")
-			{
-				// Small delay required when continuation run faster than drawing page
-				setTimeout(() => this.endGame(score), 100);
-			}
-		},
-
 		resign: function(e) {
 			this.getRidOfTooltip(e.currentTarget);
 			if (this.mode == "human" && this.oppConnected)
@@ -1258,147 +261,6 @@ Vue.component('my-game', {
 			this.timeStart = Date.now();
 			this.compWorker.postMessage(["askmove"]);
 		},
-
-		// TODO: purely graphical, move in a "chessground-like" component
-		// Get the identifier of a HTML table cell 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;
-			let elem = e.target;
-			while (!ingame && elem !== null)
-			{
-				if (elem.classList.contains("game"))
-				{
-					ingame = true;
-					break;
-				}
-				elem = elem.parentElement;
-			}
-			if (!ingame) //let default behavior (click on button...)
-				return;
-			e.preventDefault(); //disable native drag & drop
-			if (!this.selectedPiece && e.target.classList.contains("piece"))
-			{
-				// Next few lines to center the piece on mouse cursor
-				let rect = e.target.parentNode.getBoundingClientRect();
-				this.start = {
-					x: rect.x + rect.width/2,
-					y: rect.y + rect.width/2,
-					id: e.target.parentNode.id
-				};
-				this.selectedPiece = e.target.cloneNode();
-				this.selectedPiece.style.position = "absolute";
-				this.selectedPiece.style.top = 0;
-				this.selectedPiece.style.display = "inline-block";
-				this.selectedPiece.style.zIndex = 3000;
-				const startSquare = this.getSquareFromId(e.target.parentNode.id);
-				this.possibleMoves = [];
-				if (this.score == "*")
-				{
-					const color = ["friend","problem"].includes(this.mode)
-						? this.vr.turn
-						: this.mycolor;
-					if (this.vr.canIplay(color,startSquare))
-						this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
-				}
-				// Next line add moving piece just after current image
-				// (required for Crazyhouse reserve)
-				e.target.parentNode.insertBefore(this.selectedPiece, e.target.nextSibling);
-			}
-		},
-		mousemove: function(e) {
-			if (!this.selectedPiece)
-				return;
-			e = e || window.event;
-			// If there is an active element, move it around
-			if (!!this.selectedPiece)
-			{
-				const [offsetX,offsetY] = !!e.clientX
-					? [e.clientX,e.clientY] //desktop browser
-					: [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
-				this.selectedPiece.style.left = (offsetX-this.start.x) + "px";
-				this.selectedPiece.style.top = (offsetY-this.start.y) + "px";
-			}
-		},
-		mouseup: function(e) {
-			if (!this.selectedPiece)
-				return;
-			e = e || window.event;
-			// Read drop target (or parentElement, parentNode... if type == "img")
-			this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
-			const [offsetX,offsetY] = !!e.clientX
-				? [e.clientX,e.clientY]
-				: [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
-			let landing = document.elementFromPoint(offsetX, offsetY);
-			this.selectedPiece.style.zIndex = 3000;
-			// Next condition: classList.contains(piece) fails because of marks
-			while (landing.tagName == "IMG")
-				landing = landing.parentNode;
-			if (this.start.id == landing.id)
-			{
-				// A click: selectedPiece and possibleMoves are already filled
-				return;
-			}
-			// OK: process move attempt
-			let endSquare = this.getSquareFromId(landing.id);
-			let moves = this.findMatchingMoves(endSquare);
-			this.possibleMoves = [];
-			if (moves.length > 1)
-				this.choices = moves;
-			else if (moves.length==1)
-				this.play(moves[0]);
-			// Else: impossible move
-			this.selectedPiece.parentNode.removeChild(this.selectedPiece);
-			delete this.selectedPiece;
-			this.selectedPiece = null;
-		},
-		findMatchingMoves: function(endSquare) {
-			// Run through moves list and return the matching set (if promotions...)
-			let moves = [];
-			this.possibleMoves.forEach(function(m) {
-				if (endSquare[0] == m.end.x && endSquare[1] == m.end.y)
-					moves.push(m);
-			});
-			return moves;
-		},
-		animateMove: function(move) {
-			let startSquare = document.getElementById(this.getSquareId(move.start));
-			let endSquare = document.getElementById(this.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");
-			// 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))
-					square.style.zIndex = "-1";
-			}
-			movingPiece.style.transform = "translate(" + translation.x + "px," +
-				translation.y + "px)";
-			movingPiece.style.transitionDuration = "0.2s";
-			movingPiece.style.zIndex = "3000";
-			setTimeout( () => {
-				for (let i=0; i<squares.length; i++)
-					squares.item(i).style.zIndex = "auto";
-				movingPiece.style = {}; //required e.g. for 0-0 with KR swap
-				this.play(move);
-			}, 250);
-		},
-
 		// OK, these last functions can stay here (?!)
 		play: function(move, programmatic) {
 			if (!move)
diff --git a/public/javascripts/components/moveList.js b/public/javascripts/components/moveList.js
new file mode 100644
index 00000000..97980040
--- /dev/null
+++ b/public/javascripts/components/moveList.js
@@ -0,0 +1 @@
+//TODO: component for moves list on the right
diff --git a/public/javascripts/components/problemPreview.js b/public/javascripts/components/problemPreview.js
new file mode 100644
index 00000000..53365035
--- /dev/null
+++ b/public/javascripts/components/problemPreview.js
@@ -0,0 +1,25 @@
+// Preview a problem on variant page
+Vue.component('my-problem-preview', {
+	props: ['prob'],
+	template: `
+		<div class="row problem">
+			<div class="col-sm-12 col-md-6 diagram"
+				v-html="getDiagram(prob.fen)">
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<p v-html="prob.instructions"></p>
+				<p v-html="prob.solution"></p>
+			</div>
+		</div>
+	`,
+	methods: {
+		getDiagram: function(fen) {
+			const fenParsed = V.ParseFen(fen);
+			return getDiagram({
+				position: fenParsed.position,
+				turn: fenParsed.turn,
+				// No need for flags here
+			});
+		},
+	},
+})
diff --git a/public/javascripts/components/problemSummary.js b/public/javascripts/components/problemSummary.js
deleted file mode 100644
index 5f0fd44b..00000000
--- a/public/javascripts/components/problemSummary.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// Show a problem summary on variant page or new problem preview
-Vue.component('my-problem-summary', {
-	props: ['prob','preview'],
-	template: `
-		<div class="row problem">
-			<div class="col-sm-12 col-md-6 diagram"
-				v-html="getDiagram(prob.fen)">
-			</div>
-			<div class="col-sm-12 col-md-6">
-				<p v-html="prob.instructions"></p>
-				<p v-if="preview" v-html="prob.solution"></p>
-				<p v-else class="problem-time">{{ timestamp2date(prob.added) }}</p>
-				<button v-if="!preview" @click="showProblem()">{{ translate("Solve") }}</button>
-			</div>
-		</div>
-	`,
-	methods: {
-		translate: function(text) {
-			return translations[text];
-		},
-		getDiagram: function(fen) {
-			const fenParsed = V.ParseFen(fen);
-			return getDiagram({
-				position: fenParsed.position,
-				turn: fenParsed.turn,
-				// No need for flags here
-			});
-		},
-		timestamp2date(ts) {
-			return getDate(new Date(ts));
-		},
-		// Propagate "show problem" event to parent component (my-problems)
-		showProblem: function() {
-			this.$emit('show-problem');
-		},
-	},
-})
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index bcd069cc..ad8c54c0 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -1,38 +1,98 @@
 Vue.component('my-problems', {
 	data: function () {
 		return {
-			problems: [],
+			problems: [], //oldest first
+			curIdx: 0, //index in problems array
+			stage: "nothing", //or "preview" after new problem is filled
 			newProblem: {
 				fen: "",
 				instructions: "",
 				solution: "",
-				stage: "nothing", //or "preview" after new problem is filled
 			},
 		};
 	},
 	template: `
 		<div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
 			<div id="problemControls" class="button-group">
-				<button :aria-label='translate("Load previous problems")' class="tooltip"
-						@click="fetchProblems('backward')">
+				<button :aria-label='translate("Load previous problem")' class="tooltip"
+						@click="showPreviousProblem()">
 					<i class="material-icons">skip_previous</i>
 				</button>
 				<button :aria-label='translate("Add a problem")' class="tooltip"
 						@click="showNewproblemModal">
 					{{ translate("New") }}
 				</button>
-				<button :aria-label='translate("Load next problems")' class="tooltip"
-						@click="fetchProblems('forward')">
+				<button :aria-label='translate("Load next problem")' class="tooltip"
+						@click="showNextProblem()">
 					<i class="material-icons">skip_next</i>
 				</button>
 			</div>
-			<my-problem-summary v-on:show-problem="bubbleUp(p)"
-				v-for="(p,idx) in sortedProblems"
+		
+
+
+			if (this.mode == "problem")
+			{
+				// Show problem instructions
+				elementArray.push(
+					h('div',
+						{
+							attrs: { id: "instructions-div" },
+							"class": {
+								"clearer": true,
+								"section-content": true,
+							},
+						},
+						[
+							h('p',
+								{
+									attrs: { id: "problem-instructions" },
+									domProps: { innerHTML: this.problem.instructions }
+								}
+							)
+						]
+					)
+				);
+			}
+
+
+			// TODO ici :: instrus + diag interactif + solution
+			my-board + pilotage via movesList + VariantRules !
+			
+			<my-problem-preview v-show="stage=='preview'"
+				v-for="(p,idx) in problems"
 				v-bind:prob="p" v-bind:preview="false" v-bind:key="idx">
 			</my-problem-summary>
+			if (this.mode == "problem")
+			{
+				// Show problem solution (on click)
+				elementArray.push(
+					h('div',
+						{
+							attrs: { id: "solution-div" },
+							"class": { "section-content": true },
+						},
+						[
+							h('h3',
+								{
+									"class": { clickable: true },
+									domProps: { innerHTML: translations["Show solution"] },
+									on: { click: this.toggleShowSolution },
+								}
+							),
+							h('p',
+								{
+									attrs: { id: "problem-solution" },
+									domProps: { innerHTML: this.problem.solution }
+								}
+							)
+						]
+					)
+				);
+			}
+			
 			<input type="checkbox" id="modal-newproblem" class="modal">
 			<div role="dialog" aria-labelledby="newProblemTxt">
-				<div v-show="newProblem.stage=='nothing'" class="card newproblem-form">
+				<div v-show="stage=='nothing'" class="card newproblem-form">
 					<label for="modal-newproblem" class="modal-close"></label>
 					<h3 id="newProblemTxt">{{ translate("Add a problem") }}</h3>
 					<form @submit.prevent="previewNewProblem">
@@ -53,10 +113,9 @@ Vue.component('my-problems', {
 						</fieldset>
 					</form>
 				</div>
-				<div v-show="newProblem.stage=='preview'" class="card newproblem-preview">
+				<div v-show="stage=='preview'" class="card newproblem-preview">
 					<label for="modal-newproblem" class="modal-close"></label>
-					<my-problem-summary v-bind:prob="newProblem" v-bind:preview="true">
-					</my-problem-summary>
+					<my-problem-preview v-bind:prob="newProblem"></my-problem-summary>
 					<div class="button-group">
 						<button @click="newProblem.stage='nothing'">{{ translate("Cancel") }}</button>
 						<button @click="sendNewProblem()">{{ translate("Send") }}</button>
@@ -68,10 +127,10 @@ Vue.component('my-problems', {
 	computed: {
 		sortedProblems: function() {
 			// Newest problem first
-			return this.problems.sort((p1,p2) => { return p2.added - p1.added; });
 		},
 	},
 	created: function() {
+		// Analyse URL: if a single problem required, show it. Otherwise,
 		// TODO: fetch most recent problems from server
 	},
 	methods: {
@@ -83,6 +142,26 @@ Vue.component('my-problems', {
 //		bubbleUp: function(problem) {
 //			this.$emit('show-problem', JSON.stringify(problem));
 //		},
+		toggleShowSolution: function() {
+			let problemSolution = document.getElementById("problem-solution");
+			problemSolution.style.display =
+				!problemSolution.style.display || problemSolution.style.display == "none"
+					? "block"
+					: "none";
+		},
+		showPreviousProblem: function() {
+			if (this.curIdx == 0)
+				this.fetchProblems("backward");
+			else
+				this.curIdx--;
+		},
+		showNextProblem: function() {
+			if (this.curIdx == this.problems.length - 1)
+				this.fetchProblems("forward");
+			else
+				this.curIdx++;
+		},
+		// TODO: modal "no more problems"
 		fetchProblems: function(direction) {
 			if (this.problems.length == 0)
 				return; //what could we do?!
@@ -101,7 +180,11 @@ Vue.component('my-problems', {
 				last_dt: last_dt,
 			}, response => {
 				if (response.problems.length > 0)
-					this.problems = response.problems;
+				{
+					this.problems = response.problems
+						.sort((p1,p2) => { return p1.added - p2.added; });
+					this.curIdx = response.problems.length - 1;
+				}
 			});
 		},
 		showNewproblemModal: function() {
diff --git a/public/javascripts/components/room.js b/public/javascripts/components/room.js
index 941434b4..16fcc903 100644
--- a/public/javascripts/components/room.js
+++ b/public/javascripts/components/room.js
@@ -18,3 +18,225 @@ chat général (gauche, activé ou non (bool global storage)).
 quand je poste un lastMove corr, supprimer mon ancien lastMove le cas échéant (tlm l'a eu)
 fin de partie corr: garder maxi nbPlayers lastMove sur serveur, pendant 7 jours (arbitraire)
 */
+				case "newgame": //opponent found
+					// oppid: opponent socket ID
+					this.newGame("human", data.fen, data.color, data.oppid, data.gameid);
+					break;
+
+		// TODO: elsewhere, probably (new game button)
+		clickGameSeek: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			if (this.mode == "human" && this.score == "*")
+				return; //no newgame while playing
+			if (this.seek)
+			{
+				this.conn.send(JSON.stringify({code:"cancelnewgame"}));
+				this.seek = false;
+			}
+			else
+				this.newGame("human");
+		},
+		clickComputerGame: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			if (this.mode == "computer" && this.score == "*"
+				&& this.vr.turn != this.mycolor)
+			{
+				// Wait for computer reply first (avoid potential "ghost move" bug)
+				return;
+			}
+			this.newGame("computer");
+		},
+		clickFriendGame: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			document.getElementById("modal-fenedit").checked = true;
+		},
+		// In main hall :
+		newGame: function(mode, fenInit, color, oppId, gameId) {
+			const fen = fenInit || VariantRules.GenRandInitFen();
+			console.log(fen); //DEBUG
+			if (mode=="human" && !oppId)
+			{
+				const storageVariant = localStorage.getItem("variant");
+				if (!!storageVariant && storageVariant !== variant
+					&& localStorage["score"] == "*")
+				{
+					return alert(translations["Finish your "] +
+						storageVariant + translations[" game first!"]);
+				}
+				// Send game request and wait..
+				try {
+					this.conn.send(JSON.stringify({code:"newgame", fen:fen, gameid: getRandString() }));
+				} catch (INVALID_STATE_ERR) {
+					return; //nothing achieved
+				}
+				this.seek = true;
+				let modalBox = document.getElementById("modal-newgame");
+				modalBox.checked = true;
+				setTimeout(() => { modalBox.checked = false; }, 2000);
+				return;
+			}
+			const prefix = this.getStoragePrefix(mode);
+			if (mode == "computer")
+			{
+				const storageVariant = localStorage.getItem(prefix+"variant");
+				if (!!storageVariant)
+				{
+					const score = localStorage.getItem(prefix+"score");
+					if (storageVariant !== variant && score == "*")
+					{
+						if (!confirm(storageVariant +
+							translations[": unfinished computer game will be erased"]))
+						{
+							return;
+						}
+					}
+				}
+			}
+			else if (mode == "friend")
+			{
+				const storageVariant = localStorage.getItem(prefix+"variant");
+				if (!!storageVariant)
+				{
+					const score = localStorage.getItem(prefix+"score");
+					if (storageVariant !== variant && score == "*")
+					{
+						if (!confirm(storageVariant +
+							translations[": current analysis will be erased"]))
+						{
+							return;
+						}
+					}
+				}
+			}
+			this.vr = new VariantRules(fen, []);
+			this.score = "*";
+			this.pgnTxt = ""; //redundant with this.score = "*", but cleaner
+			this.mode = mode;
+			this.incheck = [];
+			this.fenStart = V.ParseFen(fen).position; //this is enough
+			if (mode=="human")
+			{
+				// Opponent found!
+				this.gameId = gameId;
+				this.oppid = oppId;
+				this.oppConnected = true;
+				this.mycolor = color;
+				this.seek = false;
+				if (this.sound >= 1)
+					new Audio("/sounds/newgame.mp3").play().catch(err => {});
+				document.getElementById("modal-newgame").checked = false;
+			}
+			else if (mode == "computer")
+			{
+				this.compWorker.postMessage(["init",this.vr.getFen()]);
+				this.mycolor = (Math.random() < 0.5 ? 'w' : 'b');
+				if (this.mycolor != this.vr.turn)
+					this.playComputerMove();
+			}
+			else if (mode == "friend")
+				this.mycolor = "w"; //convention...
+			//else: problem solving: nothing more to do
+			if (mode != "problem")
+				this.setStorage(); //store game state in case of interruptions
+		},
+		continueGame: function(mode) {
+			this.mode = mode;
+			this.oppid = (mode=="human" ? localStorage.getItem("oppid") : undefined);
+			const prefix = this.getStoragePrefix(mode);
+			this.mycolor = localStorage.getItem(prefix+"mycolor");
+			const moves = JSON.parse(localStorage.getItem(prefix+"moves"));
+			const fen = localStorage.getItem(prefix+"fen");
+			const score = localStorage.getItem(prefix+"score"); //set in "endGame()"
+			this.fenStart = localStorage.getItem(prefix+"fenStart");
+			this.vr = new VariantRules(fen, moves);
+			this.incheck = this.vr.getCheckSquares(this.vr.turn);
+			if (mode == "human")
+			{
+				this.gameId = localStorage.getItem("gameId");
+				// Send ping to server (answer pong if opponent is connected)
+				this.conn.send(JSON.stringify({
+					code:"ping",oppid:this.oppid,gameId:this.gameId}));
+			}
+			else if (mode == "computer")
+			{
+				this.compWorker.postMessage(["init",fen]);
+				if (score == "*" && this.mycolor != this.vr.turn)
+					this.playComputerMove();
+			}
+			//else: nothing special to do in friend mode
+			if (score != "*")
+			{
+				// Small delay required when continuation run faster than drawing page
+				setTimeout(() => this.endGame(score), 100);
+			}
+		},
+		
+	
+	// TODO: option du bouton "new game"
+	const modalFenEdit = [
+			h('input',
+				{
+					attrs: { "id": "modal-fenedit", type: "checkbox" },
+					"class": { "modal": true },
+				}),
+			h('div',
+				{
+					attrs: { "role": "dialog", "aria-labelledby": "titleFenedit" },
+				},
+				[
+					h('div',
+						{
+							"class": { "card": true, "smallpad": true },
+						},
+						[
+							h('label',
+								{
+									attrs: { "id": "close-fenedit", "for": "modal-fenedit" },
+									"class": { "modal-close": true },
+								}
+							),
+							h('h3',
+								{
+									attrs: { "id": "titleFenedit" },
+									"class": { "section": true },
+									domProps: { innerHTML: translations["Game state (FEN):"] },
+								}
+							),
+							h('input',
+								{
+									attrs: {
+										"id": "input-fen",
+										type: "text",
+										value: VariantRules.GenRandInitFen(),
+									},
+								}
+							),
+							h('button',
+								{
+									on: { click:
+										() => {
+											const fen = document.getElementById("input-fen").value;
+											document.getElementById("modal-fenedit").checked = false;
+											this.newGame("friend", fen);
+										}
+									},
+									domProps: { innerHTML: translations["Ok"] },
+								}
+							),
+							h('button',
+								{
+									on: { click:
+										() => {
+											document.getElementById("input-fen").value =
+												VariantRules.GenRandInitFen();
+										}
+									},
+									domProps: { innerHTML: translations["Random"] },
+								}
+							),
+						]
+					)
+				]
+			)
+		];
+		elementArray = elementArray.concat(modalFenEdit);
diff --git a/public/javascripts/settings.js b/public/javascripts/settings.js
index c9adbf16..85f89c1f 100644
--- a/public/javascripts/settings.js
+++ b/public/javascripts/settings.js
@@ -1,3 +1,23 @@
 // TODO:
 //à chaque onChange, envoyer matching event settings update
 //(par exemple si mise à jour du nom, juste envoyer cet update aux autres connectés ...etc)
+		setMyname: function(e) {
+			this.myname = e.target.value;
+			localStorage["username"] = this.myname;
+		},
+		showSettings: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			document.getElementById("modal-settings").checked = true;
+		},
+		toggleHints: function() {
+			this.hints = !this.hints;
+			localStorage["hints"] = (this.hints ? "1" : "0");
+		},
+		setBoardColor: function(e) {
+			this.bcolor = e.target.options[e.target.selectedIndex].value;
+			localStorage["bcolor"] = this.bcolor;
+		},
+		setSound: function(e) {
+			this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
+			localStorage["sound"] = this.sound;
+		},
diff --git a/public/javascripts/utils/storage.js b/public/javascripts/utils/storage.js
new file mode 100644
index 00000000..8bd43f0f
--- /dev/null
+++ b/public/javascripts/utils/storage.js
@@ -0,0 +1,55 @@
+		// TODO: general methods to access/retrieve from storage, to be generalized
+		// https://developer.mozilla.org/fr/docs/Web/API/API_IndexedDB
+		// https://dexie.org/
+		getStoragePrefix: function(mode) {
+			let prefix = "";
+			if (mode == "computer")
+				prefix = "comp-";
+			else if (mode == "friend")
+				prefix = "anlz-";
+			return prefix;
+		},
+		setStorage: function() {
+			if (this.mode=="human")
+			{
+				localStorage.setItem("myid", this.myid);
+				localStorage.setItem("oppid", this.oppid);
+				localStorage.setItem("gameId", this.gameId);
+			}
+			const prefix = this.getStoragePrefix(this.mode);
+			localStorage.setItem(prefix+"variant", variant);
+			localStorage.setItem(prefix+"mycolor", this.mycolor);
+			localStorage.setItem(prefix+"fenStart", this.fenStart);
+			localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
+			localStorage.setItem(prefix+"fen", this.vr.getFen());
+			localStorage.setItem(prefix+"score", "*");
+		},
+		updateStorage: function() {
+			const prefix = this.getStoragePrefix(this.mode);
+			localStorage.setItem(prefix+"moves", JSON.stringify(this.vr.moves));
+			localStorage.setItem(prefix+"fen", this.vr.getFen());
+			if (this.score != "*")
+				localStorage.setItem(prefix+"score", this.score);
+		},
+		// "computer mode" clearing is done through the menu
+		clearStorage: function() {
+			if (this.mode == "human")
+			{
+				delete localStorage["myid"];
+				delete localStorage["oppid"];
+				delete localStorage["gameId"];
+			}
+			const prefix = this.getStoragePrefix(this.mode);
+			delete localStorage[prefix+"variant"];
+			delete localStorage[prefix+"mycolor"];
+			delete localStorage[prefix+"fenStart"];
+			delete localStorage[prefix+"moves"];
+			delete localStorage[prefix+"fen"];
+			delete localStorage[prefix+"score"];
+		},
+		clearCurrentGame: function(e) {
+			this.getRidOfTooltip(e.currentTarget);
+			this.clearStorage();
+			location.reload(); //to see clearing effects
+		},
+
diff --git a/public/javascripts/variant.js b/public/javascripts/variant.js
index 736d8b3d..510ea290 100644
--- a/public/javascripts/variant.js
+++ b/public/javascripts/variant.js
@@ -21,6 +21,10 @@ new Vue({
 		},
 	},
 });
+		
+const continuation = (localStorage.getItem("variant") === variant);
+			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/views/variant.pug b/views/variant.pug
index a43ba601..2fbf21e9 100644
--- a/views/variant.pug
+++ b/views/variant.pug
@@ -26,10 +26,9 @@ block content
 						i.material-icons settings
 		.row
 			my-room(v-show="display=='room'")
-			my-games-list(v-show="display=='gameList'")
+			my-game-list(v-show="display=='gameList'")
 			my-rules(v-show="display=='rules'")
 			my-problems(v-show="display=='problems'")
-			// my-game: for room and games-list components
 			my-game(v-show="display=='game'" :gameId="gameid")
 
 block javascripts
@@ -43,8 +42,10 @@ block javascripts
 	script.
 		const V = VariantRules; //because this variable is often used
 		const variant = "#{variant}";
+	script(src="/javascripts/components/room.js")
+	script(src="/javascripts/components/gameList.js")
 	script(src="/javascripts/components/rules.js")
-	script(src="/javascripts/components/game.js")
 	script(src="/javascripts/components/problemSummary.js")
 	script(src="/javascripts/components/problems.js")
+	script(src="/javascripts/components/game.js")
 	script(src="/javascripts/variant.js")
-- 
2.44.0