From 15c1295af871a5f416b0e5b43127512c8095497a Mon Sep 17 00:00:00 2001 From: Benjamin Auder 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 { 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 += "
"; - if (markArray.length>0 && markArray[i][j]) - boardDiv += ""; if (board[i][j] != VariantRules.EMPTY) { boardDiv += ""; } + if (markArray.length>0 && markArray[i][j]) + boardDiv += ""; boardDiv += "
"; } boardDiv += ""; 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