Finish Ultima rules + a few technical fixes
authorBenjamin Auder <benjamin.auder@somewhere>
Thu, 13 Dec 2018 18:11:22 +0000 (19:11 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Thu, 13 Dec 2018 18:11:22 +0000 (19:11 +0100)
public/javascripts/base_rules.js
public/javascripts/components/game.js
public/javascripts/components/rules.js
public/javascripts/variants/Ultima.js
public/javascripts/variants/Zen.js
routes/all.js
sockets.js
views/rules/Ultima.pug

index a10a43c..63b535d 100644 (file)
@@ -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); });
        }
 
index 208e048..deccd5d 100644 (file)
@@ -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
index fd05d44..25f8a3b 100644 (file)
@@ -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>";
index d5b9f88..6f44e5c 100644 (file)
@@ -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;
+       }
 }
index c1f4814..f57ab3c 100644 (file)
@@ -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)
index ca09be5..3e6dd00 100644 (file)
@@ -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,
index ee4c1cf..675a80e 100644 (file)
@@ -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")
                {
index 423ba5d..c79acc3 100644 (file)
@@ -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