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);
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)
VariantRules.PlayOnBoard(this.board, move);
if (!!ingame)
- this.addHashState();
+ move.hash = this.getHashState();
}
undo(move)
// 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); });
}
},
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; });
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
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);
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
{
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>";
{
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;
+ }
}
}
// 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)
var express = require('express');
var router = express.Router();
+var createError = require('http-errors');
const Variants = require("../variants");
});
// 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,
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")
{
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