From 15c1295af871a5f416b0e5b43127512c8095497a Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 13 Dec 2018 19:11:22 +0100
Subject: [PATCH] Finish Ultima rules + a few technical fixes

---
 public/javascripts/base_rules.js       |  29 +++---
 public/javascripts/components/game.js  |  12 ++-
 public/javascripts/components/rules.js |   4 +-
 public/javascripts/variants/Ultima.js  |  21 ++++
 public/javascripts/variants/Zen.js     |   6 +-
 routes/all.js                          |   5 +-
 sockets.js                             |   6 ++
 views/rules/Ultima.pug                 | 131 ++++++++++++++++++++++++-
 8 files changed, 190 insertions(+), 24 deletions(-)

diff --git a/public/javascripts/base_rules.js b/public/javascripts/base_rules.js
index a10a43ca..63b535da 100644
--- a/public/javascripts/base_rules.js
+++ b/public/javascripts/base_rules.js
@@ -54,7 +54,6 @@ class ChessRules
 	constructor(fen, moves)
 	{
 		this.moves = moves;
-		this.hashStates = {}; //for repetitions detection
 		// Use fen string to initialize variables, flags and board
 		this.board = VariantRules.GetBoard(fen);
 		this.setFlags(fen);
@@ -728,16 +727,10 @@ class ChessRules
 			this.kingPos[c] = [move.start.x, move.start.y];
 	}
 
-	// Store a hash of the position + flags + turn after a move is played
-	// (for repetitions detection)
-	addHashState()
+	// Hash of position+flags+turn after a move is played (to detect repetitions)
+	getHashState()
 	{
-		const strToHash = this.getFen() + " " + this.turn;
-		const hash = hex_md5(strToHash);
-		if (!this.hashStates[hash])
-			this.hashStates[hash] = 1;
-		else
-			this.hashStates[hash]++;
+		return hex_md5(this.getFen() + " " + this.turn);
 	}
 
 	play(move, ingame)
@@ -756,7 +749,7 @@ class ChessRules
 		VariantRules.PlayOnBoard(this.board, move);
 
 		if (!!ingame)
-			this.addHashState();
+			move.hash = this.getHashState();
 	}
 
 	undo(move)
@@ -779,6 +772,20 @@ class ChessRules
 	// Check for 3 repetitions (position + flags + turn)
 	checkRepetition()
 	{
+		if (!this.hashStates)
+			this.hashStates = {};
+		const startIndex =
+			Object.values(this.hashStates).reduce((a,b) => { return a+b; }, 0)
+		// Update this.hashStates with last move (or all moves if continuation)
+		// NOTE: redundant storage, but faster and moderate size
+		for (let i=startIndex; i<this.moves.length; i++)
+		{
+			const move = this.moves[i];
+			if (!this.hashStates[move.hash])
+				this.hashStates[move.hash] = 1;
+			else
+				this.hashStates[move.hash]++;
+		}
 		return Object.values(this.hashStates).some(elt => { return (elt >= 3); });
 	}
 
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index 208e0482..deccd5da 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -25,7 +25,7 @@ Vue.component('my-game', {
 	},
 	render(h) {
 		const [sizeX,sizeY] = VariantRules.size;
-		const smallScreen = (screen.width <= 420);
+		const smallScreen = (window.innerWidth <= 420);
 		// Precompute hints squares to facilitate rendering
 		let hintSquares = doubleArray(sizeX, sizeY, false);
 		this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
@@ -779,7 +779,7 @@ Vue.component('my-game', {
 	created: function() {
 		const url = socketUrl;
 		const continuation = (localStorage.getItem("variant") === variant);
-		this.myid = continuation ? localStorage.getItem("myid") : getRandString();
+		this.myid = (continuation ? localStorage.getItem("myid") : getRandString());
 		if (!continuation)
 		{
 			// HACK: play a small silent sound to allow "new game" sound later
@@ -808,6 +808,12 @@ Vue.component('my-game', {
 			const data = JSON.parse(msg.data);
 			switch (data.code)
 			{
+				case "duplicate":
+					// We opened another tab on the same game
+					this.mode = "idle";
+					this.vr = null;
+					alert("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);
@@ -1051,7 +1057,7 @@ Vue.component('my-game', {
 					document.getElementById("modal-newgame").checked = false;
 				}
 				this.oppid = oppId;
-				this.oppConnected = true;
+				this.oppConnected = !continuation;
 				this.mycolor = color;
 				this.seek = false;
 				if (!!moves && moves.length > 0) //imply continuation
diff --git a/public/javascripts/components/rules.js b/public/javascripts/components/rules.js
index fd05d44d..25f8a3bd 100644
--- a/public/javascripts/components/rules.js
+++ b/public/javascripts/components/rules.js
@@ -54,13 +54,13 @@ Vue.component('my-rules', {
 				{
 					boardDiv += "<div class='board board" + sizeY + " " +
 						((i+j)%2==0 ? "light-square-diag" : "dark-square-diag") + "'>";
-					if (markArray.length>0 && markArray[i][j])
-						boardDiv += "<img src='/images/mark.svg' class='markSquare'/>";
 					if (board[i][j] != VariantRules.EMPTY)
 					{
 						boardDiv += "<img src='/images/pieces/" +
 							VariantRules.getPpath(board[i][j]) + ".svg' class='piece'/>";
 					}
+					if (markArray.length>0 && markArray[i][j])
+						boardDiv += "<img src='/images/mark.svg' class='mark-square'/>";
 					boardDiv += "</div>";
 				}
 				boardDiv += "</div>";
diff --git a/public/javascripts/variants/Ultima.js b/public/javascripts/variants/Ultima.js
index d5b9f884..6f44e5c0 100644
--- a/public/javascripts/variants/Ultima.js
+++ b/public/javascripts/variants/Ultima.js
@@ -623,4 +623,25 @@ class UltimaRules extends ChessRules
 	{
 		return "0000"; //TODO: or "-" ?
 	}
+
+	getNotation(move)
+	{
+		const initialSquare =
+			String.fromCharCode(97 + move.start.y) + (VariantRules.size[0]-move.start.x);
+		const finalSquare =
+			String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x);
+		let notation = undefined;
+		if (move.appear[0].p == VariantRules.PAWN)
+		{
+			// Pawn: generally ambiguous short notation, so we use full description
+			notation = "P" + initialSquare + finalSquare;
+		}
+		else if (move.appear[0].p == VariantRules.KING)
+			notation = "K" + (move.vanish.length>1 ? "x" : "") + finalSquare;
+		else
+			notation = move.appear[0].p.toUpperCase() + finalSquare;
+		if (move.vanish.length > 1 && move.appear[0].p != VariantRules.KING)
+			notation += "X"; //capture mark (not describing what is captured...)
+		return notation;
+	}
 }
diff --git a/public/javascripts/variants/Zen.js b/public/javascripts/variants/Zen.js
index c1f48147..f57ab3c5 100644
--- a/public/javascripts/variants/Zen.js
+++ b/public/javascripts/variants/Zen.js
@@ -194,15 +194,15 @@ class ZenRules extends ChessRules
 		}
 
 		// Translate initial square (because pieces may fly unusually in this variant!)
-		let initialSquare =
+		const initialSquare =
 			String.fromCharCode(97 + move.start.y) + (VariantRules.size[0]-move.start.x);
 
 		// Translate final square
-		let finalSquare =
+		const finalSquare =
 			String.fromCharCode(97 + move.end.y) + (VariantRules.size[0]-move.end.x);
 
 		let notation = "";
-		let piece = this.getPiece(move.start.x, move.start.y);
+		const piece = this.getPiece(move.start.x, move.start.y);
 		if (piece == VariantRules.PAWN)
 		{
 			// pawn move (TODO: enPassant indication)
diff --git a/routes/all.js b/routes/all.js
index ca09be53..3e6dd001 100644
--- a/routes/all.js
+++ b/routes/all.js
@@ -1,5 +1,6 @@
 var express = require('express');
 var router = express.Router();
+var createError = require('http-errors');
 
 const Variants = require("../variants");
 
@@ -12,8 +13,10 @@ router.get('/', function(req, res, next) {
 });
 
 // Variant
-router.get("/:vname([a-zA-Z0-9]+)", (req,res) => {
+router.get("/:vname([a-zA-Z0-9]+)", (req,res,next) => {
 	const vname = req.params["vname"];
+	if (!Variants.some(v => { return (v.name == vname); }))
+		return next(createError(404));
 	res.render('variant', {
 		title: vname + ' Variant',
 		variant: vname,
diff --git a/sockets.js b/sockets.js
index ee4c1cfd..675a80eb 100644
--- a/sockets.js
+++ b/sockets.js
@@ -25,6 +25,12 @@ module.exports = function(wss) {
 		const params = new URL("http://localhost" + req.url).searchParams;
 		const sid = params.get("sid");
 		const page = params.get("page");
+		// Ignore duplicate connections:
+		if (!!clients[page][sid])
+		{
+			socket.send(JSON.stringify({code:"duplicate"}));
+			return;
+		}
 		clients[page][sid] = socket;
 		if (page == "index")
 		{
diff --git a/views/rules/Ultima.pug b/views/rules/Ultima.pug
index 423ba5d6..c79acc35 100644
--- a/views/rules/Ultima.pug
+++ b/views/rules/Ultima.pug
@@ -13,18 +13,141 @@ ul
 	li Captures: very special.
 	li End of game: standard; see below.
 
+h4 Pieces names
+
+p Pieces names refer to the way they capture, which is described later.
+ul
+	li Pawn : pawn or pincer
+	li Rook : coordinator
+	li Knight : long leaper
+	li Bishop : chameleon
+	li Queen : withdrawer
+	li King : king (same behavior as in standard chess)
+p.
+	Besides, a new piece is introduced: the immobilizer, represented by the letter 'm'
+	in FEN diagrams and PGN games. It is represented by an upside-down rook:
+
+figure.diagram-container
+	.diagram
+		| fen:8/8/4m3/8/8/8/3M4/8:
+	figcaption Immobilizers on d2 and e6.
+
 h3 Non-capturing moves
 
-// TODO: short paragraph, only the king moves like an orthodox king
-// Consider these rules modifications: http://www.inference.org.uk/mackay/ultima/ultima.html
+p
+	| Pawns move as orthodox rooks, and the king moves as usual,
+	| one square in any direction.
+	| All other pieces move like an orthodox queen.
+
+p When a piece is adjacent to an enemy immobilizer, it cannot move unless
+ul
+	li it is an immobilizer or a chameleon; or
+	li.
+		the enemy immobilizer is adjacent to a friendly immobilizer or chameleon
+		(cancelling the powers of the opponent's immobilizer)
+p
+	| Note : this corresponds to the "pure rules" described on 
+	a(href="http://www.inference.org.uk/mackay/ultima/ultima.html") this page
+	| , which slightly differ from the initial rules.
+	| The aim is to get rid of the weird suicide rule, weakening the immobilizers lock
+	| (in particular, in the original rules two adjacent immobilizer are stuck forever
+	| until one is captured).
 
 h3 Capturing moves
 
-// TODO...
+p
+	| Easy case first: the king captures as usual, by moving onto an adjacent square
+	| occupied by an enemy piece. But this is the only piece following orthodox rules,
+	| and also the only one which captures by moving onto an occupied square.
+	| All other pieces capture passively: they land on a free square and captured
+	| units are determined by some characteristics of the movement.
+
+p Note: the immobilizer does not capture.
+
+h4 Pawns/Pincers
+
+p.
+	If at the end of its movement a pawn is horizontally or vertically adjacent to an
+	enemy piece, which itself is next to a friendly piece (in the same direction),
+	the "pinced" unit is removed from the board.
+
+figure.diagram-container
+	.diagram
+		| fen:7k/5ppp/2N5/2n5/3rB3/8/PPP5/K7:
+	figcaption 1.Pc2c4 captures both coordinator and long leaper.
+
+h4 Coordinators (rooks)
+
+p.
+	Imagine that rook and king are two corners of a rectangle (this works if these
+	two pieces are unaligned).
+	If at the end of a rook move an enemy piece stands in any of the two remaining
+	corners, it is captured.
+
+figure.diagram-container
+	.diagram
+		| fen:8/2b4K/2q5/3p1N1p/8/8/2R5/k7:
+	figcaption 1.Rc5 captures on c7 and h5.
+
+h4 Long leapers (knights)
+
+p.
+	A knight captures exactly as a queen in draughts game: by jumping over its enemies,
+	as many times as it can/want but always in the same direction.
+	In this respect it is less powerful than a draughts' queen:
+	on the following diagram c8 or f6 cannot be captured.
+
+figure.diagram-container
+	.diagram
+		| fen:2n2b1k/3r4/8/3p4/8/3b4/3N4/K7 w d4,d6,d8:
+	figcaption All marked squares are playable from d2.
+
+h4 Withdrawer (queen)
+
+p.
+	The queen captures by moving away from an adjacent enemy piece, in the opposite
+	direction (only the long leaper can jump).
+
+figure.diagram-container
+	.diagram
+		| fen:7k/8/8/3Qr3/8/8/8/K7 w a5,b5,c5:
+	figcaption 1.Qa5, 1.Qb5 or 1.Qc5 captures the black rook.
+
+h4 Chameleon (bishop)
+
+p The chameleon captures pieces in the way they would capture. So, it
+ul
+	li pinces pawns,
+	li withdraws from withdrawers,
+	li leaps over long leapers,
+	li coordinates coordinators.
+p ...and these captures can be combined.
+
+figure.diagram-container
+	.diagram
+		| fen:7k/8/8/m3pP2/2n5/8/B7/K7 w a5,c4,e5:
+	figcaption 1.Bd5 captures all marked pieces.
+
+p.
+	Besides, chameleon immobilizes immobilizers (but cannot capture them since they
+	do not capture).
+
+p.
+	A chameleon captures the king in the same way the king captures, which means that
+	a chameleon adjacent to a king gives check.
 
 h3 End of the game
 
-// TODO: show the situation from Wikipedia page
+p.
+	Checkmate or stalemate as in standard chess. Note however that checks are more
+	difficult to see, because of the exotic capturing rules. For example, on the
+	following diagram the white king cannot move to the marked squares because then
+	the black pawn could capture by moving next to it.
+
+figure.diagram-container
+	.diagram
+		| fen:7k/8/8/p4r/4K3/8/8/8 w e5:
+	figcaption 1.Ke5 is impossible
 
 h3 Credits
 
-- 
2.44.0