From 7931e479adf93c87771ded1892a0873af72ae46d Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 15 Dec 2018 18:22:14 +0100
Subject: [PATCH] Almost finished problems logic. TODO: showProblem() part

---
 TODO                                          |  2 +
 public/javascripts/base_rules.js              | 99 +++++++++++++++----
 public/javascripts/components/game.js         |  5 +-
 .../javascripts/components/problemSummary.js  | 37 +++++--
 public/javascripts/components/problems.js     | 56 +++++++----
 public/javascripts/components/rules.js        |  6 +-
 public/javascripts/variants/Alice.js          |  4 +
 public/javascripts/variants/Antiking.js       |  6 +-
 public/javascripts/variants/Checkered.js      | 25 ++++-
 public/javascripts/variants/Grand.js          |  4 +
 public/javascripts/variants/Loser.js          |  5 +
 public/javascripts/variants/Ultima.js         |  9 ++
 public/javascripts/variants/Wildebeest.js     |  4 +
 routes/all.js                                 | 16 ++-
 14 files changed, 210 insertions(+), 68 deletions(-)
 create mode 100644 TODO

diff --git a/TODO b/TODO
new file mode 100644
index 00000000..e6641b48
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+Finish showProblem() in components/problemSummary.js (send event with infos, and pass message to game component)
+Add new mode in game component: "problem", in which we show description + hidden solution (reveal on click)
diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js
index 51c9dda8..5763af7e 100644
--- a/public/javascripts/base_rules.js
+++ b/public/javascripts/base_rules.js
@@ -54,12 +54,15 @@ class ChessRules
 	constructor(fen, moves)
 	{
 		this.moves = moves;
-		// Use fen string to initialize variables, flags and board
-		this.board = V.GetBoard(fen);
-		this.setFlags(fen);
+		// Use fen string to initialize variables, flags, turn and board
+		const fenParts = fen.split(" ");
+		this.board = V.GetBoard(fenParts[0]);
+		this.setFlags(fenParts[1]); //NOTE: fenParts[1] might be undefined
+		this.setTurn(fenParts[2]); //Same note
 		this.initVariables(fen);
 	}
 
+	// Some additional variables from FEN (variant dependant)
 	initVariables(fen)
 	{
 		this.INIT_COL_KING = {'w':-1, 'b':-1};
@@ -95,7 +98,7 @@ class ChessRules
 							this.INIT_COL_ROOK['w'][1] = k;
 						break;
 					default:
-						let num = parseInt(position[i].charAt(j));
+						const num = parseInt(position[i].charAt(j));
 						if (!isNaN(num))
 							k += (num-1);
 				}
@@ -106,18 +109,68 @@ class ChessRules
 		this.epSquares = [ epSq ];
 	}
 
+	// Check if FEN describe a position
+	static IsGoodFen(fen)
+	{
+		const fenParts = fen.split(" ");
+		if (fenParts.length== 0 || fenParts.length > 3)
+			return false;
+		// 1) Check position
+		const position = fenParts[0];
+		const rows = position.split("/");
+		if (rows.length != V.size.x)
+			return false;
+		for (let row of rows)
+		{
+			let sumElts = 0;
+			for (let i=0; i<row.length; i++)
+			{
+				if (V.PIECES.includes(row[i].toLowerCase()))
+					sumElts++;
+				else
+				{
+					const num = parseInt(row[i]);
+					if (isNaN(num))
+						return false;
+					sumElts += num;
+				}
+			}
+			if (sumElts != V.size.y)
+				return false;
+		}
+		// 2) Check flags (if present)
+		if (fenParts.length >= 2)
+		{
+			if (!V.IsGoodFlags(fenParts[1]))
+				return false;
+		}
+		// 3) Check turn (if present)
+		if (fenParts.length == 3)
+		{
+			if (!["w","b"].includes(fenParts[2]))
+				return false;
+		}
+		return true;
+	}
+
+	// For FEN checking
+	static IsGoodFlags(flags)
+	{
+		return !!flags.match(/^[01]{4,4}$/);
+	}
+
 	// Turn diagram fen into double array ["wb","wp","bk",...]
 	static GetBoard(fen)
 	{
-		let rows = fen.split(" ")[0].split("/");
+		const rows = fen.split(" ")[0].split("/");
 		let board = doubleArray(V.size.x, V.size.y, "");
 		for (let i=0; i<rows.length; i++)
 		{
 			let j = 0;
 			for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++)
 			{
-				let character = rows[i][indexInRow];
-				let num = parseInt(character);
+				const character = rows[i][indexInRow];
+				const num = parseInt(character);
 				if (!isNaN(num))
 					j += num; //just shift j
 				else //something at position i,j
@@ -128,13 +181,20 @@ class ChessRules
 	}
 
 	// Extract (relevant) flags from fen
-	setFlags(fen)
+	setFlags(fenflags)
 	{
 		// white a-castle, h-castle, black a-castle, h-castle
-		this.castleFlags = {'w': new Array(2), 'b': new Array(2)};
-		let flags = fen.split(" ")[1]; //flags right after position
+		this.castleFlags = {'w': [true,true], 'b': [true,true]};
+		if (!fenflags)
+			return;
 		for (let i=0; i<4; i++)
-			this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (flags.charAt(i) == '1');
+			this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1');
+	}
+
+	// Initialize turn (white or black)
+	setTurn(turnflag)
+	{
+		this.turn = turnflag || "w";
 	}
 
 	///////////////////
@@ -154,10 +214,6 @@ class ChessRules
 		return (L>0 ? this.moves[L-1] : null);
 	}
 
-	get turn() {
-		return (this.moves.length%2==0 ? 'w' : 'b');
-	}
-
 	// Pieces codes
 	static get PAWN() { return 'p'; }
 	static get ROOK() { return 'r'; }
@@ -166,6 +222,11 @@ class ChessRules
 	static get QUEEN() { return 'q'; }
 	static get KING() { return 'k'; }
 
+	// For FEN checking:
+	static get PIECES() {
+		return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING];
+	}
+
 	// Empty square
 	static get EMPTY() { return ''; }
 
@@ -483,9 +544,7 @@ class ChessRules
 
 	canIplay(side, [x,y])
 	{
-		return ((side=='w' && this.moves.length%2==0)
-				|| (side=='b' && this.moves.length%2==1))
-			&& this.getColor(x,y) == side;
+		return (this.turn == side && this.getColor(x,y) == side);
 	}
 
 	getPossibleMovesFrom(sq)
@@ -717,7 +776,7 @@ class ChessRules
 	// Hash of position+flags+turn after a move is played (to detect repetitions)
 	getHashState()
 	{
-		return hex_md5(this.getFen() + " " + this.turn);
+		return hex_md5(this.getFen());
 	}
 
 	play(move, ingame)
@@ -1054,7 +1113,7 @@ class ChessRules
 	// Return current fen according to pieces+colors state
 	getFen()
 	{
-		return this.getBaseFen() + " " + this.getFlagsFen();
+		return this.getBaseFen() + " " + this.getFlagsFen() + " " + this.turn;
 	}
 
 	// Position part of the FEN string
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index 278e0782..49db6188 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -1024,10 +1024,7 @@ Vue.component('my-game', {
 			{
 				const storageVariant = localStorage.getItem("variant");
 				if (!!storageVariant && storageVariant !== variant)
-				{
-					alert("Finish your " + storageVariant + " game first!");
-					return;
-				}
+					return alert("Finish your " + storageVariant + " game first!");
 				// Send game request and wait..
 				localStorage["newgame"] = variant;
 				this.seek = true;
diff --git a/public/javascripts/components/problemSummary.js b/public/javascripts/components/problemSummary.js
index 6003f085..48a42533 100644
--- a/public/javascripts/components/problemSummary.js
+++ b/public/javascripts/components/problemSummary.js
@@ -2,16 +2,33 @@
 Vue.component('my-problem-summary', {
 	props: ['prob'],
 	template: `
-		<div class="problem col-sm-12">
-			<div class="diagram">
-				{{ getDiagram(prob.fen) }}
-			</div>
-			<div class="problem-instructions">
-				{{ prob.instructions.substr(0,32) }}
-			</div>
-			<div class="problem-time">
-				{{ prob.added }}
-			</div>
+		<div class="problem col-sm-12" @click="showProblem()">
+			<div class="diagram" v-html="getDiagram(prob.fen)"></div>
+			<div class="problem-instructions" v-html="prob.instructions.substr(0,32)"></div>
+			<div class="problem-time">{{ timestamp2datetime(prob.added) }}</div>
 		</div>
 	`,
+	methods: {
+		getDiagram: function(fen) {
+			const fenParts = fen.split(" ");
+			return getDiagram({
+				position: fenParts[0],
+				// No need for flags here
+				turn: fenParts[2],
+			});
+		},
+		timestamp2datetime(ts) {
+			// TODO
+			return ts;
+		},
+		showProblem: function() {
+			alert("show problem");
+			//..........
+			//TODO: send event with object prob.fen, prob.instructions, prob.solution
+			//Event should propagate to game, which set mode=="problem" + other variables
+			//click on a problem ==> land on variant page with mode==friend, FEN prefilled... ok
+			// click on problem ==> masque problems, affiche game tab, launch new game Friend with
+			//   FEN + turn + flags + rappel instructions / solution on click sous l'échiquier
+		},
+	},
 })
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index 594d6e64..dbd8340c 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -6,14 +6,11 @@ Vue.component('my-problems', {
 	},
 	template: `
 		<div>
-			<button>Previous</button>
-			<button>Next</button>
+			<button @click="fetchProblems('backward')">Previous</button>
+			<button @click="fetchProblems('forward')">Next</button>
 			<button @click="showNewproblemModal">New</button>
 			<my-problem-summary
-				v-for="(p,idx) in sortedProblems",
-				v-bind:prob="p",
-				v-bind:key="idx",
-				@click="showProblem(p)"
+				v-for="(p,idx) in sortedProblems" v-bind:prob="p" v-bind:key="idx">
 			</my-problem-summary>
 			<input type="checkbox" id="modal-newproblem" class="modal">
 			<div role="dialog" aria-labelledby="newProblemTxt">
@@ -23,7 +20,8 @@ Vue.component('my-problems', {
 					<form @submit.prevent="postNewProblem">
 						<fieldset>
 							<label for="newpbFen">Fen</label>
-							<input type="text" id="newpbFen" placeholder="Position [+ flags [+ turn]]"/>
+							<input type="text" id="newpbFen"
+								placeholder="Position [+ flags [+ turn]]"/>
 						</fieldset>
 						<fieldset>
 							<p class="emphasis">
@@ -47,8 +45,9 @@ Vue.component('my-problems', {
 	`,
 	computed: {
 		sortedProblems: function() {
+			console.log("call");
 			// Newest problem first
-			return problems.sort((p1,p2) => { return p2.added - p1.added; });
+			return this.problems.sort((p1,p2) => { return p2.added - p1.added; });
 		},
 		mailErrProblem: function() {
 			return "mailto:contact@vchess.club?subject=[" + variant + " problems] error";
@@ -56,26 +55,43 @@ Vue.component('my-problems', {
 	},
 	methods: {
 		fetchProblems: function(direction) {
-			// TODO: ajax call return list of max 10 problems
-			// Do not do anything if no older problems (and store this result in cache!)
-			// TODO: ajax call return list of max 10 problems
-			// Do not do anything if no newer problems
-		},
-		showProblem: function(prob) {
-			//TODO: send event with object prob.fen, prob.instructions, prob.solution
-			//Event should propagate to game, which set mode=="problem" + other variables
-			//click on a problem ==> land on variant page with mode==friend, FEN prefilled... ok
-			// click on problem ==> masque problems, affiche game tab, launch new game Friend with
-			//   FEN + turn + flags + rappel instructions / solution on click sous l'échiquier
+			return; //TODO: re-activate after server side is implemented (see routes/all.js)
+			if (this.problems.length == 0)
+				return; //what could we do?!
+			// Search for newest date (or oldest)
+			let last_dt = this.problems[0].added;
+			for (let i=0; i<this.problems.length; i++)
+			{
+				if ((direction == "forward" && this.problems[i].added > last_dt) ||
+					(direction == "backward" && this.problems[i].added < last_dt))
+				{
+					last_dt = this.problems[i].added;
+				}
+			}
+			ajax("/problems/" + variant, "GET", {
+				direction: direction,
+				last_dt: last_dt,
+			}, response => {
+				if (response.problems.length > 0)
+					this.problems = response.problems;
+			});
 		},
 		showNewproblemModal: function() {
 			document.getElementById("modal-newproblem").checked = true;
 		},
 		postNewProblem: function() {
 			const fen = document.getElementById("newpbFen").value;
+			if (!V.IsGoodFen(fen))
+				return alert("Bad FEN string");
 			const instructions = document.getElementById("newpbInstructions").value;
 			const solution = document.getElementById("newpbSolution").value;
-			
+			ajax("/problems/" + variant, "POST", {
+				fen: fen,
+				instructions: instructions,
+				solution: solution,
+			}, response => {
+				document.getElementById("modal-newproblem").checked = false;
+			});
 		},
 	},
 })
diff --git a/public/javascripts/components/rules.js b/public/javascripts/components/rules.js
index 660f0be9..829bf3b1 100644
--- a/public/javascripts/components/rules.js
+++ b/public/javascripts/components/rules.js
@@ -8,11 +8,11 @@ Vue.component('my-rules', {
 		// AJAX request to get rules content (plain text, HTML)
 		ajax("/rules/" + variant, "GET", response => {
 			let replaceByDiag = (match, p1, p2) => {
-				const args = self.parseFen(p2);
+				const args = this.parseFen(p2);
 				return getDiagram(args);
 			};
-			self.content = response.replace(/(fen:)([^:]*):/g, replaceByDiag);
-		}
+			this.content = response.replace(/(fen:)([^:]*):/g, replaceByDiag);
+		});
 	},
 	methods: {
 		parseFen(fen) {
diff --git a/public/javascripts/variants/Alice.js b/public/javascripts/variants/Alice.js
index 5e10a059..ece59cdc 100644
--- a/public/javascripts/variants/Alice.js
+++ b/public/javascripts/variants/Alice.js
@@ -29,6 +29,10 @@ class AliceRules extends ChessRules
 		return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
 	}
 
+	static get PIECES() {
+		return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES));
+	}
+
 	initVariables(fen)
 	{
 		super.initVariables(fen);
diff --git a/public/javascripts/variants/Antiking.js b/public/javascripts/variants/Antiking.js
index e7411684..2821f3f5 100644
--- a/public/javascripts/variants/Antiking.js
+++ b/public/javascripts/variants/Antiking.js
@@ -6,7 +6,11 @@ class AntikingRules extends ChessRules
 	}
 
 	static get ANTIKING() { return 'a'; }
-	
+
+	static get PIECES() {
+		return ChessRules.PIECES.concat([V.ANTIKING]);
+	}
+
 	initVariables(fen)
 	{
 		super.initVariables(fen);
diff --git a/public/javascripts/variants/Checkered.js b/public/javascripts/variants/Checkered.js
index 2d62d4b8..585d20ff 100644
--- a/public/javascripts/variants/Checkered.js
+++ b/public/javascripts/variants/Checkered.js
@@ -19,18 +19,34 @@ class CheckeredRules extends ChessRules
 	}
 	static fen2board(f)
 	{
+		// Tolerate upper-case versions of checkered pieces (why not?)
 		const checkered_pieces = {
 			's': 'p',
+			'S': 'p',
 			't': 'q',
+			'T': 'q',
 			'u': 'r',
+			'U': 'r',
 			'c': 'b',
+			'C': 'b',
 			'o': 'n',
+			'O': 'n',
 		};
 		if (Object.keys(checkered_pieces).includes(f))
 			return 'c'+checkered_pieces[f];
 		return ChessRules.fen2board(f);
 	}
 
+	static get PIECES() {
+		return ChessRules.PIECES.concat(['s','t','u','c','o']);
+	}
+
+	static IsGoodFlags(flags)
+	{
+		// 4 for castle + 16 for pawns
+		return !!flags.match(/^[01]{20,20}$/);
+	}
+
 	setFlags(fen)
 	{
 		super.setFlags(fen); //castleFlags
@@ -108,9 +124,7 @@ class CheckeredRules extends ChessRules
 
 	canIplay(side, [x,y])
 	{
-		return ((side=='w' && this.moves.length%2==0)
-				|| (side=='b' && this.moves.length%2==1))
-			&& [side,'c'].includes(this.getColor(x,y));
+		return (side == this.turn && [side,'c'].includes(this.getColor(x,y)));
 	}
 
 	// Does m2 un-do m1 ? (to disallow undoing checkered moves)
@@ -194,11 +208,12 @@ class CheckeredRules extends ChessRules
 	checkGameEnd()
 	{
 		const color = this.turn;
-		this.moves.length++; //artifically change turn, for checkered pawns (TODO)
+		// Artifically change turn, for checkered pawns
+		this.turn = this.getOppCol(this.turn);
 		const res = this.isAttacked(this.kingPos[color], [this.getOppCol(color),'c'])
 			? (color == "w" ? "0-1" : "1-0")
 			: "1/2";
-		this.moves.length--;
+		this.turn = this.getOppCol(this.turn);
 		return res;
 	}
 
diff --git a/public/javascripts/variants/Grand.js b/public/javascripts/variants/Grand.js
index f5ae0653..cafdadab 100644
--- a/public/javascripts/variants/Grand.js
+++ b/public/javascripts/variants/Grand.js
@@ -18,6 +18,10 @@ class GrandRules extends ChessRules
 	static get MARSHALL() { return 'm'; } //rook+knight
 	static get CARDINAL() { return 'c'; } //bishop+knight
 
+	static get PIECES() {
+		return ChessRules.PIECES.concat([V.MARSHALL,V.CARDINAL]);
+	}
+
 	// En-passant after 2-sq or 3-sq jumps
 	getEpSquare(move)
 	{
diff --git a/public/javascripts/variants/Loser.js b/public/javascripts/variants/Loser.js
index 3def40a9..98bd9443 100644
--- a/public/javascripts/variants/Loser.js
+++ b/public/javascripts/variants/Loser.js
@@ -6,6 +6,11 @@ class LoserRules extends ChessRules
 		this.epSquares = [ epSq ];
 	}
 
+	static IsGoodFlags(flags)
+	{
+		return true; //anything is good: no flags
+	}
+
 	setFlags(fen)
 	{
 		// No castling, hence no flags; but flags defined for compatibility
diff --git a/public/javascripts/variants/Ultima.js b/public/javascripts/variants/Ultima.js
index 04ef29b0..d6fecb70 100644
--- a/public/javascripts/variants/Ultima.js
+++ b/public/javascripts/variants/Ultima.js
@@ -7,6 +7,15 @@ class UltimaRules extends ChessRules
 		return b; //usual piece
 	}
 
+	static get PIECES() {
+		return ChessRules.PIECES.concat([V.IMMOBILIZER]);
+	}
+
+	static IsGoodFlags(flags)
+	{
+		return true; //anything is good: no flags
+	}
+
 	initVariables(fen)
 	{
 		this.kingPos = {'w':[-1,-1], 'b':[-1,-1]};
diff --git a/public/javascripts/variants/Wildebeest.js b/public/javascripts/variants/Wildebeest.js
index bb478cc2..dcf2d582 100644
--- a/public/javascripts/variants/Wildebeest.js
+++ b/public/javascripts/variants/Wildebeest.js
@@ -10,6 +10,10 @@ class WildebeestRules extends ChessRules
 	static get CAMEL() { return 'c'; }
 	static get WILDEBEEST() { return 'w'; }
 
+	static get PIECES() {
+		return ChessRules.PIECES.concat([V.CAMEL,V.WILDEBEEST]);
+	}
+
 	static get steps() {
 		return Object.assign(
 			ChessRules.steps, //add camel moves:
diff --git a/routes/all.js b/routes/all.js
index f3e184e6..b1e0fda3 100644
--- a/routes/all.js
+++ b/routes/all.js
@@ -28,6 +28,7 @@ router.get("/:vname([a-zA-Z0-9]+)", (req,res,next) => {
 				return next(err);
 			if (!variant || variant.length==0)
 				return next(createError(404));
+			// TODO (later...) get only n=100(?) most recent problems
 			db.all("SELECT * FROM Problems WHERE variant='" + vname + "'",
 				(err2,problems) => {
 					if (!!err2)
@@ -55,6 +56,9 @@ router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
 	if (!req.xhr)
 		return res.json({errmsg: "Unauthorized access"});
 	// TODO: next or previous: in params + timedate (of current oldest or newest)
+	db.serialize(function() {
+		//TODO
+	});
 });
 
 // Upload a problem (AJAX)
@@ -62,10 +66,13 @@ router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
 	if (!req.xhr)
 		return res.json({errmsg: "Unauthorized access"});
 	const vname = req.params["variant"];
-	
-	// TODO: get parameters and sanitize them
-	sanitizeHtml(req.body["fen"]); // [/a-z0-9 ]*
-	sanitizeHtml(req.body["instructions"]);
+	const timestamp = Date.now();
+	// Sanitize them
+	const fen = req.body["fen"];
+	if (!fen.match(/^[a-zA-Z0-9 /]*$/))
+		return res.json({errmsg: "Bad characters in FEN string"});
+	const instructions = sanitizeHtml(req.body["instructions"]);
+	const solution = sanitizeHtml(req.body["solution"]);
 	db.serialize(function() {
 		let stmt = db.prepare("INSERT INTO Problems VALUES (?,?,?,?,?)");
 		stmt.run(timestamp, vname, fen, instructions, solution);
@@ -74,5 +81,4 @@ router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
   res.json({});
 });
 
-
 module.exports = router;
-- 
2.44.0