From d66135396f3a6e140947545630004ce11f8eee7b Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 4 Jan 2024 10:37:28 +0100 Subject: [PATCH 01/16] Fix Dobutsu, extend Align4 --- variants/Align4/class.js | 19 +++++++++++++++++++ variants/Dobutsu/class.js | 15 ++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/variants/Align4/class.js b/variants/Align4/class.js index 09a79ed..f5b8f83 100644 --- a/variants/Align4/class.js +++ b/variants/Align4/class.js @@ -13,6 +13,14 @@ export default class Align4Rules extends ChessRules { {label: "Random", value: 1} ] }], + input: [ + { + label: "Pawn first", + variable: "pawnfirst", + type: "checkbox", + defaut: false + } + ], styles: ["atomic", "capture", "cylinder"] }; } @@ -39,6 +47,17 @@ export default class Align4Rules extends ChessRules { // Just do not update any reserve (infinite supply) updateReserve() {} + canDrop([c, p], [i, j]) { + return ( + this.board[i][j] == "" && + ( + p != "p" || this.options["pawnfirst"] || + (c == 'w' && i < this.size.x - 1) || + (c == 'b' && i > 0) + ) + ); + } + getCurrentScore(move_s) { const score = super.getCurrentScore(move_s); if (score != "*") diff --git a/variants/Dobutsu/class.js b/variants/Dobutsu/class.js index 29b0708..d8ceafa 100644 --- a/variants/Dobutsu/class.js +++ b/variants/Dobutsu/class.js @@ -87,11 +87,16 @@ export default class DobutsuRules extends ChessRules { const res = super.getCurrentScore(move_s); if (res != '*') return res; - const oppCol = C.GetOppTurn(this.turn); - const oppLastRank = (oppCol == 'b' ? 3 : 0); - for (let j=0; j < this.size.y; j++) { - if (this.board[oppLastRank][j] == oppCol + 'k') - return (oppCol == 'w' ? "1-0" : "0-1"); + for (let lastRank of [0, 3]) { + const color = (lastRank == 0 ? 'w' : 'b'); + for (let j=0; j < this.size.y; j++) { + if ( + this.board[lastRank][j] == color + 'k' && + !this.underAttack([lastRank, j], [C.GetOppTurn(color)]) + ) { + return (color == 'w' ? "1-0" : "0-1"); + } + } } return "*"; } -- 2.48.1 From 66ab134b7ab3ba00204fb316ba7636c904331d6c Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 4 Jan 2024 11:19:10 +0100 Subject: [PATCH 02/16] Add Doublearmy. Start thinking about Dynamo --- README.md | 10 +- TODO | 3 - initialize.sh | 8 + pieces/black_commoner.svg | 105 ++++ pieces/white_commoner.svg | 94 +++ variants.js | 4 +- variants/Doublearmy/class.js | 43 ++ variants/Doublearmy/rules.html | 6 + variants/Doublearmy/style.css | 9 + variants/Dynamo/class.js | 921 ++++++++++++++++++++++++++++ variants/Dynamo/complete_rules.html | 142 +++++ variants/Dynamo/rules.html | 24 + variants/Dynamo/style.css | 1 + 13 files changed, 1359 insertions(+), 11 deletions(-) create mode 100755 initialize.sh create mode 100644 pieces/black_commoner.svg create mode 100644 pieces/white_commoner.svg create mode 100644 variants/Doublearmy/class.js create mode 100644 variants/Doublearmy/rules.html create mode 100644 variants/Doublearmy/style.css create mode 100644 variants/Dynamo/class.js create mode 100644 variants/Dynamo/complete_rules.html create mode 100644 variants/Dynamo/rules.html create mode 100644 variants/Dynamo/style.css diff --git a/README.md b/README.md index 4636ac4..0b900c5 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,10 @@ PHP + Node.js + npm. ## Usage -```wget https://xogo.live/assets.zip && unzip assets.zip```
-```wget https://xogo.live/extras.zip && unzip extras.zip```
-Rename parameters.js.dist → parameters.js, and edit file.
-```npm i``` +Initialisation (done once): -Generate some pieces:
-```python generateSVG.py``` in pieces/Avalam +```./initialize.sh``` + +You may want to edit the parameters.js file. Then: ```./start.sh``` (and later, ```./stop.sh```) diff --git a/TODO b/TODO index fac46e3..8afe466 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,3 @@ -add variants : -Dark Racing Kings ? Checkered-Teleport ? - Hmm... non ? --> Otage, Emergo, Pacosako : fonction "buildPiece(arg1, arg2)" returns HTML element with 2 SVG or SVG + number ==> plus simple : deux classes, images superposées. diff --git a/initialize.sh b/initialize.sh new file mode 100755 index 0000000..b7a9f4f --- /dev/null +++ b/initialize.sh @@ -0,0 +1,8 @@ +!#/bin/sh + +wget https://xogo.live/assets.zip && unzip assets.zip +wget https://xogo.live/extras.zip && unzip extras.zip +cp parameters.js.dist parameters.js +npm i +cd pieces/Avalam && python generateSVG.py +#cd pieces/Emergo && python generateSVG.py diff --git a/pieces/black_commoner.svg b/pieces/black_commoner.svg new file mode 100644 index 0000000..0995449 --- /dev/null +++ b/pieces/black_commoner.svg @@ -0,0 +1,105 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pieces/white_commoner.svg b/pieces/white_commoner.svg new file mode 100644 index 0000000..12f2b27 --- /dev/null +++ b/pieces/white_commoner.svg @@ -0,0 +1,94 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/variants.js b/variants.js index f42033e..10cf42a 100644 --- a/variants.js +++ b/variants.js @@ -45,9 +45,9 @@ const variants = [ {name: 'Dice', desc: 'Roll the dice'}, {name: 'Discoduel', desc: 'Enter the disco', disp: 'Disco Duel'}, {name: 'Dobutsu', desc: "Let's catch the Lion!"}, -// {name: 'Doublearmy', desc: '64 pieces on the board', disp: 'Double Army'}, + {name: 'Doublearmy', desc: '64 pieces on the board', disp: 'Double Army'}, {name: 'Doublemove', desc: 'Double moves'}, -// {name: 'Dynamo', desc: 'Push and pull'}, + {name: 'Dynamo', desc: 'Push and pull'}, // {name: 'Eightpieces', desc: 'Each piece is unique', disp: '8 Pieces'}, // {name: 'Emergo', desc: 'Stacking Checkers variant'}, // {name: 'Empire', desc: 'Empire versus Kingdom'}, diff --git a/variants/Doublearmy/class.js b/variants/Doublearmy/class.js new file mode 100644 index 0000000..abbb564 --- /dev/null +++ b/variants/Doublearmy/class.js @@ -0,0 +1,43 @@ +import ChessRules from "/base_rules.js"; + +export default class DoublearmyRules extends ChessRules { + + static get Options() { + return { + select: C.Options.select, + input: C.Options.input, + styles: C.Options.styles.filter(s => s != "madrasi") + }; + } + + pieces(color, x, y) { + let res = super.pieces(color, x, y); + return Object.assign( + { + 'c': { + "class": "commoner", + moveas: 'k' + } + }, + res + ); + } + + genRandInitBaseFen() { + const s = super.genRandInitBaseFen(); + const rows = s.fen.split('/'); + return { + fen: + rows[0] + "/" + + rows[1] + "/" + + rows[0].replace('k', 'c') + "/" + + rows[1] + "/" + + rows[6] + "/" + + rows[7].replace('K', 'C') + "/" + + rows[6] + "/" + + rows[7], + o: s.o + }; + } + +}; diff --git a/variants/Doublearmy/rules.html b/variants/Doublearmy/rules.html new file mode 100644 index 0000000..b9908ab --- /dev/null +++ b/variants/Doublearmy/rules.html @@ -0,0 +1,6 @@ +

+ The four middle ranks contain a replica of the initial pieces. + The central "king" has no royal status, and is thus named "commoner". +

+ +

Vincent Rothuis (2020).

diff --git a/variants/Doublearmy/style.css b/variants/Doublearmy/style.css new file mode 100644 index 0000000..e50c2f4 --- /dev/null +++ b/variants/Doublearmy/style.css @@ -0,0 +1,9 @@ +@import url("/base_pieces.css"); + +piece.black.commoner { + background-image: url('/pieces/black_commoner.svg'); +} + +piece.white.commoner { + background-image: url('/pieces/white_commoner.svg'); +} diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js new file mode 100644 index 0000000..0996a70 --- /dev/null +++ b/variants/Dynamo/class.js @@ -0,0 +1,921 @@ +import ChessRules from "/base_rules.js"; + +export default class DynamoRules extends ChessRules { + + // TODO? later, allow to push out pawns on a and h files + get hasEnpassant() { + return false; + } + +/// TODO::: + + canIplay(side, [x, y]) { + // Sometimes opponent's pieces can be moved directly + return this.turn == side; + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + this.subTurn = 1; + // Local stack of "action moves" + this.amoves = []; + const amove = V.ParseFen(fen).amove; + if (amove != "-") { + const amoveParts = amove.split("/"); + let move = { + // No need for start & end + appear: [], + vanish: [] + }; + [0, 1].map(i => { + if (amoveParts[i] != "-") { + amoveParts[i].split(".").forEach(av => { + // Format is "bpe3" + const xy = V.SquareToCoords(av.substr(2)); + move[i == 0 ? "appear" : "vanish"].push( + new PiPo({ + x: xy.x, + y: xy.y, + c: av[0], + p: av[1] + }) + ); + }); + } + }); + this.amoves.push(move); + } + // Stack "first moves" (on subTurn 1) to merge and check opposite moves + this.firstMove = []; + } + + static ParseFen(fen) { + return Object.assign( + ChessRules.ParseFen(fen), + { amove: fen.split(" ")[4] } + ); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParts = fen.split(" "); + if (fenParts.length != 5) return false; + if (fenParts[4] != "-") { + // TODO: a single regexp instead. + // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-' + const amoveParts = fenParts[4].split("/"); + if (amoveParts.length != 2) return false; + for (let part of amoveParts) { + if (part != "-") { + for (let psq of part.split(".")) + if (!psq.match(/^[a-z]{3}[1-8]$/)) return false; + } + } + } + return true; + } + + getFen() { + return super.getFen() + " " + this.getAmoveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getAmoveFen(); + } + + getAmoveFen() { + const L = this.amoves.length; + if (L == 0) return "-"; + return ( + ["appear","vanish"].map( + mpart => { + if (this.amoves[L-1][mpart].length == 0) return "-"; + return ( + this.amoves[L-1][mpart].map( + av => { + const square = V.CoordsToSquare({ x: av.x, y: av.y }); + return av.c + av.p + square; + } + ).join(".") + ); + } + ).join("/") + ); + } + + canTake() { + // Captures don't occur (only pulls & pushes) + return false; + } + + // Step is right, just add (push/pull) moves in this direction + // Direction is assumed normalized. + getMovesInDirection([x, y], [dx, dy], nbSteps) { + nbSteps = nbSteps || 8; //max 8 steps anyway + let [i, j] = [x + dx, y + dy]; + let moves = []; + const color = this.getColor(x, y); + const piece = this.getPiece(x, y); + const lastRank = (color == 'w' ? 0 : 7); + let counter = 1; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (i == lastRank && piece == V.PAWN) { + // Promotion by push or pull + V.PawnSpecs.promotions.forEach(p => { + let move = super.getBasicMove([x, y], [i, j], { c: color, p: p }); + moves.push(move); + }); + } + else moves.push(super.getBasicMove([x, y], [i, j])); + if (++counter > nbSteps) break; + i += dx; + j += dy; + } + if (!V.OnBoard(i, j) && piece != V.KING) { + // Add special "exit" move, by "taking king" + moves.push( + new Move({ + start: { x: x, y: y }, + end: { x: this.kingPos[color][0], y: this.kingPos[color][1] }, + appear: [], + vanish: [{ x: x, y: y, c: color, p: piece }] + }) + ); + } + return moves; + } + + // Normalize direction to know the step + getNormalizedDirection([dx, dy]) { + const absDir = [Math.abs(dx), Math.abs(dy)]; + let divisor = 0; + if (absDir[0] != 0 && absDir[1] != 0 && absDir[0] != absDir[1]) + // Knight + divisor = Math.min(absDir[0], absDir[1]); + else + // Standard slider (or maybe a pawn or king: same) + divisor = Math.max(absDir[0], absDir[1]); + return [dx / divisor, dy / divisor]; + } + + // There was something on x2,y2, maybe our color, pushed or (self)pulled + isAprioriValidExit([x1, y1], [x2, y2], color2, piece2) { + const color1 = this.getColor(x1, y1); + const pawnShift = (color1 == 'w' ? -1 : 1); + const lastRank = (color1 == 'w' ? 0 : 7); + const deltaX = Math.abs(x1 - x2); + const deltaY = Math.abs(y1 - y2); + const checkSlider = () => { + const dir = this.getNormalizedDirection([x2 - x1, y2 - y1]); + let [i, j] = [x1 + dir[0], y1 + dir[1]]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += dir[0]; + j += dir[1]; + } + return !V.OnBoard(i, j); + }; + switch (piece2 || this.getPiece(x1, y1)) { + case V.PAWN: + return ( + x1 + pawnShift == x2 && + ( + (color1 == color2 && x2 == lastRank && y1 == y2) || + ( + color1 != color2 && + deltaY == 1 && + !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + ) + ) + ); + case V.ROOK: + if (x1 != x2 && y1 != y2) return false; + return checkSlider(); + case V.KNIGHT: + return ( + deltaX + deltaY == 3 && + (deltaX == 1 || deltaY == 1) && + !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + ); + case V.BISHOP: + if (deltaX != deltaY) return false; + return checkSlider(); + case V.QUEEN: + if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) return false; + return checkSlider(); + case V.KING: + return ( + deltaX <= 1 && + deltaY <= 1 && + !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + ); + } + return false; + } + + isAprioriValidVertical([x1, y1], x2) { + const piece = this.getPiece(x1, y1); + const deltaX = Math.abs(x1 - x2); + const startRank = (this.getColor(x1, y1) == 'w' ? 6 : 1); + return ( + [V.QUEEN, V.ROOK].includes(piece) || + ( + [V.KING, V.PAWN].includes(piece) && + ( + deltaX == 1 || + (deltaX == 2 && piece == V.PAWN && x1 == startRank) + ) + ) + ); + } + + // NOTE: for pushes, play the pushed piece first. + // for pulls: play the piece doing the action first + // NOTE: to push a piece out of the board, make it slide until its king + getPotentialMovesFrom([x, y]) { + const color = this.turn; + const sqCol = this.getColor(x, y); + const pawnShift = (color == 'w' ? -1 : 1); + const pawnStartRank = (color == 'w' ? 6 : 1); + const getMoveHash = (m) => { + return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end); + }; + if (this.subTurn == 1) { + const addMoves = (dir, nbSteps) => { + const newMoves = + this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps) + .filter(m => !movesHash[getMoveHash(m)]); + newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; }); + Array.prototype.push.apply(moves, newMoves); + }; + // Free to play any move (if piece of my color): + let moves = + sqCol == color + ? super.getPotentialMovesFrom([x, y]) + : []; + // There may be several suicide moves: keep only one + let hasExit = false; + moves = moves.filter(m => { + const suicide = (m.appear.length == 0); + if (suicide) { + if (hasExit) return false; + hasExit = true; + } + return true; + }); + // Structure to avoid adding moves twice (can be action & move) + let movesHash = {}; + moves.forEach(m => { movesHash[getMoveHash(m)] = true; }); + // [x, y] is pushed by 'color' + for (let step of V.steps[V.KNIGHT]) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.KNIGHT + ) { + addMoves(step, 1); + } + } + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + let [i, j] = [x + step[0], y + step[1]]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += step[0]; + j += step[1]; + } + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + const deltaX = Math.abs(i - x); + const deltaY = Math.abs(j - y); + switch (this.getPiece(i, j)) { + case V.PAWN: + if ( + (x - i) / deltaX == pawnShift && + deltaX <= 2 && + deltaY <= 1 + ) { + if (sqCol == color && deltaY == 0) { + // Pushed forward + const maxSteps = (i == pawnStartRank && deltaX == 1 ? 2 : 1); + addMoves(step, maxSteps); + } + else if (sqCol != color && deltaY == 1 && deltaX == 1) + // Pushed diagonally + addMoves(step, 1); + } + break; + case V.ROOK: + if (deltaX == 0 || deltaY == 0) addMoves(step); + break; + case V.BISHOP: + if (deltaX == deltaY) addMoves(step); + break; + case V.QUEEN: + // All steps are valid for a queen: + addMoves(step); + break; + case V.KING: + if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1); + break; + } + } + } + return moves; + } + // If subTurn == 2 then we should have a first move, + // which restrict what we can play now: only in the first move direction + const L = this.firstMove.length; + const fm = this.firstMove[L-1]; + if ( + (fm.appear.length == 2 && fm.vanish.length == 2) || + (fm.vanish[0].c == sqCol && sqCol != color) + ) { + // Castle or again opponent color: no move playable then. + return []; + } + const piece = this.getPiece(x, y); + const getPushExit = () => { + // Piece at subTurn 1 exited: can I have caused the exit? + if ( + this.isAprioriValidExit( + [x, y], + [fm.start.x, fm.start.y], + fm.vanish[0].c + ) + ) { + // Seems so: + const dir = this.getNormalizedDirection( + [fm.start.x - x, fm.start.y - y]); + const nbSteps = + [V.PAWN, V.KING, V.KNIGHT].includes(piece) + ? 1 + : null; + return this.getMovesInDirection([x, y], dir, nbSteps); + } + return []; + } + const getPushMoves = () => { + // Piece from subTurn 1 is still on board: + const dirM = this.getNormalizedDirection( + [fm.end.x - fm.start.x, fm.end.y - fm.start.y]); + const dir = this.getNormalizedDirection( + [fm.start.x - x, fm.start.y - y]); + // Normalized directions should match + if (dir[0] == dirM[0] && dir[1] == dirM[1]) { + // We don't know if first move is a pushed piece or normal move, + // so still must check if the push is valid. + const deltaX = Math.abs(fm.start.x - x); + const deltaY = Math.abs(fm.start.y - y); + switch (piece) { + case V.PAWN: + if (x == pawnStartRank) { + if ( + (fm.start.x - x) * pawnShift < 0 || + deltaX >= 3 || + deltaY >= 2 || + (fm.vanish[0].c == color && deltaY > 0) || + (fm.vanish[0].c != color && deltaY == 0) || + Math.abs(fm.end.x - fm.start.x) > deltaX || + fm.end.y - fm.start.y != fm.start.y - y + ) { + return []; + } + } + else { + if ( + fm.start.x - x != pawnShift || + deltaY >= 2 || + (fm.vanish[0].c == color && deltaY == 1) || + (fm.vanish[0].c != color && deltaY == 0) || + fm.end.x - fm.start.x != pawnShift || + fm.end.y - fm.start.y != fm.start.y - y + ) { + return []; + } + } + break; + case V.KNIGHT: + if ( + (deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) || + (fm.end.x - fm.start.x != fm.start.x - x) || + (fm.end.y - fm.start.y != fm.start.y - y) + ) { + return []; + } + break; + case V.KING: + if ( + (deltaX >= 2 || deltaY >= 2) || + (fm.end.x - fm.start.x != fm.start.x - x) || + (fm.end.y - fm.start.y != fm.start.y - y) + ) { + return []; + } + break; + case V.BISHOP: + if (deltaX != deltaY) return []; + break; + case V.ROOK: + if (deltaX != 0 && deltaY != 0) return []; + break; + case V.QUEEN: + if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return []; + break; + } + // Nothing should stand between [x, y] and the square fm.start + let [i, j] = [x + dir[0], y + dir[1]]; + while ( + (i != fm.start.x || j != fm.start.y) && + this.board[i][j] == V.EMPTY + ) { + i += dir[0]; + j += dir[1]; + } + if (i == fm.start.x && j == fm.start.y) + return this.getMovesInDirection([x, y], dir); + } + return []; + } + const getPullExit = () => { + // Piece at subTurn 1 exited: can I be pulled? + // Note: kings cannot suicide, so fm.vanish[0].p is not KING. + // Could be PAWN though, if a pawn was pushed out of board. + if ( + fm.vanish[0].p != V.PAWN && //pawns cannot pull + this.isAprioriValidExit( + [x, y], + [fm.start.x, fm.start.y], + fm.vanish[0].c, + fm.vanish[0].p + ) + ) { + // Seems so: + const dir = this.getNormalizedDirection( + [fm.start.x - x, fm.start.y - y]); + const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null); + return this.getMovesInDirection([x, y], dir, nbSteps); + } + return []; + }; + const getPullMoves = () => { + if (fm.vanish[0].p == V.PAWN) + // pawns cannot pull + return []; + const dirM = this.getNormalizedDirection( + [fm.end.x - fm.start.x, fm.end.y - fm.start.y]); + const dir = this.getNormalizedDirection( + [fm.start.x - x, fm.start.y - y]); + // Normalized directions should match + if (dir[0] == dirM[0] && dir[1] == dirM[1]) { + // Am I at the right distance? + const deltaX = Math.abs(x - fm.start.x); + const deltaY = Math.abs(y - fm.start.y); + if ( + (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) || + (fm.vanish[0].p == V.KNIGHT && + (deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0)) + ) { + return []; + } + // Nothing should stand between [x, y] and the square fm.start + let [i, j] = [x + dir[0], y + dir[1]]; + while ( + (i != fm.start.x || j != fm.start.y) && + this.board[i][j] == V.EMPTY + ) { + i += dir[0]; + j += dir[1]; + } + if (i == fm.start.x && j == fm.start.y) + return this.getMovesInDirection([x, y], dir); + } + return []; + }; + if (fm.vanish[0].c != color) { + // Only possible action is a push: + if (fm.appear.length == 0) return getPushExit(); + return getPushMoves(); + } + else if (sqCol != color) { + // Only possible action is a pull, considering moving piece abilities + if (fm.appear.length == 0) return getPullExit(); + return getPullMoves(); + } + else { + // My color + my color: both actions possible + // Structure to avoid adding moves twice (can be action & move) + let movesHash = {}; + if (fm.appear.length == 0) { + const pushes = getPushExit(); + pushes.forEach(m => { movesHash[getMoveHash(m)] = true; }); + return ( + pushes.concat(getPullExit().filter(m => !movesHash[getMoveHash(m)])) + ); + } + const pushes = getPushMoves(); + pushes.forEach(m => { movesHash[getMoveHash(m)] = true; }); + return ( + pushes.concat(getPullMoves().filter(m => !movesHash[getMoveHash(m)])) + ); + } + return []; + } + + getSlideNJumpMoves([x, y], steps, oneStep) { + let moves = []; + const c = this.getColor(x, y); + const piece = this.getPiece(x, y); + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [i, j])); + if (oneStep) continue outerLoop; + i += step[0]; + j += step[1]; + } + if (V.OnBoard(i, j)) { + if (this.canTake([x, y], [i, j])) + moves.push(this.getBasicMove([x, y], [i, j])); + } + else { + // Add potential board exit (suicide), except for the king + if (piece != V.KING) { + moves.push({ + start: { x: x, y: y}, + end: { x: this.kingPos[c][0], y: this.kingPos[c][1] }, + appear: [], + vanish: [ + new PiPo({ + x: x, + y: y, + c: c, + p: piece + }) + ] + }); + } + } + } + return moves; + } + + // Does m2 un-do m1 ? (to disallow undoing actions) + oppositeMoves(m1, m2) { + const isEqual = (av1, av2) => { + for (let av of av1) { + const avInAv2 = av2.find(elt => { + return ( + elt.x == av.x && + elt.y == av.y && + elt.c == av.c && + elt.p == av.p + ); + }); + if (!avInAv2) return false; + } + return true; + }; + // All appear and vanish arrays must have the same length + const mL = m1.appear.length; + return ( + m2.appear.length == mL && + m1.vanish.length == mL && + m2.vanish.length == mL && + isEqual(m1.appear, m2.vanish) && + isEqual(m1.vanish, m2.appear) + ); + } + + getAmove(move1, move2) { + // Just merge (one is action one is move, one may be empty) + return { + appear: move1.appear.concat(move2.appear), + vanish: move1.vanish.concat(move2.vanish) + } + } + + filterValid(moves) { + const color = this.turn; + const La = this.amoves.length; + if (this.subTurn == 1) { + return moves.filter(m => { + // A move is valid either if it doesn't result in a check, + // or if a second move is possible to counter the check + // (not undoing a potential move + action of the opponent) + this.play(m); + let res = this.underCheck(color); + if (this.subTurn == 2) { + let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m); + if (res || isOpposite) { + const moves2 = this.getAllPotentialMoves(); + for (let m2 of moves2) { + this.play(m2); + const res2 = this.underCheck(color); + const amove = this.getAmove(m, m2); + isOpposite = + La > 0 && this.oppositeMoves(this.amoves[La-1], amove); + this.undo(m2); + if (!res2 && !isOpposite) { + res = false; + break; + } + } + } + } + this.undo(m); + return !res; + }); + } + if (La == 0) return super.filterValid(moves); + const Lf = this.firstMove.length; + return ( + super.filterValid( + moves.filter(m => { + // Move shouldn't undo another: + const amove = this.getAmove(this.firstMove[Lf-1], m); + return !this.oppositeMoves(this.amoves[La-1], amove); + }) + ) + ); + } + + isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) { + for (let step of steps) { + let rx = x + step[0], + ry = y + step[1]; + while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { + rx += step[0]; + ry += step[1]; + } + if ( + V.OnBoard(rx, ry) && + this.getPiece(rx, ry) == piece && + this.getColor(rx, ry) == color + ) { + // Continue some steps in the same direction (pull) + rx += step[0]; + ry += step[1]; + while ( + V.OnBoard(rx, ry) && + this.board[rx][ry] == V.EMPTY && + !oneStep + ) { + rx += step[0]; + ry += step[1]; + } + if (!V.OnBoard(rx, ry)) return true; + // Step in the other direction (push) + rx = x - step[0]; + ry = y - step[1]; + while ( + V.OnBoard(rx, ry) && + this.board[rx][ry] == V.EMPTY && + !oneStep + ) { + rx -= step[0]; + ry -= step[1]; + } + if (!V.OnBoard(rx, ry)) return true; + } + } + return false; + } + + isAttackedByPawn([x, y], color) { + // The king can be pushed out by a pawn on last rank or near the edge + const pawnShift = (color == "w" ? 1 : -1); + for (let i of [-1, 1]) { + if ( + V.OnBoard(x + pawnShift, y + i) && + this.board[x + pawnShift][y + i] != V.EMPTY && + this.getPiece(x + pawnShift, y + i) == V.PAWN && + this.getColor(x + pawnShift, y + i) == color + ) { + if (!V.OnBoard(x - pawnShift, y - i)) return true; + } + } + return false; + } + + static OnTheEdge(x, y) { + return (x == 0 || x == 7 || y == 0 || y == 7); + } + + isAttackedByKing([x, y], color) { + // Attacked if I'm on the edge and the opponent king just next, + // but not on the edge. + if (V.OnTheEdge(x, y)) { + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + !V.OnTheEdge(i, j) && + this.board[i][j] != V.EMPTY && + this.getPiece(i, j) == V.KING + // NOTE: since only one king of each color, and (x, y) is occupied + // by our king, no need to check other king's color. + ) { + return true; + } + } + } + return false; + } + + // No consideration of color: all pieces could be played + getAllPotentialMoves() { + let potentialMoves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY) { + Array.prototype.push.apply( + potentialMoves, + this.getPotentialMovesFrom([i, j]) + ); + } + } + } + return potentialMoves; + } + + getEmptyMove() { + return new Move({ + start: { x: -1, y: -1 }, + end: { x: -1, y: -1 }, + appear: [], + vanish: [] + }); + } + + doClick(square) { + // A click to promote a piece on subTurn 2 would trigger this. + // For now it would then return [NaN, NaN] because surrounding squares + // have no IDs in the promotion modal. TODO: improve this? + if (isNaN(square[0])) return null; + // If subTurn == 2 && square is empty && !underCheck && !isOpposite, + // then return an empty move, allowing to "pass" subTurn2 + const La = this.amoves.length; + const Lf = this.firstMove.length; + if ( + this.subTurn == 2 && + this.board[square[0]][square[1]] == V.EMPTY && + !this.underCheck(this.turn) && + (La == 0 || !this.oppositeMoves(this.amoves[La-1], this.firstMove[Lf-1])) + ) { + return this.getEmptyMove(); + } + return null; + } + + play(move) { + if (this.subTurn == 1 && move.vanish.length == 0) { + // Patch to work with old format: (TODO: remove later) + move.ignore = true; + return; + } + const color = this.turn; + move.subTurn = this.subTurn; //for undo + const gotoNext = (mv) => { + const L = this.firstMove.length; + this.amoves.push(this.getAmove(this.firstMove[L-1], mv)); + this.turn = V.GetOppCol(color); + this.subTurn = 1; + this.movesCount++; + }; + move.flags = JSON.stringify(this.aggregateFlags()); + V.PlayOnBoard(this.board, move); + if (this.subTurn == 2) gotoNext(move); + else { + this.subTurn = 2; + this.firstMove.push(move); + this.toNewKingPos(move); + if ( + // Condition is true on empty arrays: + this.getAllPotentialMoves().every(m => { + V.PlayOnBoard(this.board, m); + this.toNewKingPos(m); + const res = this.underCheck(color); + V.UndoOnBoard(this.board, m); + this.toOldKingPos(m); + return res; + }) + ) { + // No valid move at subTurn 2 + gotoNext(this.getEmptyMove()); + } + this.toOldKingPos(move); + } + this.postPlay(move); + } + + toNewKingPos(move) { + for (let a of move.appear) + if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y]; + } + + postPlay(move) { + if (move.start.x < 0) return; + this.toNewKingPos(move); + this.updateCastleFlags(move); + } + + updateCastleFlags(move) { + const firstRank = { 'w': V.size.x - 1, 'b': 0 }; + for (let v of move.vanish) { + if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y]; + else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) { + const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1); + this.castleFlags[v.c][flagIdx] = V.size.y; + } + } + } + + undo(move) { + if (!!move.ignore) return; //TODO: remove that later + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + if (this.subTurn == 1) { + this.amoves.pop(); + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + } + if (move.subTurn == 1) this.firstMove.pop(); + this.subTurn = move.subTurn; + this.toOldKingPos(move); + } + + toOldKingPos(move) { + // (Potentially) Reset king position + for (let v of move.vanish) + if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y]; + } + + getComputerMove() { + let moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + // "Search" at depth 1 for now + const maxeval = V.INFINITY; + const color = this.turn; + const emptyMove = { + start: { x: -1, y: -1 }, + end: { x: -1, y: -1 }, + appear: [], + vanish: [] + }; + moves.forEach(m => { + this.play(m); + if (this.turn != color) m.eval = this.evalPosition(); + else { + m.eval = (color == "w" ? -1 : 1) * maxeval; + const moves2 = this.getAllValidMoves().concat([emptyMove]); + m.next = moves2[0]; + moves2.forEach(m2 => { + this.play(m2); + const score = this.getCurrentScore(); + let mvEval = 0; + if (score != "1/2") { + if (score != "*") mvEval = (score == "1-0" ? 1 : -1) * maxeval; + else mvEval = this.evalPosition(); + } + if ( + (color == 'w' && mvEval > m.eval) || + (color == 'b' && mvEval < m.eval) + ) { + m.eval = mvEval; + m.next = m2; + } + this.undo(m2); + }); + } + this.undo(m); + }); + moves.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) + candidates.push(i); + const mIdx = candidates[randInt(candidates.length)]; + if (!moves[mIdx].next) return moves[mIdx]; + const move2 = moves[mIdx].next; + delete moves[mIdx]["next"]; + return [moves[mIdx], move2]; + } + + getNotation(move) { + if (move.start.x < 0) + // A second move is always required, but may be empty + return "-"; + const initialSquare = V.CoordsToSquare(move.start); + const finalSquare = V.CoordsToSquare(move.end); + if (move.appear.length == 0) + // Pushed or pulled out of the board + return initialSquare + "R"; + return move.appear[0].p.toUpperCase() + initialSquare + finalSquare; + } + +}; diff --git a/variants/Dynamo/complete_rules.html b/variants/Dynamo/complete_rules.html new file mode 100644 index 0000000..68e9cc0 --- /dev/null +++ b/variants/Dynamo/complete_rules.html @@ -0,0 +1,142 @@ + + + Dynamo Rules + + + + +
+

Dynamo Rules

+ +

+ Pieces have the same movement as in orthodox chess, but they cannot + take other pieces in the usual way. Instead of the normal captures, pieces + can pull or push other pieces, potentially off the board. + The goal is to send the enemy king off the board. +

+ +

Each turn, a player has the following options:

+
    +
  • + Move one of his pieces normally, then optionally pull something as an + effect of this move. +
  • +
  • + Push any piece with one of his pieces, then optionally follow the pushed + piece. +
  • + +

    + It seems easier to understand with some examples. For a detailed + introduction please visit + + this page + (in French). +

    + +
    +
    +
    +
    Possible "pawn moves" in the initial position.
    +
    + +

    + The e2 pawn can move to e3 and e4 as usual. It can also slide diagonally, + being pushed by the bishop or the queen (which may or may not move along + this line afterward). It can also go to c3, being pushed by the knight from + g1; then the knight can move to e2, or stay motionless. + Finally, the pawn can "take the king": this is a special move indicating that + you want it to exit the board. Indeed it could be pushed off the board by the + bishop or the queen. +

    + +

    + Note: if an action is possible but you don't want to play a second part in + a move, click on any empty square: this will send an empty move. +

    + +
    +
    +
    +
    +
    +
    + Pulling the d5 pawn to c3 (left: before, right: after). +
    +
    + +
      +
    • Pawns cannot pull (because they only move forward).
    • +
    • + When they could reach the square beyond the edge, + pieces can exit the board by themselves, possibly dragging another piece + out (friendly or enemy). +
    • +
    + +
    +
    +
    +
    + Check: the queen threatens to pull the king off the board + along the a4-e8 diagonal. +
    +
    + +

    + It is forbidden to undo a "move + action". For example here, white could + push back the black bishop on g7 but not return to d4 then. +

    + +
    +
    +
    +
    +
    + Pushing the d4 bishop to b2 (left: before, right: after). +
    +
    + +

    + Castling is possible as long as the king and rook have not moved and + haven't been pushed or pulled (this differs from the chessvariants + description). +

    + +

    End of the game

    + +

    + The game ends when a push or pull action threatens to send the king off the + board, and he has no way to escape it. +

    + +
    +
    +
    +
    Dynamo checkmate ("Dynamate" :) )
    +
    + +

    + The king cannot "take" on g4: this would just push the queen one step to the + left, and she would then push the king beyond the 'h' file. + There are no en-passant captures. +

    + +

    Source

    + +

    + + Dynamo chess + + on chessvariants.com. The short description given on + this page + might help too. +

    diff --git a/variants/Dynamo/rules.html b/variants/Dynamo/rules.html new file mode 100644 index 0000000..4a78e5a --- /dev/null +++ b/variants/Dynamo/rules.html @@ -0,0 +1,24 @@ +

    + Moves are potentially played in two times: + move a piece, and / or push or pull something with that unit. +

    + +

    Each turn, a player has the following options:

    +
      +
    • + Move one of his pieces normally, + then optionally pull something as an effect of this move. +
    • +
    • + Push any piece with one of his pieces, + then optionally follow the pushed piece. +
    • +
    + +

    + + Full rules description. + +

    + +

    Hans Kluever and Peter Kahl (1968).

    diff --git a/variants/Dynamo/style.css b/variants/Dynamo/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Dynamo/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css"); -- 2.48.1 From 939e06bf9febef0a7935c7f6a58c5e28dee4dedc Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 5 Jan 2024 09:58:16 +0100 Subject: [PATCH 03/16] Start working on Dynamo --- base_rules.js | 22 ++- variants/Dynamo/class.js | 289 +++++---------------------------------- 2 files changed, 52 insertions(+), 259 deletions(-) diff --git a/base_rules.js b/base_rules.js index 9a364e8..3d8e463 100644 --- a/base_rules.js +++ b/base_rules.js @@ -175,6 +175,22 @@ export default class ChessRules { return Object.values(cd).map(c => c.toString(36)).join(""); } + // c10 --> 02 (assuming 10 rows) + static SquareFromUsual(sq) { + return ( + (this.size.x - parseInt(sq.substring(1), 10)).toString(36) + + (sq.charCodeAt(0) - 97).toString(36) + ); + } + + // 02 --> c10 + static UsualFromSquare(sq) { + return ( + String.fromCharCode(parseInt(sq.charAt(1), 36) + 97) + + (this.size.x - parseInt(sq.charAt(0), 36)).toString(10) + ); + } + coordsToId(cd) { if (typeof cd.x == "number") { return ( @@ -651,10 +667,8 @@ export default class ChessRules { this[arrName] = ArrayFun.init(this.size.x, this.size.y, null); if (arrName == "d_pieces") this.marks.forEach((m) => { - const formattedSquare = - (this.size.x - parseInt(m.substring(1), 10)).toString(36) + - (m.charCodeAt(0) - 97).toString(36); - const mCoords = V.SquareToCoords(formattedSquare); + const formattedSquare = C.SquareFromUsual(m); + const mCoords = C.SquareToCoords(formattedSquare); addPiece(mCoords.x, mCoords.y, arrName, "mark"); }); }; diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index 0996a70..05f2aed 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -2,110 +2,51 @@ import ChessRules from "/base_rules.js"; export default class DynamoRules extends ChessRules { - // TODO? later, allow to push out pawns on a and h files - get hasEnpassant() { - return false; + static get Options() { + // TODO } -/// TODO::: + get hasEnpassant() { + return this.options["enpassant"]; + } - canIplay(side, [x, y]) { + canIplay(x, y) { // Sometimes opponent's pieces can be moved directly - return this.turn == side; + return this.playerColor == this.turn; + } + + canTake() { + // Captures don't occur (only pulls & pushes) + return false; } - setOtherVariables(fen) { - super.setOtherVariables(fen); + setOtherVariables(fenParsed) { + super.setOtherVariables(fenParsed); this.subTurn = 1; - // Local stack of "action moves" - this.amoves = []; - const amove = V.ParseFen(fen).amove; - if (amove != "-") { - const amoveParts = amove.split("/"); - let move = { - // No need for start & end - appear: [], - vanish: [] - }; - [0, 1].map(i => { - if (amoveParts[i] != "-") { - amoveParts[i].split(".").forEach(av => { - // Format is "bpe3" - const xy = V.SquareToCoords(av.substr(2)); - move[i == 0 ? "appear" : "vanish"].push( - new PiPo({ - x: xy.x, - y: xy.y, - c: av[0], - p: av[1] - }) - ); - }); - } + // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5 + // for example, and moving herself to g4. If just move: e2h5 + this.lastAction = []; + if (fenParsed.amove != '-') { + this.lastAction = fenParsed.amove.split('/').map(a => { + return { + c1: C.SquareToCoords(C.SquareFromUsual(a.substr(0, 2))), + c2: C.SquareToCoords(C.SquareFromUsual(a.substr(2, 2))) + }; }); - this.amoves.push(move); } - // Stack "first moves" (on subTurn 1) to merge and check opposite moves - this.firstMove = []; - } - - static ParseFen(fen) { - return Object.assign( - ChessRules.ParseFen(fen), - { amove: fen.split(" ")[4] } - ); } - static IsGoodFen(fen) { - if (!ChessRules.IsGoodFen(fen)) return false; - const fenParts = fen.split(" "); - if (fenParts.length != 5) return false; - if (fenParts[4] != "-") { - // TODO: a single regexp instead. - // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-' - const amoveParts = fenParts[4].split("/"); - if (amoveParts.length != 2) return false; - for (let part of amoveParts) { - if (part != "-") { - for (let psq of part.split(".")) - if (!psq.match(/^[a-z]{3}[1-8]$/)) return false; - } - } + getPartFen(o) { + let res = super.getPartFen(o); + if (o.init) + res["amove"] = '-'; + else { + res["amove"] = this.lastAction.map(a => { + C.UsualFromSquare(C.CoordsToSquare(a.c1)) + + C.UsualFromSquare(C.CoordsToSquare(a.c2)) + }).join('/'); } - return true; - } - - getFen() { - return super.getFen() + " " + this.getAmoveFen(); - } - - getFenForRepeat() { - return super.getFenForRepeat() + "_" + this.getAmoveFen(); - } - - getAmoveFen() { - const L = this.amoves.length; - if (L == 0) return "-"; - return ( - ["appear","vanish"].map( - mpart => { - if (this.amoves[L-1][mpart].length == 0) return "-"; - return ( - this.amoves[L-1][mpart].map( - av => { - const square = V.CoordsToSquare({ x: av.x, y: av.y }); - return av.c + av.p + square; - } - ).join(".") - ); - } - ).join("/") - ); - } - - canTake() { - // Captures don't occur (only pulls & pushes) - return false; + return res; } // Step is right, just add (push/pull) moves in this direction @@ -590,6 +531,7 @@ export default class DynamoRules extends ChessRules { ); } + // TODO: just stack in this.lastAction instead getAmove(move1, move2) { // Just merge (one is action one is move, one may be empty) return { @@ -643,105 +585,6 @@ export default class DynamoRules extends ChessRules { ); } - isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) { - for (let step of steps) { - let rx = x + step[0], - ry = y + step[1]; - while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { - rx += step[0]; - ry += step[1]; - } - if ( - V.OnBoard(rx, ry) && - this.getPiece(rx, ry) == piece && - this.getColor(rx, ry) == color - ) { - // Continue some steps in the same direction (pull) - rx += step[0]; - ry += step[1]; - while ( - V.OnBoard(rx, ry) && - this.board[rx][ry] == V.EMPTY && - !oneStep - ) { - rx += step[0]; - ry += step[1]; - } - if (!V.OnBoard(rx, ry)) return true; - // Step in the other direction (push) - rx = x - step[0]; - ry = y - step[1]; - while ( - V.OnBoard(rx, ry) && - this.board[rx][ry] == V.EMPTY && - !oneStep - ) { - rx -= step[0]; - ry -= step[1]; - } - if (!V.OnBoard(rx, ry)) return true; - } - } - return false; - } - - isAttackedByPawn([x, y], color) { - // The king can be pushed out by a pawn on last rank or near the edge - const pawnShift = (color == "w" ? 1 : -1); - for (let i of [-1, 1]) { - if ( - V.OnBoard(x + pawnShift, y + i) && - this.board[x + pawnShift][y + i] != V.EMPTY && - this.getPiece(x + pawnShift, y + i) == V.PAWN && - this.getColor(x + pawnShift, y + i) == color - ) { - if (!V.OnBoard(x - pawnShift, y - i)) return true; - } - } - return false; - } - - static OnTheEdge(x, y) { - return (x == 0 || x == 7 || y == 0 || y == 7); - } - - isAttackedByKing([x, y], color) { - // Attacked if I'm on the edge and the opponent king just next, - // but not on the edge. - if (V.OnTheEdge(x, y)) { - for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { - const [i, j] = [x + step[0], y + step[1]]; - if ( - V.OnBoard(i, j) && - !V.OnTheEdge(i, j) && - this.board[i][j] != V.EMPTY && - this.getPiece(i, j) == V.KING - // NOTE: since only one king of each color, and (x, y) is occupied - // by our king, no need to check other king's color. - ) { - return true; - } - } - } - return false; - } - - // No consideration of color: all pieces could be played - getAllPotentialMoves() { - let potentialMoves = []; - for (let i = 0; i < V.size.x; i++) { - for (let j = 0; j < V.size.y; j++) { - if (this.board[i][j] != V.EMPTY) { - Array.prototype.push.apply( - potentialMoves, - this.getPotentialMovesFrom([i, j]) - ); - } - } - } - return potentialMoves; - } - getEmptyMove() { return new Move({ start: { x: -1, y: -1 }, @@ -854,68 +697,4 @@ export default class DynamoRules extends ChessRules { if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y]; } - getComputerMove() { - let moves = this.getAllValidMoves(); - if (moves.length == 0) return null; - // "Search" at depth 1 for now - const maxeval = V.INFINITY; - const color = this.turn; - const emptyMove = { - start: { x: -1, y: -1 }, - end: { x: -1, y: -1 }, - appear: [], - vanish: [] - }; - moves.forEach(m => { - this.play(m); - if (this.turn != color) m.eval = this.evalPosition(); - else { - m.eval = (color == "w" ? -1 : 1) * maxeval; - const moves2 = this.getAllValidMoves().concat([emptyMove]); - m.next = moves2[0]; - moves2.forEach(m2 => { - this.play(m2); - const score = this.getCurrentScore(); - let mvEval = 0; - if (score != "1/2") { - if (score != "*") mvEval = (score == "1-0" ? 1 : -1) * maxeval; - else mvEval = this.evalPosition(); - } - if ( - (color == 'w' && mvEval > m.eval) || - (color == 'b' && mvEval < m.eval) - ) { - m.eval = mvEval; - m.next = m2; - } - this.undo(m2); - }); - } - this.undo(m); - }); - moves.sort((a, b) => { - return (color == "w" ? 1 : -1) * (b.eval - a.eval); - }); - let candidates = [0]; - for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++) - candidates.push(i); - const mIdx = candidates[randInt(candidates.length)]; - if (!moves[mIdx].next) return moves[mIdx]; - const move2 = moves[mIdx].next; - delete moves[mIdx]["next"]; - return [moves[mIdx], move2]; - } - - getNotation(move) { - if (move.start.x < 0) - // A second move is always required, but may be empty - return "-"; - const initialSquare = V.CoordsToSquare(move.start); - const finalSquare = V.CoordsToSquare(move.end); - if (move.appear.length == 0) - // Pushed or pulled out of the board - return initialSquare + "R"; - return move.appear[0].p.toUpperCase() + initialSquare + finalSquare; - } - }; -- 2.48.1 From 253e65f6c4f342e5ac8230d7340ed413354f9c7f Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 19 Jan 2024 17:53:42 +0100 Subject: [PATCH 04/16] New variant idea --- TODO | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO b/TODO index 8afe466..f49a584 100644 --- a/TODO +++ b/TODO @@ -4,3 +4,7 @@ Otage, Emergo, Pacosako : fonction "buildPiece(arg1, arg2)" returns HTML element https://fr.wikipedia.org/wiki/Unlur Yoxii ? + +Idée new variant: "bed" random moves and "capture" (opponent?) pieces which cannot move on next turn (sleeping). (Crazybed ?) +one "bed" per player. +one turn = place bed (potentially with opponent piece), then normal move, then random bed move - dropping potential piece on initial square (awaken). -- 2.48.1 From d4be1a764e43f208ef34ee5c7298249b98e0baf1 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Tue, 28 Jan 2025 18:50:55 +0100 Subject: [PATCH 05/16] Work on Dynamo --- variants/Dynamo/class.js | 227 +++++++++++++++++++-------------------- 1 file changed, 113 insertions(+), 114 deletions(-) diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index 05f2aed..ed0ea27 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -3,7 +3,11 @@ import ChessRules from "/base_rules.js"; export default class DynamoRules extends ChessRules { static get Options() { - // TODO + return { + select: C.Options.select, + input: [], + styles: ["cylinder", "doublemove", "progressive"] + }; } get hasEnpassant() { @@ -51,7 +55,7 @@ export default class DynamoRules extends ChessRules { // Step is right, just add (push/pull) moves in this direction // Direction is assumed normalized. - getMovesInDirection([x, y], [dx, dy], nbSteps) { + getMovesInDirection([x, y], [dx, dy], nbSteps, kp) { nbSteps = nbSteps || 8; //max 8 steps anyway let [i, j] = [x + dx, y + dy]; let moves = []; @@ -59,25 +63,27 @@ export default class DynamoRules extends ChessRules { const piece = this.getPiece(x, y); const lastRank = (color == 'w' ? 0 : 7); let counter = 1; - while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { - if (i == lastRank && piece == V.PAWN) { + while (this.onBoard(i, j) && this.board[i][j] == "") { + if (i == lastRank && piece == 'p') { // Promotion by push or pull - V.PawnSpecs.promotions.forEach(p => { + this.pawnPromotions.forEach(p => { let move = super.getBasicMove([x, y], [i, j], { c: color, p: p }); moves.push(move); }); } - else moves.push(super.getBasicMove([x, y], [i, j])); - if (++counter > nbSteps) break; + else + moves.push(super.getBasicMove([x, y], [i, j])); + if (++counter > nbSteps) + break; i += dx; j += dy; } - if (!V.OnBoard(i, j) && piece != V.KING) { + if (!this.onBoard(i, j) && piece != 'k') { // Add special "exit" move, by "taking king" moves.push( new Move({ start: { x: x, y: y }, - end: { x: this.kingPos[color][0], y: this.kingPos[color][1] }, + end: { x: kp[0], y: kp[1] }, appear: [], vanish: [{ x: x, y: y, c: color, p: piece }] }) @@ -109,14 +115,14 @@ export default class DynamoRules extends ChessRules { const checkSlider = () => { const dir = this.getNormalizedDirection([x2 - x1, y2 - y1]); let [i, j] = [x1 + dir[0], y1 + dir[1]]; - while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + while (this.onBoard(i, j) && this.board[i][j] == "") { i += dir[0]; j += dir[1]; } - return !V.OnBoard(i, j); + return !this.onBoard(i, j); }; switch (piece2 || this.getPiece(x1, y1)) { - case V.PAWN: + case 'p': return ( x1 + pawnShift == x2 && ( @@ -124,30 +130,33 @@ export default class DynamoRules extends ChessRules { ( color1 != color2 && deltaY == 1 && - !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + !this.onBoard(2 * x2 - x1, 2 * y2 - y1) ) ) ); - case V.ROOK: - if (x1 != x2 && y1 != y2) return false; + case 'r': + if (x1 != x2 && y1 != y2) + return false; return checkSlider(); - case V.KNIGHT: + case 'n': return ( deltaX + deltaY == 3 && (deltaX == 1 || deltaY == 1) && - !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + !this.onBoard(2 * x2 - x1, 2 * y2 - y1) ); - case V.BISHOP: - if (deltaX != deltaY) return false; + case 'b': + if (deltaX != deltaY) + return false; return checkSlider(); - case V.QUEEN: - if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) return false; + case 'q': + if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) + return false; return checkSlider(); - case V.KING: + case 'k': return ( deltaX <= 1 && deltaY <= 1 && - !V.OnBoard(2 * x2 - x1, 2 * y2 - y1) + !this.onBoard(2 * x2 - x1, 2 * y2 - y1) ); } return false; @@ -158,12 +167,12 @@ export default class DynamoRules extends ChessRules { const deltaX = Math.abs(x1 - x2); const startRank = (this.getColor(x1, y1) == 'w' ? 6 : 1); return ( - [V.QUEEN, V.ROOK].includes(piece) || + ['q', 'r'].includes(piece) || ( - [V.KING, V.PAWN].includes(piece) && + ['k', 'p'].includes(piece) && ( deltaX == 1 || - (deltaX == 2 && piece == V.PAWN && x1 == startRank) + (deltaX == 2 && piece == 'p' && x1 == startRank) ) ) ); @@ -178,12 +187,13 @@ export default class DynamoRules extends ChessRules { const pawnShift = (color == 'w' ? -1 : 1); const pawnStartRank = (color == 'w' ? 6 : 1); const getMoveHash = (m) => { - return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end); + return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end); }; if (this.subTurn == 1) { + const kp = this.searchKingPos(color); const addMoves = (dir, nbSteps) => { const newMoves = - this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps) + this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp) .filter(m => !movesHash[getMoveHash(m)]); newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; }); Array.prototype.push.apply(moves, newMoves); @@ -198,7 +208,8 @@ export default class DynamoRules extends ChessRules { moves = moves.filter(m => { const suicide = (m.appear.length == 0); if (suicide) { - if (hasExit) return false; + if (hasExit) + return false; hasExit = true; } return true; @@ -207,32 +218,34 @@ export default class DynamoRules extends ChessRules { let movesHash = {}; moves.forEach(m => { movesHash[getMoveHash(m)] = true; }); // [x, y] is pushed by 'color' - for (let step of V.steps[V.KNIGHT]) { + for (let step of this.pieces()['n'].both[0].steps) { const [i, j] = [x + step[0], y + step[1]]; if ( - V.OnBoard(i, j) && - this.board[i][j] != V.EMPTY && + this.onBoard(i, j) && + this.board[i][j] != "" && this.getColor(i, j) == color && - this.getPiece(i, j) == V.KNIGHT + this.getPiece(i, j) == 'n' ) { addMoves(step, 1); } } - for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + for (let step of this.pieces()['r'].both[0].steps.concat( + this.pieces()['b'].both[0].steps)) + { let [i, j] = [x + step[0], y + step[1]]; - while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + while (this.onBoard(i, j) && this.board[i][j] == "") { i += step[0]; j += step[1]; } if ( - V.OnBoard(i, j) && - this.board[i][j] != V.EMPTY && + this.onBoard(i, j) && + this.board[i][j] != "" && this.getColor(i, j) == color ) { const deltaX = Math.abs(i - x); const deltaY = Math.abs(j - y); switch (this.getPiece(i, j)) { - case V.PAWN: + case 'p': if ( (x - i) / deltaX == pawnShift && deltaX <= 2 && @@ -248,18 +261,21 @@ export default class DynamoRules extends ChessRules { addMoves(step, 1); } break; - case V.ROOK: - if (deltaX == 0 || deltaY == 0) addMoves(step); + case 'r': + if (deltaX == 0 || deltaY == 0) + addMoves(step); break; - case V.BISHOP: - if (deltaX == deltaY) addMoves(step); + case 'b': + if (deltaX == deltaY) + addMoves(step); break; - case V.QUEEN: + case 'q': // All steps are valid for a queen: addMoves(step); break; - case V.KING: - if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1); + case 'k': + if (deltaX <= 1 && deltaY <= 1) + addMoves(step, 1); break; } } @@ -357,13 +373,16 @@ export default class DynamoRules extends ChessRules { } break; case V.BISHOP: - if (deltaX != deltaY) return []; + if (deltaX != deltaY) + return []; break; case V.ROOK: - if (deltaX != 0 && deltaY != 0) return []; + if (deltaX != 0 && deltaY != 0) + return []; break; case V.QUEEN: - if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return []; + if (deltaX != deltaY && deltaX != 0 && deltaY != 0) + return []; break; } // Nothing should stand between [x, y] and the square fm.start @@ -437,12 +456,14 @@ export default class DynamoRules extends ChessRules { }; if (fm.vanish[0].c != color) { // Only possible action is a push: - if (fm.appear.length == 0) return getPushExit(); + if (fm.appear.length == 0) + return getPushExit(); return getPushMoves(); } else if (sqCol != color) { // Only possible action is a pull, considering moving piece abilities - if (fm.appear.length == 0) return getPullExit(); + if (fm.appear.length == 0) + return getPullExit(); return getPullMoves(); } else { @@ -474,7 +495,8 @@ export default class DynamoRules extends ChessRules { let j = y + step[1]; while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { moves.push(this.getBasicMove([x, y], [i, j])); - if (oneStep) continue outerLoop; + if (oneStep) + continue outerLoop; i += step[0]; j += step[1]; } @@ -516,7 +538,8 @@ export default class DynamoRules extends ChessRules { elt.p == av.p ); }); - if (!avInAv2) return false; + if (!avInAv2) + return false; } return true; }; @@ -540,6 +563,7 @@ export default class DynamoRules extends ChessRules { } } + // TODO :: filterValid(moves) { const color = this.turn; const La = this.amoves.length; @@ -572,7 +596,8 @@ export default class DynamoRules extends ChessRules { return !res; }); } - if (La == 0) return super.filterValid(moves); + if (La == 0) + return super.filterValid(moves); const Lf = this.firstMove.length; return ( super.filterValid( @@ -598,7 +623,8 @@ export default class DynamoRules extends ChessRules { // A click to promote a piece on subTurn 2 would trigger this. // For now it would then return [NaN, NaN] because surrounding squares // have no IDs in the promotion modal. TODO: improve this? - if (isNaN(square[0])) return null; + if (isNaN(square[0])) + return null; // If subTurn == 2 && square is empty && !underCheck && !isOpposite, // then return an empty move, allowing to "pass" subTurn2 const La = this.amoves.length; @@ -614,87 +640,60 @@ export default class DynamoRules extends ChessRules { return null; } - play(move) { - if (this.subTurn == 1 && move.vanish.length == 0) { - // Patch to work with old format: (TODO: remove later) - move.ignore = true; - return; + updateCastleFlags(move) { + if (move.start.x < 0) + return; //empty move (pass subTurn 2) + const firstRank = { 'w': V.size.x - 1, 'b': 0 }; + for (let v of move.vanish) { + if (v.p == 'k') + this.castleFlags[v.c] = [this.size.y, this.size.y]; + else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) { + const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1); + this.castleFlags[v.c][flagIdx] = this.size.y; + } } + } + + play(move) { +// if (this.subTurn == 1 && move.vanish.length == 0) { +// // Patch to work with old format: (TODO: remove later) +// move.ignore = true; +// return; +// } + + // In preplay ? + this.updateCastleFlags(move); + + const oppCol = C.GetOppTurn(color); + const color = this.turn; - move.subTurn = this.subTurn; //for undo const gotoNext = (mv) => { const L = this.firstMove.length; this.amoves.push(this.getAmove(this.firstMove[L-1], mv)); - this.turn = V.GetOppCol(color); + this.turn = oppCol; this.subTurn = 1; this.movesCount++; }; - move.flags = JSON.stringify(this.aggregateFlags()); - V.PlayOnBoard(this.board, move); - if (this.subTurn == 2) gotoNext(move); + this.playOnBoard(move); + if (this.subTurn == 2) + gotoNext(move); else { this.subTurn = 2; this.firstMove.push(move); - this.toNewKingPos(move); if ( - // Condition is true on empty arrays: + // Condition is true on empty arrays: //TODO: getAllPotentialMoves doesn't exist this.getAllPotentialMoves().every(m => { - V.PlayOnBoard(this.board, m); - this.toNewKingPos(m); - const res = this.underCheck(color); - V.UndoOnBoard(this.board, m); - this.toOldKingPos(m); + this.playOnBoard(m); + const res = this.underCheck([kp], oppCol); //TODO: find kp first + this.undoOnBoard(m); return res; }) ) { // No valid move at subTurn 2 gotoNext(this.getEmptyMove()); } - this.toOldKingPos(move); } this.postPlay(move); } - toNewKingPos(move) { - for (let a of move.appear) - if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y]; - } - - postPlay(move) { - if (move.start.x < 0) return; - this.toNewKingPos(move); - this.updateCastleFlags(move); - } - - updateCastleFlags(move) { - const firstRank = { 'w': V.size.x - 1, 'b': 0 }; - for (let v of move.vanish) { - if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y]; - else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) { - const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1); - this.castleFlags[v.c][flagIdx] = V.size.y; - } - } - } - - undo(move) { - if (!!move.ignore) return; //TODO: remove that later - this.disaggregateFlags(JSON.parse(move.flags)); - V.UndoOnBoard(this.board, move); - if (this.subTurn == 1) { - this.amoves.pop(); - this.turn = V.GetOppCol(this.turn); - this.movesCount--; - } - if (move.subTurn == 1) this.firstMove.pop(); - this.subTurn = move.subTurn; - this.toOldKingPos(move); - } - - toOldKingPos(move) { - // (Potentially) Reset king position - for (let v of move.vanish) - if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y]; - } - }; -- 2.48.1 From ab15cf71bdd9be67bfa0df1d7137ff2a6de7139f Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 30 Jan 2025 12:00:00 +0100 Subject: [PATCH 06/16] Some further transformation on Dynamo, prepare next variants too --- variants/Dynamo/class.js | 41 +- variants/Eightpieces/class.js | 1136 ++++++++++++++++++++++ variants/Eightpieces/complete_rules.html | 48 + variants/Eightpieces/rules.html | 3 + variants/Eightpieces/style.css | 1 + variants/Emergo/class.js | 576 +++++++++++ variants/Empire/class.js | 432 ++++++++ variants/Enpassant/class.js | 209 ++++ variants/Evolution/class.js | 34 + variants/Extinction/class.js | 118 +++ variants/Fanorona/class.js | 342 +++++++ variants/Sleepy/class.js | 110 +++ variants/Sleepy/rules.html | 7 + variants/Sleepy/style.css | 1 + 14 files changed, 3038 insertions(+), 20 deletions(-) create mode 100644 variants/Eightpieces/class.js create mode 100644 variants/Eightpieces/complete_rules.html create mode 100644 variants/Eightpieces/rules.html create mode 100644 variants/Eightpieces/style.css create mode 100644 variants/Emergo/class.js create mode 100644 variants/Empire/class.js create mode 100644 variants/Enpassant/class.js create mode 100644 variants/Evolution/class.js create mode 100644 variants/Extinction/class.js create mode 100644 variants/Fanorona/class.js create mode 100644 variants/Sleepy/class.js create mode 100644 variants/Sleepy/rules.html create mode 100644 variants/Sleepy/style.css diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index ed0ea27..7b835e0 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -307,7 +307,7 @@ export default class DynamoRules extends ChessRules { const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); const nbSteps = - [V.PAWN, V.KING, V.KNIGHT].includes(piece) + ['p', 'k', 'n'].includes(piece) ? 1 : null; return this.getMovesInDirection([x, y], dir, nbSteps); @@ -327,7 +327,7 @@ export default class DynamoRules extends ChessRules { const deltaX = Math.abs(fm.start.x - x); const deltaY = Math.abs(fm.start.y - y); switch (piece) { - case V.PAWN: + case 'p': if (x == pawnStartRank) { if ( (fm.start.x - x) * pawnShift < 0 || @@ -354,7 +354,7 @@ export default class DynamoRules extends ChessRules { } } break; - case V.KNIGHT: + case 'n': if ( (deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) || (fm.end.x - fm.start.x != fm.start.x - x) || @@ -363,7 +363,7 @@ export default class DynamoRules extends ChessRules { return []; } break; - case V.KING: + case 'k': if ( (deltaX >= 2 || deltaY >= 2) || (fm.end.x - fm.start.x != fm.start.x - x) || @@ -372,15 +372,15 @@ export default class DynamoRules extends ChessRules { return []; } break; - case V.BISHOP: + case 'b': if (deltaX != deltaY) return []; break; - case V.ROOK: + case 'r': if (deltaX != 0 && deltaY != 0) return []; break; - case V.QUEEN: + case 'q': if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return []; break; @@ -404,7 +404,7 @@ export default class DynamoRules extends ChessRules { // Note: kings cannot suicide, so fm.vanish[0].p is not KING. // Could be PAWN though, if a pawn was pushed out of board. if ( - fm.vanish[0].p != V.PAWN && //pawns cannot pull + fm.vanish[0].p != 'p' && //pawns cannot pull this.isAprioriValidExit( [x, y], [fm.start.x, fm.start.y], @@ -415,13 +415,13 @@ export default class DynamoRules extends ChessRules { // Seems so: const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); - const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null); + const nbSteps = (fm.vanish[0].p == 'n' ? 1 : null); return this.getMovesInDirection([x, y], dir, nbSteps); } return []; }; const getPullMoves = () => { - if (fm.vanish[0].p == V.PAWN) + if (fm.vanish[0].p == 'p') // pawns cannot pull return []; const dirM = this.getNormalizedDirection( @@ -434,8 +434,8 @@ export default class DynamoRules extends ChessRules { const deltaX = Math.abs(x - fm.start.x); const deltaY = Math.abs(y - fm.start.y); if ( - (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) || - (fm.vanish[0].p == V.KNIGHT && + (fm.vanish[0].p == 'k' && (deltaX > 1 || deltaY > 1)) || + (fm.vanish[0].p == 'n' && (deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0)) ) { return []; @@ -444,7 +444,7 @@ export default class DynamoRules extends ChessRules { let [i, j] = [x + dir[0], y + dir[1]]; while ( (i != fm.start.x || j != fm.start.y) && - this.board[i][j] == V.EMPTY + this.board[i][j] == "" ) { i += dir[0]; j += dir[1]; @@ -493,20 +493,20 @@ export default class DynamoRules extends ChessRules { outerLoop: for (let step of steps) { let i = x + step[0]; let j = y + step[1]; - while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + while (this.onBoard(i, j) && this.board[i][j] == "") { moves.push(this.getBasicMove([x, y], [i, j])); if (oneStep) continue outerLoop; i += step[0]; j += step[1]; } - if (V.OnBoard(i, j)) { + if (this.onBoard(i, j)) { if (this.canTake([x, y], [i, j])) moves.push(this.getBasicMove([x, y], [i, j])); } else { // Add potential board exit (suicide), except for the king - if (piece != V.KING) { + if (piece != 'k') { moves.push({ start: { x: x, y: y}, end: { x: this.kingPos[c][0], y: this.kingPos[c][1] }, @@ -563,7 +563,8 @@ export default class DynamoRules extends ChessRules { } } - // TODO :: +// TODO: re-write just for here getAllPotentialMoves() ? + filterValid(moves) { const color = this.turn; const La = this.amoves.length; @@ -572,7 +573,7 @@ export default class DynamoRules extends ChessRules { // A move is valid either if it doesn't result in a check, // or if a second move is possible to counter the check // (not undoing a potential move + action of the opponent) - this.play(m); + this.playOnBoard(m); let res = this.underCheck(color); if (this.subTurn == 2) { let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m); @@ -580,7 +581,7 @@ export default class DynamoRules extends ChessRules { const moves2 = this.getAllPotentialMoves(); for (let m2 of moves2) { this.play(m2); - const res2 = this.underCheck(color); + const res2 = this.underCheck(color); //TODO: + square const amove = this.getAmove(m, m2); isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], amove); @@ -592,7 +593,7 @@ export default class DynamoRules extends ChessRules { } } } - this.undo(m); + this.undoOnBoard(m); return !res; }); } diff --git a/variants/Eightpieces/class.js b/variants/Eightpieces/class.js new file mode 100644 index 0000000..3808a16 --- /dev/null +++ b/variants/Eightpieces/class.js @@ -0,0 +1,1136 @@ +import { randInt, sample } from "@/utils/alea"; +import { ChessRules, PiPo, Move } from "@/base_rules"; + +export class EightpiecesRules extends ChessRules { + + static get JAILER() { + return "j"; + } + static get SENTRY() { + return "s"; + } + static get LANCER() { + return "l"; + } + + static get IMAGE_EXTENSION() { + // Temporarily, for the time SVG pieces are being designed: + return ".png"; + } + + // Lancer directions *from white perspective* + static get LANCER_DIRS() { + return { + 'c': [-1, 0], //north + 'd': [-1, 1], //N-E + 'e': [0, 1], //east + 'f': [1, 1], //S-E + 'g': [1, 0], //south + 'h': [1, -1], //S-W + 'm': [0, -1], //west + 'o': [-1, -1] //N-W + }; + } + + static get PIECES() { + return ChessRules.PIECES + .concat([V.JAILER, V.SENTRY]) + .concat(Object.keys(V.LANCER_DIRS)); + } + + getPiece(i, j) { + const piece = this.board[i][j].charAt(1); + // Special lancer case: 8 possible orientations + if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER; + return piece; + } + + getPpath(b, color, score, orientation) { + if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b; + if (Object.keys(V.LANCER_DIRS).includes(b[1])) { + if (orientation == 'w') return "Eightpieces/tmp_png/" + b; + // Find opposite direction for adequate display: + let oppDir = ''; + switch (b[1]) { + case 'c': + oppDir = 'g'; + break; + case 'g': + oppDir = 'c'; + break; + case 'd': + oppDir = 'h'; + break; + case 'h': + oppDir = 'd'; + break; + case 'e': + oppDir = 'm'; + break; + case 'm': + oppDir = 'e'; + break; + case 'f': + oppDir = 'o'; + break; + case 'o': + oppDir = 'f'; + break; + } + return "Eightpieces/tmp_png/" + b[0] + oppDir; + } + // TODO: after we have SVG pieces, remove the folder and next prefix: + return "Eightpieces/tmp_png/" + b; + } + + getPPpath(m, orientation) { + return ( + this.getPpath( + m.appear[0].c + m.appear[0].p, + null, + null, + orientation + ) + ); + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { sentrypush: fenParts[5] } + ); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check sentry push (if any) + if ( + fenParsed.sentrypush != "-" && + !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/) + ) { + return false; + } + return true; + } + + getFen() { + return super.getFen() + " " + this.getSentrypushFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getSentrypushFen(); + } + + getSentrypushFen() { + const L = this.sentryPush.length; + if (!this.sentryPush[L-1]) return "-"; + let res = ""; + const spL = this.sentryPush[L-1].length; + // Condensate path: just need initial and final squares: + return [0, spL - 1] + .map(i => V.CoordsToSquare(this.sentryPush[L-1][i])) + .join(""); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + // subTurn == 2 only when a sentry moved, and is about to push something + this.subTurn = 1; + // Sentry position just after a "capture" (subTurn from 1 to 2) + this.sentryPos = null; + // Stack pieces' forbidden squares after a sentry move at each turn + const parsedFen = V.ParseFen(fen); + if (parsedFen.sentrypush == "-") this.sentryPush = [null]; + else { + // Expand init + dest squares into a full path: + const init = V.SquareToCoords(parsedFen.sentrypush.substr(0, 2)), + dest = V.SquareToCoords(parsedFen.sentrypush.substr(2)); + let newPath = [init]; + const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i])); + // Check that it's not a knight movement: + if (delta[0] == 0 || delta[1] == 0 || delta[0] == delta[1]) { + const step = ['x', 'y'].map((i, idx) => { + return (dest[i] - init[i]) / delta[idx] || 0 + }); + let x = init.x + step[0], + y = init.y + step[1]; + while (x != dest.x || y != dest.y) { + newPath.push({ x: x, y: y }); + x += step[0]; + y += step[1]; + } + } + newPath.push(dest); + this.sentryPush = [newPath]; + } + } + + static GenRandInitFen(options) { + if (options.randomness == 0) + return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -"; + + const baseFen = ChessRules.GenRandInitFen(options); + const fenParts = baseFen.split(' '); + const posParts = fenParts[0].split('/'); + + // Replace one bishop by sentry, so that sentries on different colors + // Also replace one random rook by jailer, + // and one random knight by lancer (facing north/south) + let pieceLine = { b: posParts[0], w: posParts[7].toLowerCase() }; + let posBlack = { r: -1, n: -1, b: -1 }; + const mapP = { r: 'j', n: 'l', b: 's' }; + ['b', 'w'].forEach(c => { + ['r', 'n', 'b'].forEach(p => { + let pl = pieceLine[c]; + let pos = -1; + if (options.randomness == 2 || c == 'b') + pos = (randInt(2) == 0 ? pl.indexOf(p) : pl.lastIndexOf(p)); + else pos = posBlack[p]; + pieceLine[c] = + pieceLine[c].substr(0, pos) + mapP[p] + pieceLine[c].substr(pos+1); + if (options.randomness == 1 && c == 'b') posBlack[p] = pos; + }); + }); + // Rename 'l' into 'g' (black) or 'c' (white) + pieceLine['w'] = pieceLine['w'].replace('l', 'c'); + pieceLine['b'] = pieceLine['b'].replace('l', 'g'); + if (options.randomness == 2) { + const ws = pieceLine['w'].indexOf('s'); + const bs = pieceLine['b'].indexOf('s'); + if (ws % 2 != bs % 2) { + // Fix sentry: should be on different colors. + // => move sentry on other bishop for random color + const c = sample(['w', 'b'], 1); + pieceLine[c] = pieceLine[c] + .replace('b', 't') //tmp + .replace('s', 'b') + .replace('t', 's'); + } + } + + return ( + pieceLine['b'] + "/" + + posParts.slice(1, 7).join('/') + "/" + + pieceLine['w'].toUpperCase() + " " + + fenParts.slice(1, 5).join(' ') + " -" + ); + } + + canTake([x1, y1], [x2, y2]) { + if (this.subTurn == 2) + // Only self captures on this subturn: + return this.getColor(x1, y1) == this.getColor(x2, y2); + return super.canTake([x1, y1], [x2, y2]); + } + + // Is piece on square (x,y) immobilized? + isImmobilized([x, y]) { + const color = this.getColor(x, y); + const oppCol = V.GetOppCol(color); + for (let step of V.steps[V.ROOK]) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + if (this.getPiece(i, j) == V.JAILER) return [i, j]; + } + } + return null; + } + + canIplay(side, [x, y]) { + return ( + (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) + || + (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y) + ); + } + + getPotentialMovesFrom([x, y]) { + const piece = this.getPiece(x, y); + const L = this.sentryPush.length; + // At subTurn == 2, jailers aren't effective (Jeff K) + if (this.subTurn == 1) { + const jsq = this.isImmobilized([x, y]); + if (!!jsq) { + let moves = []; + // Special pass move if king: + if (piece == V.KING) { + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: jsq[0], y: jsq[1] } + }) + ); + } + else if (piece == V.LANCER && !!this.sentryPush[L-1]) { + // A pushed lancer next to the jailer: reorient + const color = this.getColor(x, y); + const curDir = this.board[x][y].charAt(1); + Object.keys(V.LANCER_DIRS).forEach(k => { + moves.push( + new Move({ + appear: [{ x: x, y: y, c: color, p: k }], + vanish: [{ x: x, y: y, c: color, p: curDir }], + start: { x: x, y: y }, + end: { x: jsq[0], y: jsq[1] } + }) + ); + }); + } + return moves; + } + } + let moves = []; + switch (piece) { + case V.JAILER: + moves = this.getPotentialJailerMoves([x, y]); + break; + case V.SENTRY: + moves = this.getPotentialSentryMoves([x, y]); + break; + case V.LANCER: + moves = this.getPotentialLancerMoves([x, y]); + break; + default: + moves = super.getPotentialMovesFrom([x, y]); + break; + } + if (!!this.sentryPush[L-1]) { + // Delete moves walking back on sentry push path, + // only if not a pawn, and the piece is the pushed one. + const pl = this.sentryPush[L-1].length; + const finalPushedSq = this.sentryPush[L-1][pl-1]; + moves = moves.filter(m => { + if ( + m.vanish[0].p != V.PAWN && + m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y && + this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y) + ) { + return false; + } + return true; + }); + } + else if (this.subTurn == 2) { + // Put back the sentinel on board: + const color = this.turn; + moves.forEach(m => { + m.appear.push({x: x, y: y, p: V.SENTRY, c: color}); + }); + } + return moves; + } + + getPotentialPawnMoves([x, y]) { + const color = this.getColor(x, y); + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + let shiftX = (color == "w" ? -1 : 1); + if (this.subTurn == 2) shiftX *= -1; + const firstRank = color == "w" ? sizeX - 1 : 0; + const startRank = color == "w" ? sizeX - 2 : 1; + const lastRank = color == "w" ? 0 : sizeX - 1; + + // Pawns might be pushed on 1st rank and attempt to move again: + if (!V.OnBoard(x + shiftX, y)) return []; + + // A push cannot put a pawn on last rank (it goes backward) + let finalPieces = [V.PAWN]; + if (x + shiftX == lastRank) { + // Only allow direction facing inside board: + const allowedLancerDirs = + lastRank == 0 + ? ['e', 'f', 'g', 'h', 'm'] + : ['c', 'd', 'e', 'm', 'o']; + finalPieces = + allowedLancerDirs + .concat([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]); + } + if (this.board[x + shiftX][y] == V.EMPTY) { + // One square forward + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y], { + c: color, + p: piece + }) + ); + } + if ( + // 2-squares jumps forbidden if pawn push + this.subTurn == 1 && + [startRank, firstRank].includes(x) && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + // Two squares jump + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); + } + } + // Captures + for (let shiftY of [-1, 1]) { + if ( + y + shiftY >= 0 && + y + shiftY < sizeY && + this.board[x + shiftX][y + shiftY] != V.EMPTY && + this.canTake([x, y], [x + shiftX, y + shiftY]) + ) { + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y + shiftY], { + c: color, + p: piece + }) + ); + } + } + } + + // En passant: only on subTurn == 1 + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; + if ( + this.subTurn == 1 && + !!epSquare && + epSquare.x == x + shiftX && + Math.abs(epSquare.y - y) == 1 + ) { + let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]); + enpassantMove.vanish.push({ + x: x, + y: epSquare.y, + p: "p", + c: this.getColor(x, epSquare.y) + }); + moves.push(enpassantMove); + } + + return moves; + } + + doClick(square) { + if (isNaN(square[0])) return null; + const L = this.sentryPush.length; + const [x, y] = [square[0], square[1]]; + const color = this.turn; + if ( + this.subTurn == 2 || + this.board[x][y] == V.EMPTY || + this.getPiece(x, y) != V.LANCER || + this.getColor(x, y) != color || + !!this.sentryPush[L-1] + ) { + return null; + } + // Stuck lancer? + const orientation = this.board[x][y][1]; + const step = V.LANCER_DIRS[orientation]; + if (!V.OnBoard(x + step[0], y + step[1])) { + let choices = []; + Object.keys(V.LANCER_DIRS).forEach(k => { + const dir = V.LANCER_DIRS[k]; + if ( + (dir[0] != step[0] || dir[1] != step[1]) && + V.OnBoard(x + dir[0], y + dir[1]) + ) { + choices.push( + new Move({ + vanish: [ + new PiPo({ + x: x, + y: y, + c: color, + p: orientation + }) + ], + appear: [ + new PiPo({ + x: x, + y: y, + c: color, + p: k + }) + ], + start: { x: x, y : y }, + end: { x: -1, y: -1 } + }) + ); + } + }); + return choices; + } + return null; + } + + // Obtain all lancer moves in "step" direction + getPotentialLancerMoves_aux([x, y], step, tr) { + let moves = []; + // Add all moves to vacant squares until opponent is met: + const color = this.getColor(x, y); + const oppCol = + this.subTurn == 1 + ? V.GetOppCol(color) + // at subTurn == 2, consider own pieces as opponent + : color; + let sq = [x + step[0], y + step[1]]; + while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) { + if (this.board[sq[0]][sq[1]] == V.EMPTY) + moves.push(this.getBasicMove([x, y], sq, tr)); + sq[0] += step[0]; + sq[1] += step[1]; + } + if (V.OnBoard(sq[0], sq[1])) + // Add capturing move + moves.push(this.getBasicMove([x, y], sq, tr)); + return moves; + } + + getPotentialLancerMoves([x, y]) { + let moves = []; + // Add all lancer possible orientations, similar to pawn promotions. + // Except if just after a push: allow all movements from init square then + const L = this.sentryPush.length; + const color = this.getColor(x, y); + const dirCode = this.board[x][y][1]; + const curDir = V.LANCER_DIRS[dirCode]; + if (!!this.sentryPush[L-1]) { + // Maybe I was pushed + const pl = this.sentryPush[L-1].length; + if ( + this.sentryPush[L-1][pl-1].x == x && + this.sentryPush[L-1][pl-1].y == y + ) { + // I was pushed: allow all directions (for this move only), but + // do not change direction after moving, *except* if I keep the + // same orientation in which I was pushed. + // Also allow simple reorientation ("capturing king"): + if (!V.OnBoard(x + curDir[0], y + curDir[1])) { + const kp = this.kingPos[color]; + let reorientMoves = []; + Object.keys(V.LANCER_DIRS).forEach(k => { + const dir = V.LANCER_DIRS[k]; + if ( + (dir[0] != curDir[0] || dir[1] != curDir[1]) && + V.OnBoard(x + dir[0], y + dir[1]) + ) { + reorientMoves.push( + new Move({ + vanish: [ + new PiPo({ + x: x, + y: y, + c: color, + p: dirCode + }) + ], + appear: [ + new PiPo({ + x: x, + y: y, + c: color, + p: k + }) + ], + start: { x: x, y : y }, + end: { x: kp[0], y: kp[1] } + }) + ); + } + }); + Array.prototype.push.apply(moves, reorientMoves); + } + Object.values(V.LANCER_DIRS).forEach(step => { + const dirCode = Object.keys(V.LANCER_DIRS).find(k => { + return ( + V.LANCER_DIRS[k][0] == step[0] && + V.LANCER_DIRS[k][1] == step[1] + ); + }); + const dirMoves = + this.getPotentialLancerMoves_aux( + [x, y], + step, + { p: dirCode, c: color } + ); + if (curDir[0] == step[0] && curDir[1] == step[1]) { + // Keeping same orientation: can choose after + let chooseMoves = []; + dirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + const newDir = V.LANCER_DIRS[k]; + // Prevent orientations toward outer board: + if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + chooseMoves.push(mk); + } + }); + }); + Array.prototype.push.apply(moves, chooseMoves); + } + else Array.prototype.push.apply(moves, dirMoves); + }); + return moves; + } + } + // I wasn't pushed: standard lancer move + const monodirMoves = + this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]); + // Add all possible orientations aftermove except if I'm being pushed + if (this.subTurn == 1) { + monodirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + const newDir = V.LANCER_DIRS[k]; + // Prevent orientations toward outer board: + if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + moves.push(mk); + } + }); + }); + return moves; + } + else { + // I'm pushed: add potential nudges, except for current orientation + let potentialNudges = []; + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + if ( + (step[0] != curDir[0] || step[1] != curDir[1]) && + V.OnBoard(x + step[0], y + step[1]) && + this.board[x + step[0]][y + step[1]] == V.EMPTY + ) { + const newDirCode = Object.keys(V.LANCER_DIRS).find(k => { + const codeStep = V.LANCER_DIRS[k]; + return (codeStep[0] == step[0] && codeStep[1] == step[1]); + }); + potentialNudges.push( + this.getBasicMove( + [x, y], + [x + step[0], y + step[1]], + { c: color, p: newDirCode } + ) + ); + } + } + return monodirMoves.concat(potentialNudges); + } + } + + getPotentialSentryMoves([x, y]) { + // The sentry moves a priori like a bishop: + let moves = super.getPotentialBishopMoves([x, y]); + // ...but captures are replaced by special move, if and only if + // "captured" piece can move now, considered as the capturer unit. + // --> except is subTurn == 2, in this case I don't push anything. + if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1); + moves.forEach(m => { + if (m.vanish.length == 2) { + // Temporarily cancel the sentry capture: + m.appear.pop(); + m.vanish.pop(); + } + }); + const color = this.getColor(x, y); + const fMoves = moves.filter(m => { + // Can the pushed unit make any move? ...resulting in a non-self-check? + if (m.appear.length == 0) { + let res = false; + this.play(m); + let moves2 = this.getPotentialMovesFrom([m.end.x, m.end.y]); + for (let m2 of moves2) { + this.play(m2); + res = !this.underCheck(color); + this.undo(m2); + if (res) break; + } + this.undo(m); + return res; + } + return true; + }); + return fMoves; + } + + getPotentialJailerMoves([x, y]) { + return super.getPotentialRookMoves([x, y]).filter(m => { + // Remove jailer captures + return m.vanish[0].p != V.JAILER || m.vanish.length == 1; + }); + } + + getPotentialKingMoves(sq) { + const moves = this.getSlideNJumpMoves( + sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); + return ( + this.subTurn == 1 + ? moves.concat(this.getCastleMoves(sq)) + : moves + ); + } + + atLeastOneMove() { + // If in second-half of a move, we already know that a move is possible + if (this.subTurn == 2) return true; + return super.atLeastOneMove(); + } + + filterValid(moves) { + if (moves.length == 0) return []; + const basicFilter = (m, c) => { + this.play(m); + const res = !this.underCheck(c); + this.undo(m); + return res; + }; + // Disable check tests for sentry pushes, + // because in this case the move isn't finished + let movesWithoutSentryPushes = []; + let movesWithSentryPushes = []; + moves.forEach(m => { + // Second condition below for special king "pass" moves + if (m.appear.length > 0 || m.vanish.length == 0) + movesWithoutSentryPushes.push(m); + else movesWithSentryPushes.push(m); + }); + const color = this.turn; + const oppCol = V.GetOppCol(color); + const filteredMoves = + movesWithoutSentryPushes.filter(m => basicFilter(m, color)); + // If at least one full move made, everything is allowed. + // Else: forbid checks and captures. + return ( + this.movesCount >= 2 + ? filteredMoves + : filteredMoves.filter(m => { + return (m.vanish.length <= 1 && basicFilter(m, oppCol)); + }) + ).concat(movesWithSentryPushes); + } + + getAllValidMoves() { + if (this.subTurn == 1) return super.getAllValidMoves(); + // Sentry push: + const sentrySq = [this.sentryPos.x, this.sentryPos.y]; + return this.filterValid(this.getPotentialMovesFrom(sentrySq)); + } + + isAttacked(sq, color) { + return ( + super.isAttacked(sq, color) || + this.isAttackedByLancer(sq, color) || + this.isAttackedBySentry(sq, color) + // The jailer doesn't capture. + ); + } + + isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) { + for (let step of steps) { + let rx = x + step[0], + ry = y + step[1]; + while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { + rx += step[0]; + ry += step[1]; + } + if ( + V.OnBoard(rx, ry) && + this.getPiece(rx, ry) == piece && + this.getColor(rx, ry) == color && + !this.isImmobilized([rx, ry]) + ) { + return true; + } + } + return false; + } + + isAttackedByPawn([x, y], color) { + const pawnShift = (color == "w" ? 1 : -1); + if (x + pawnShift >= 0 && x + pawnShift < V.size.x) { + for (let i of [-1, 1]) { + if ( + y + i >= 0 && + y + i < V.size.y && + this.getPiece(x + pawnShift, y + i) == V.PAWN && + this.getColor(x + pawnShift, y + i) == color && + !this.isImmobilized([x + pawnShift, y + i]) + ) { + return true; + } + } + } + return false; + } + + isAttackedByLancer([x, y], color) { + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + // If in this direction there are only enemy pieces and empty squares, + // and we meet a lancer: can he reach us? + // NOTE: do not stop at first lancer, there might be several! + let coord = { x: x + step[0], y: y + step[1] }; + let lancerPos = []; + while ( + V.OnBoard(coord.x, coord.y) && + ( + this.board[coord.x][coord.y] == V.EMPTY || + this.getColor(coord.x, coord.y) == color + ) + ) { + if ( + this.getPiece(coord.x, coord.y) == V.LANCER && + !this.isImmobilized([coord.x, coord.y]) + ) { + lancerPos.push({x: coord.x, y: coord.y}); + } + coord.x += step[0]; + coord.y += step[1]; + } + const L = this.sentryPush.length; + const pl = (!!this.sentryPush[L-1] ? this.sentryPush[L-1].length : 0); + for (let xy of lancerPos) { + const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)]; + if ( + (dir[0] == -step[0] && dir[1] == -step[1]) || + // If the lancer was just pushed, this is an attack too: + ( + !!this.sentryPush[L-1] && + this.sentryPush[L-1][pl-1].x == xy.x && + this.sentryPush[L-1][pl-1].y == xy.y + ) + ) { + return true; + } + } + } + return false; + } + + // Helper to check sentries attacks: + selfAttack([x1, y1], [x2, y2]) { + const color = this.getColor(x1, y1); + const oppCol = V.GetOppCol(color); + const sliderAttack = (allowedSteps, lancer) => { + const deltaX = x2 - x1, + deltaY = y2 - y1; + const absDeltaX = Math.abs(deltaX), + absDeltaY = Math.abs(deltaY); + const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ]; + if ( + // Check that the step is a priori valid: + (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) || + allowedSteps.every(st => st[0] != step[0] || st[1] != step[1]) + ) { + return false; + } + let sq = [ x1 + step[0], y1 + step[1] ]; + while (sq[0] != x2 || sq[1] != y2) { + // NOTE: no need to check OnBoard in this special case + if (this.board[sq[0]][sq[1]] != V.EMPTY) { + const p = this.getPiece(sq[0], sq[1]); + const pc = this.getColor(sq[0], sq[1]); + if ( + // Enemy sentry on the way will be gone: + (p != V.SENTRY || pc != oppCol) && + // Lancer temporarily "changed color": + (!lancer || pc == color) + ) { + return false; + } + } + sq[0] += step[0]; + sq[1] += step[1]; + } + return true; + }; + switch (this.getPiece(x1, y1)) { + case V.PAWN: { + // Pushed pawns move as enemy pawns + const shift = (color == 'w' ? 1 : -1); + return (x1 + shift == x2 && Math.abs(y1 - y2) == 1); + } + case V.KNIGHT: { + const deltaX = Math.abs(x1 - x2); + const deltaY = Math.abs(y1 - y2); + return ( + deltaX + deltaY == 3 && + [1, 2].includes(deltaX) && + [1, 2].includes(deltaY) + ); + } + case V.ROOK: + return sliderAttack(V.steps[V.ROOK]); + case V.BISHOP: + return sliderAttack(V.steps[V.BISHOP]); + case V.QUEEN: + return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP])); + case V.LANCER: { + // Special case: as long as no enemy units stands in-between, + // it attacks (if it points toward the king). + const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)]; + return sliderAttack([allowedStep], "lancer"); + } + // No sentries or jailer tests: they cannot self-capture + } + return false; + } + + isAttackedBySentry([x, y], color) { + // Attacked by sentry means it can self-take our king. + // Just check diagonals of enemy sentry(ies), and if it reaches + // one of our pieces: can I self-take? + const myColor = V.GetOppCol(color); + let candidates = []; + for (let i=0; i { + const score = this.getCurrentScore(); + const curEval = move.eval; + if (score != "*") { + move.eval = + score == "1/2" + ? 0 + : (score == "1-0" ? 1 : -1) * maxeval; + } else move.eval = this.evalPosition(); + if ( + // "next" is defined after sentry pushes + !!next && ( + !curEval || + color == 'w' && move.eval > curEval || + color == 'b' && move.eval < curEval + ) + ) { + move.second = next; + } + }; + + // Just search_depth == 1 (because of sentries. TODO: can do better...) + moves1.forEach(m1 => { + this.play(m1); + if (this.subTurn == 1) setEval(m1); + else { + // Need to play every pushes and count: + const moves2 = this.getAllValidMoves(); + moves2.forEach(m2 => { + this.play(m2); + setEval(m1, m2); + this.undo(m2); + }); + } + this.undo(m1); + }); + + moves1.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++) + candidates.push(j); + const choice = moves1[candidates[randInt(candidates.length)]]; + return (!choice.second ? choice : [choice, choice.second]); + } + + // For moves notation: + static get LANCER_DIRNAMES() { + return { + 'c': "N", + 'd': "NE", + 'e': "E", + 'f': "SE", + 'g': "S", + 'h': "SW", + 'm': "W", + 'o': "NW" + }; + } + + getNotation(move) { + // Special case "king takes jailer" is a pass move + if (move.appear.length == 0 && move.vanish.length == 0) return "pass"; + let notation = undefined; + if (this.subTurn == 2) { + // Do not consider appear[1] (sentry) for sentry pushes + const simpleMove = { + appear: [move.appear[0]], + vanish: move.vanish, + start: move.start, + end: move.end + }; + notation = super.getNotation(simpleMove); + } + else if ( + move.appear.length > 0 && + move.vanish[0].x == move.appear[0].x && + move.vanish[0].y == move.appear[0].y + ) { + // Lancer in-place reorientation: + notation = "L" + V.CoordsToSquare(move.start) + ":R"; + } + else notation = super.getNotation(move); + if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p)) + // Lancer: add direction info + notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p]; + else if ( + move.vanish[0].p == V.PAWN && + Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p) + ) { + // Fix promotions in lancer: + notation = notation.slice(0, -1) + + "L:" + V.LANCER_DIRNAMES[move.appear[0].p]; + } + return notation; + } + +}; diff --git a/variants/Eightpieces/complete_rules.html b/variants/Eightpieces/complete_rules.html new file mode 100644 index 0000000..c1d12ab --- /dev/null +++ b/variants/Eightpieces/complete_rules.html @@ -0,0 +1,48 @@ +p.boxed + | Three new pieces appear. All pieces are unique. + +p. + There are only one rook, one bishop and one knight per side in this variant. + That explains the name. The king and queen are still there, + and the three remaining slots are taken by new pieces: + +ul + li. + The lancer 'L' is oriented and can only move in the direction it points, + by any number of squares as long as an enemy isn't met + (it can jump over friendly pieces). If an opponent' piece is found, + it can be captured. After moving you can reorient the lancer. + li. + The sentry 'S' moves like a bishop but doesn't capture directly. + It "pushes" enemy pieces instead, either on an empty square or on other + enemy pieces which are thus (self-)captured. + li. + The jailer 'J' moves like a rook but also doesn't capture. + It immobilizes enemy pieces which are vertically or horizontally adjacent. + +p. + On the following diagram the white sentry can push the black lancer to + capture the black pawn on b4. The lancer is then immobilized + by the white jailer at a4. + +figure.diagram-container + .diagram.diag12 + | fen:7k/8/8/8/Jp3m2/8/3S4/K7: + .diagram.diag22 + | fen:7k/8/8/8/Jm3S2/8/8/K7: + figcaption Left: before white move S"push"f4. Right: after this move. + +p To reorient a stuck lancer, +ul + li Just after being pushed: play a move which 'capture your king". + li Later in the game: click on the lancer. + +h3 Complete rules + +p + | The rules were invented by Jeff Kubach (2020), who described them much + | more precisely on the + a(href="https://www.chessvariants.com/rules/8-piece-chess") + | chessvariants page + | . While the summary given above may suffice to start playing, + | you should read the complete rules to fully understand this variant. diff --git a/variants/Eightpieces/rules.html b/variants/Eightpieces/rules.html new file mode 100644 index 0000000..41e9d42 --- /dev/null +++ b/variants/Eightpieces/rules.html @@ -0,0 +1,3 @@ +Three new pieces appear: the lancer is a rook with a constrained direction, the sentry moves like a bishop and pushes pieces, and the jailer immobilizes pieces orthogonally adjacent. + +The goal is still to checkmate. diff --git a/variants/Eightpieces/style.css b/variants/Eightpieces/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Eightpieces/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css"); diff --git a/variants/Emergo/class.js b/variants/Emergo/class.js new file mode 100644 index 0000000..13069de --- /dev/null +++ b/variants/Emergo/class.js @@ -0,0 +1,576 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; +import { ArrayFun } from "@/utils/array"; + +export class EmergoRules extends ChessRules { + + // Simple encoding: A to L = 1 to 12, from left to right, if white controls. + // Lowercase if black controls. + // Single piece (no prisoners): A@ to L@ (+ lowercase) + + static get Options() { + return null; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get DarkBottomRight() { + return true; + } + + // board element == file name: + static board2fen(b) { + return b; + } + static fen2board(f) { + return f; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + 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++) { + // Add only 0.5 per symbol because 2 per piece + if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + static GetBoard(position) { + const rows = position.split("/"); + let board = ArrayFun.init(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++) { + const character = rows[i][indexInRow]; + const num = parseInt(character, 10); + // If num is a number, just shift j: + if (!isNaN(num)) j += num; + else + // Something at position i,j + board[i][j++] = V.fen2board(character + rows[i][++indexInRow]); + } + } + return board; + } + + getPpath(b) { + return "Emergo/" + b; + } + + getColor(x, y) { + if (x >= V.size.x) return x == V.size.x ? "w" : "b"; + if (this.board[x][y].charCodeAt(0) < 97) return 'w'; + return 'b'; + } + + getPiece() { + return V.PAWN; //unused + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 3) Check reserves + if ( + !fenParsed.reserve || + !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/) + ) { + return false; + } + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[3] } + ); + } + + static get size() { + return { x: 9, y: 9 }; + } + + static GenRandInitFen() { + return "9/9/9/9/9/9/9/9/9 w 0 12,12"; + } + + getFen() { + return super.getFen() + " " + this.getReserveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getReserveFen(); + } + + getReserveFen() { + return ( + (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," + + (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN]) + ); + } + + getReservePpath(index, color) { + return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@'; + } + + static get RESERVE_PIECES() { + return [V.PAWN]; //only array length matters + } + + setOtherVariables(fen) { + const reserve = + V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10)); + this.reserve = { w: null, b: null }; + if (reserve[0] > 0) this.reserve['w'] = { [V.PAWN]: reserve[0] }; + if (reserve[1] > 0) this.reserve['b'] = { [V.PAWN]: reserve[1] }; + // Local stack of captures during a turn (squares + directions) + this.captures = [ [] ]; + } + + atLeastOneCaptureFrom([x, y], color, forbiddenStep) { + for (let s of V.steps[V.BISHOP]) { + if ( + !forbiddenStep || + (s[0] != -forbiddenStep[0] || s[1] != -forbiddenStep[1]) + ) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color && + this.board[i + s[0]][j + s[1]] == V.EMPTY + ) { + return true; + } + } + } + return false; + } + + atLeastOneCapture(color) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + return ( + this.atLeastOneCaptureFrom( + captures[L-1].square, color, captures[L-1].step) + ); + } + for (let i = 0; i < V.size.x; i++) { + for (let j=0; j< V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.atLeastOneCaptureFrom([i, j], color) + ) { + return true; + } + } + } + return false; + } + + maxLengthIndices(caps) { + let maxLength = 0; + let res = []; + for (let i = 0; i < caps.length; i++) { + if (caps[i].length > maxLength) { + res = [i]; + maxLength = caps[i].length; + } + else if (caps[i].length == maxLength) res.push(i); + } + return res; + }; + + getLongestCaptures_aux([x, y], color, locSteps) { + let res = []; + const L = locSteps.length; + const lastStep = (L > 0 ? locSteps[L-1] : null); + for (let s of V.steps[V.BISHOP]) { + if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] == V.EMPTY && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color + ) { + const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]); + locSteps.push(s); + V.PlayOnBoard(this.board, move); + const nextRes = + this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps); + res.push(1 + nextRes); + locSteps.pop(); + V.UndoOnBoard(this.board, move); + } + } + if (res.length == 0) return 0; + return Math.max(...res); + } + + getLongestCapturesFrom([x, y], color, locSteps) { + let res = []; + const L = locSteps.length; + const lastStep = (L > 0 ? locSteps[L-1] : null); + for (let s of V.steps[V.BISHOP]) { + if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] == V.EMPTY && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color + ) { + const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]); + locSteps.push(s); + V.PlayOnBoard(this.board, move); + const stepRes = + this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps); + res.push({ step: s, length: 1 + stepRes }); + locSteps.pop(); + V.UndoOnBoard(this.board, move); + } + } + return this.maxLengthIndices(res).map(i => res[i]);; + } + + getAllLongestCaptures(color) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + let caps = []; + if (L > 0) { + let locSteps = [ captures[L-1].step ]; + let res = + this.getLongestCapturesFrom(captures[L-1].square, color, locSteps); + Array.prototype.push.apply( + caps, + res.map(r => Object.assign({ square: captures[L-1].square }, r)) + ); + } + else { + for (let i = 0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + let locSteps = []; + let res = this.getLongestCapturesFrom([i, j], color, locSteps); + Array.prototype.push.apply( + caps, + res.map(r => Object.assign({ square: [i, j] }, r)) + ); + } + } + } + } + return this.maxLengthIndices(caps).map(i => caps[i]); + } + + getBasicMove([x1, y1], [x2, y2], capt) { + const cp1 = this.board[x1][y1]; + if (!capt) { + return new Move({ + appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ], + vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ] + }); + } + // Compute resulting types based on jumped + jumping pieces + const color = this.getColor(x1, y1); + const firstCodes = (color == 'w' ? [65, 97] : [97, 65]); + const cpCapt = this.board[capt[0]][capt[1]]; + let count1 = [cp1.charCodeAt(0) - firstCodes[0], -1]; + if (cp1[1] != '@') count1[1] = cp1.charCodeAt(1) - firstCodes[0]; + let countC = [cpCapt.charCodeAt(0) - firstCodes[1], -1]; + if (cpCapt[1] != '@') countC[1] = cpCapt.charCodeAt(1) - firstCodes[1]; + count1[1]++; + countC[0]--; + let colorChange = false, + captVanish = false; + if (countC[0] < 0) { + if (countC[1] >= 0) { + colorChange = true; + countC = [countC[1], -1]; + } + else captVanish = true; + } + const incPrisoners = String.fromCharCode(firstCodes[0] + count1[1]); + let mv = new Move({ + appear: [ + new PiPo({ + x: x2, + y: y2, + c: cp1[0], + p: incPrisoners + }) + ], + vanish: [ + new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }), + new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] }) + ] + }); + if (!captVanish) { + mv.appear.push( + new PiPo({ + x: capt[0], + y: capt[1], + c: String.fromCharCode( + firstCodes[(colorChange ? 0 : 1)] + countC[0]), + p: (colorChange ? '@' : cpCapt[1]), + }) + ); + } + return mv; + } + + getReserveMoves(x) { + const color = this.turn; + if (!this.reserve[color] || this.atLeastOneCapture(color)) return []; + let moves = []; + const shadowPiece = + this.reserve[V.GetOppCol(color)] == null + ? this.reserve[color][V.PAWN] - 1 + : 0; + const appearColor = String.fromCharCode( + (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece); + const addMove = ([i, j]) => { + moves.push( + new Move({ + appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ], + vanish: [], + start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 } + }) + ); + }; + const oppCol = V.GetOppCol(color); + const opponentCanCapture = this.atLeastOneCapture(oppCol); + for (let i = 0; i < V.size.x; i++) { + for (let j = i % 2; j < V.size.y; j += 2) { + if ( + this.board[i][j] == V.EMPTY && + // prevent playing on central square at move 1: + (this.movesCount >= 1 || i != 4 || j != 4) + ) { + if (opponentCanCapture) addMove([i, j]); + else { + let canAddMove = true; + for (let s of V.steps[V.BISHOP]) { + if ( + V.OnBoard(i + s[0], j + s[1]) && + V.OnBoard(i - s[0], j - s[1]) && + this.board[i + s[0]][j + s[1]] != V.EMPTY && + this.board[i - s[0]][j - s[1]] == V.EMPTY && + this.getColor(i + s[0], j + s[1]) == oppCol + ) { + canAddMove = false; + break; + } + } + if (canAddMove) addMove([i, j]); + } + } + } + } + return moves; + } + + getPotentialMovesFrom([x, y], longestCaptures) { + if (x >= V.size.x) { + if (longestCaptures.length == 0) return this.getReserveMoves(x); + return []; + } + const color = this.turn; + if (!!this.reserve[color] && !this.atLeastOneCapture(color)) return []; + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + let moves = []; + if (longestCaptures.length > 0) { + if ( + L > 0 && + (x != captures[L-1].square[0] || y != captures[L-1].square[1]) + ) { + return []; + } + longestCaptures.forEach(lc => { + if (lc.square[0] == x && lc.square[1] == y) { + const s = lc.step; + const [i, j] = [x + s[0], y + s[1]]; + moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j])); + } + }); + return moves; + } + // Just search simple moves: + for (let s of V.steps[V.BISHOP]) { + const [i, j] = [x + s[0], y + s[1]]; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) + moves.push(this.getBasicMove([x, y], [i, j])); + } + return moves; + } + + getAllValidMoves() { + const color = this.turn; + const longestCaptures = this.getAllLongestCaptures(color); + let potentialMoves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + Array.prototype.push.apply( + potentialMoves, + this.getPotentialMovesFrom([i, j], longestCaptures) + ); + } + } + } + // Add reserve moves + potentialMoves = potentialMoves.concat( + this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1)) + ); + return potentialMoves; + } + + getPossibleMovesFrom([x, y]) { + const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y)); + return this.getPotentialMovesFrom([x, y], longestCaptures); + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + play(move) { + const color = this.turn; + move.turn = color; //for undo + V.PlayOnBoard(this.board, move); + if (move.vanish.length == 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.push({ + square: [move.end.x, move.end.y], + step: [(move.end.x - move.start.x)/2, (move.end.y - move.start.y)/2] + }); + if (this.atLeastOneCapture(color)) + // There could be other captures (mandatory) + move.notTheEnd = true; + } + else if (move.vanish == 0) { + const firstCode = (color == 'w' ? 65 : 97); + // Generally, reserveCount == 1 (except for shadow piece) + const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1; + this.reserve[color][V.PAWN] -= reserveCount; + if (this.reserve[color][V.PAWN] == 0) this.reserve[color] = null; + } + if (!move.notTheEnd) { + this.turn = V.GetOppCol(color); + this.movesCount++; + this.captures.push([]); + } + } + + undo(move) { + V.UndoOnBoard(this.board, move); + if (!move.notTheEnd) { + this.turn = move.turn; + this.movesCount--; + this.captures.pop(); + } + if (move.vanish.length == 0) { + const color = (move.appear[0].c == 'A' ? 'w' : 'b'); + const firstCode = (color == 'w' ? 65 : 97); + const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1; + if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 }; + this.reserve[color][V.PAWN] += reserveCount; + } + else if (move.vanish.length == 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.pop(); + } + } + + atLeastOneMove() { + const color = this.turn; + if (this.atLeastOneCapture(color)) return true; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + const moves = this.getPotentialMovesFrom([i, j], []); + if (moves.length > 0) return true; + } + } + } + const reserveMoves = + this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1)); + return (reserveMoves.length > 0); + } + + getCurrentScore() { + const color = this.turn; + // If no pieces on board + reserve, I lose + if (!!this.reserve[color]) return "*"; + let atLeastOnePiece = false; + outerLoop: for (let i=0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + atLeastOnePiece = true; + break outerLoop; + } + } + } + if (!atLeastOnePiece) return (color == 'w' ? "0-1" : "1-0"); + if (!this.atLeastOneMove()) return "1/2"; + return "*"; + } + + getComputerMove() { + // Random mover for now (TODO) + const color = this.turn; + let mvArray = []; + let mv = null; + while (this.turn == color) { + const moves = this.getAllValidMoves(); + mv = moves[randInt(moves.length)]; + mvArray.push(mv); + this.play(mv); + } + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + + getNotation(move) { + if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end); + const L0 = this.captures.length; + if (this.captures[L0 - 1].length > 0) return V.CoordsToSquare(move.end); + return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end); + } + +}; diff --git a/variants/Empire/class.js b/variants/Empire/class.js new file mode 100644 index 0000000..d04e4a8 --- /dev/null +++ b/variants/Empire/class.js @@ -0,0 +1,432 @@ +import { ChessRules } from "@/base_rules"; + +export class EmpireRules extends ChessRules { + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: [V.QUEEN] } + ); + } + + static get LoseOnRepetition() { + return true; + } + + static IsGoodFlags(flags) { + // Only black can castle + return !!flags.match(/^[a-z]{2,2}$/); + } + + getPpath(b) { + return (b[0] == 'w' ? "Empire/" : "") + b; + } + + static GenRandInitFen(options) { + if (options.randomness == 0) + return "rnbqkbnr/pppppppp/8/8/8/PPPSSPPP/8/TECDKCET w 0 ah -"; + + // Mapping kingdom --> empire: + const piecesMap = { + 'R': 'T', + 'N': 'E', + 'B': 'C', + 'Q': 'D', + 'K': 'K' + }; + + const baseFen = ChessRules.GenRandInitFen(options); + return ( + baseFen.substr(0, 24) + "PPPSSPPP/8/" + + baseFen.substr(35, 8).split('').map(p => piecesMap[p]).join('') + + baseFen.substr(43, 5) + baseFen.substr(50) + ); + } + + getFlagsFen() { + return this.castleFlags['b'].map(V.CoordToColumn).join(""); + } + + setFlags(fenflags) { + this.castleFlags = { 'b': [-1, -1] }; + for (let i = 0; i < 2; i++) + this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i)); + } + + static get TOWER() { + return 't'; + } + static get EAGLE() { + return 'e'; + } + static get CARDINAL() { + return 'c'; + } + static get DUKE() { + return 'd'; + } + static get SOLDIER() { + return 's'; + } + // Kaiser is technically a King, so let's keep things simple. + + static get PIECES() { + return ChessRules.PIECES.concat( + [V.TOWER, V.EAGLE, V.CARDINAL, V.DUKE, V.SOLDIER]); + } + + getPotentialMovesFrom(sq) { + let moves = []; + const piece = this.getPiece(sq[0], sq[1]); + switch (piece) { + case V.TOWER: + moves = this.getPotentialTowerMoves(sq); + break; + case V.EAGLE: + moves = this.getPotentialEagleMoves(sq); + break; + case V.CARDINAL: + moves = this.getPotentialCardinalMoves(sq); + break; + case V.DUKE: + moves = this.getPotentialDukeMoves(sq); + break; + case V.SOLDIER: + moves = this.getPotentialSoldierMoves(sq); + break; + default: + moves = super.getPotentialMovesFrom(sq); + } + if ( + piece != V.KING && + this.kingPos['w'][0] != this.kingPos['b'][0] && + this.kingPos['w'][1] != this.kingPos['b'][1] + ) { + return moves; + } + // TODO: factor two next "if" into one (rank/column...) + if (this.kingPos['w'][1] == this.kingPos['b'][1]) { + const colKing = this.kingPos['w'][1]; + let intercept = 0; //count intercepting pieces + let [kingPos1, kingPos2] = [this.kingPos['w'][0], this.kingPos['b'][0]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[i][colKing] != V.EMPTY) intercept++; + } + if (intercept >= 2) return moves; + // intercept == 1 (0 is impossible): + // Any move not removing intercept is OK + return moves.filter(m => { + return ( + // From another column? + m.start.y != colKing || + // From behind a king? (including kings themselves!) + m.start.x <= kingPos1 || + m.start.x >= kingPos2 || + // Intercept piece moving: must remain in-between + ( + m.end.y == colKing && + m.end.x > kingPos1 && + m.end.x < kingPos2 + ) + ); + }); + } + if (this.kingPos['w'][0] == this.kingPos['b'][0]) { + const rowKing = this.kingPos['w'][0]; + let intercept = 0; //count intercepting pieces + let [kingPos1, kingPos2] = [this.kingPos['w'][1], this.kingPos['b'][1]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[rowKing][i] != V.EMPTY) intercept++; + } + if (intercept >= 2) return moves; + // intercept == 1 (0 is impossible): + // Any move not removing intercept is OK + return moves.filter(m => { + return ( + // From another row? + m.start.x != rowKing || + // From "behind" a king? (including kings themselves!) + m.start.y <= kingPos1 || + m.start.y >= kingPos2 || + // Intercept piece moving: must remain in-between + ( + m.end.x == rowKing && + m.end.y > kingPos1 && + m.end.y < kingPos2 + ) + ); + }); + } + // piece == king: check only if move.end.y == enemy king column, + // or if move.end.x == enemy king rank. + const color = this.getColor(sq[0], sq[1]); + const oppCol = V.GetOppCol(color); + return moves.filter(m => { + if ( + m.end.y != this.kingPos[oppCol][1] && + m.end.x != this.kingPos[oppCol][0] + ) { + return true; + } + // check == -1 if (row, or col) unchecked, 1 if checked and occupied, + // 0 if checked and clear + let check = [-1, -1]; + // TODO: factor two next "if"... + if (m.end.x == this.kingPos[oppCol][0]) { + if (check[0] < 0) { + // Do the check: + check[0] = 0; + let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[m.end.x][i] != V.EMPTY) { + check[0]++; + break; + } + } + return check[0] == 1; + } + // Check already done: + return check[0] == 1; + } + //if (m.end.y == this.kingPos[oppCol][1]) //true... + if (check[1] < 0) { + // Do the check: + check[1] = 0; + let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[i][m.end.y] != V.EMPTY) { + check[1]++; + break; + } + } + return check[1] == 1; + } + // Check already done: + return check[1] == 1; + }); + } + + // TODO: some merging to do with Orda method (and into base_rules.js) + getSlideNJumpMoves_([x, y], steps, oneStep) { + let moves = []; + outerLoop: for (let step of steps) { + const s = step.s; + let i = x + s[0]; + let j = y + s[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (!step.onlyTake) moves.push(this.getBasicMove([x, y], [i, j])); + // NOTE: (bad) HACK here, since onlyTake is true only for Eagle + // capturing moves, which are oneStep... + if (oneStep || step.onlyTake) continue outerLoop; + i += s[0]; + j += s[1]; + } + if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]) && !step.onlyMove) + moves.push(this.getBasicMove([x, y], [i, j])); + } + return moves; + } + + static get steps() { + return ( + Object.assign( + { + t: [ + { s: [-1, 0] }, + { s: [1, 0] }, + { s: [0, -1] }, + { s: [0, 1] }, + { s: [-1, -1], onlyMove: true }, + { s: [-1, 1], onlyMove: true }, + { s: [1, -1], onlyMove: true }, + { s: [1, 1], onlyMove: true } + ], + c: [ + { s: [-1, 0], onlyMove: true }, + { s: [1, 0], onlyMove: true }, + { s: [0, -1], onlyMove: true }, + { s: [0, 1], onlyMove: true }, + { s: [-1, -1] }, + { s: [-1, 1] }, + { s: [1, -1] }, + { s: [1, 1] } + ], + e: [ + { s: [-1, 0], onlyMove: true }, + { s: [1, 0], onlyMove: true }, + { s: [0, -1], onlyMove: true }, + { s: [0, 1], onlyMove: true }, + { s: [-1, -1], onlyMove: true }, + { s: [-1, 1], onlyMove: true }, + { s: [1, -1], onlyMove: true }, + { s: [1, 1], onlyMove: true }, + { s: [-2, -1], onlyTake: true }, + { s: [-2, 1], onlyTake: true }, + { s: [-1, -2], onlyTake: true }, + { s: [-1, 2], onlyTake: true }, + { s: [1, -2], onlyTake: true }, + { s: [1, 2], onlyTake: true }, + { s: [2, -1], onlyTake: true }, + { s: [2, 1], onlyTake: true } + ] + }, + ChessRules.steps + ) + ); + } + + getPotentialTowerMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.TOWER]); + } + + getPotentialCardinalMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.CARDINAL]); + } + + getPotentialEagleMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.EAGLE]); + } + + getPotentialDukeMoves([x, y]) { + // Anything to capture around? mark other steps to explore after + let steps = []; + const oppCol = V.GetOppCol(this.getColor(x, y)); + let moves = []; + for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + moves.push(super.getBasicMove([x, y], [i, j])); + } + else steps.push({ s: s, onlyMove: true }); + } + if (steps.length > 0) { + const noncapturingMoves = this.getSlideNJumpMoves_([x, y], steps); + Array.prototype.push.apply(moves, noncapturingMoves); + } + return moves; + } + + getPotentialKingMoves([x, y]) { + if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]); + // Empire doesn't castle: + return super.getSlideNJumpMoves( + [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); + } + + getPotentialSoldierMoves([x, y]) { + const c = this.getColor(x, y); + const shiftX = (c == 'w' ? -1 : 1); + const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9); + let steps = []; + if (!lastRank) steps.push([shiftX, 0]); + if (y > 0) steps.push([0, -1]); + if (y < 9) steps.push([0, 1]); + return super.getSlideNJumpMoves([x, y], steps, 1); + } + + isAttacked(sq, color) { + if (color == 'b') return super.isAttacked(sq, color); + // Empire: only pawn and king (+ queen if promotion) in common: + return ( + super.isAttackedByPawn(sq, color) || + this.isAttackedByTower(sq, color) || + this.isAttackedByEagle(sq, color) || + this.isAttackedByCardinal(sq, color) || + this.isAttackedByDuke(sq, color) || + this.isAttackedBySoldier(sq, color) || + super.isAttackedByKing(sq, color) || + super.isAttackedByQueen(sq, color) + ); + } + + isAttackedByTower(sq, color) { + return super.isAttackedBySlideNJump(sq, color, V.TOWER, V.steps[V.ROOK]); + } + + isAttackedByEagle(sq, color) { + return super.isAttackedBySlideNJump( + sq, color, V.EAGLE, V.steps[V.KNIGHT], 1); + } + + isAttackedByCardinal(sq, color) { + return super.isAttackedBySlideNJump( + sq, color, V.CARDINAL, V.steps[V.BISHOP]); + } + + isAttackedByDuke(sq, color) { + return ( + super.isAttackedBySlideNJump( + sq, color, V.DUKE, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1 + ) + ); + } + + isAttackedBySoldier([x, y], color) { + const shiftX = (color == 'w' ? 1 : -1); //shift from king + return super.isAttackedBySlideNJump( + [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1); + } + + updateCastleFlags(move, piece) { + // Only black can castle: + const firstRank = 0; + if (piece == V.KING && move.appear[0].c == 'b') + this.castleFlags['b'] = [8, 8]; + else if ( + move.start.x == firstRank && + this.castleFlags['b'].includes(move.start.y) + ) { + const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + else if ( + move.end.x == firstRank && + this.castleFlags['b'].includes(move.end.y) + ) { + const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + } + + getCurrentScore() { + // Turn has changed: + const color = V.GetOppCol(this.turn); + const lastRank = (color == 'w' ? 0 : 7); + if (this.kingPos[color][0] == lastRank) + // The opposing edge is reached! + return color == "w" ? "1-0" : "0-1"; + if (this.atLeastOneMove()) return "*"; + // Game over + const oppCol = this.turn; + return (oppCol == "w" ? "0-1" : "1-0"); + } + + static get VALUES() { + return Object.assign( + {}, + ChessRules.VALUES, + { + t: 7, + e: 7, + c: 4, + d: 4, + s: 2 + } + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + +}; diff --git a/variants/Enpassant/class.js b/variants/Enpassant/class.js new file mode 100644 index 0000000..cb21830 --- /dev/null +++ b/variants/Enpassant/class.js @@ -0,0 +1,209 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; + +export class EnpassantRules extends ChessRules { + + static IsGoodEnpassant(enpassant) { + if (enpassant != "-") { + const squares = enpassant.split(","); + if (squares.length > 2) return false; + for (let sq of squares) { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) return false; + } + } + return true; + } + + getPpath(b) { + return (b[1] == V.KNIGHT ? "Enpassant/" : "") + b; + } + + getEpSquare(moveOrSquare) { + if (!moveOrSquare) return undefined; + if (typeof moveOrSquare === "string") { + const square = moveOrSquare; + if (square == "-") return undefined; + // Expand init + dest squares into a full path: + const init = V.SquareToCoords(square.substr(0, 2)); + let newPath = [init]; + if (square.length == 2) return newPath; + const dest = V.SquareToCoords(square.substr(2)); + const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i])); + // Check if it's a knight(rider) movement: + let step = [0, 0]; + if (delta[0] > 0 && delta[1] > 0 && delta[0] != delta[1]) { + // Knightrider + const minShift = Math.min(delta[0], delta[1]); + step[0] = (dest.x - init.x) / minShift; + step[1] = (dest.y - init.y) / minShift; + } else { + // "Sliders" + step = ['x', 'y'].map((i, idx) => { + return (dest[i] - init[i]) / delta[idx] || 0 + }); + } + let x = init.x + step[0], + y = init.y + step[1]; + while (x != dest.x || y != dest.y) { + newPath.push({ x: x, y: y }); + x += step[0]; + y += step[1]; + } + newPath.push(dest); + return newPath; + } + // Argument is a move: all intermediate squares are en-passant candidates, + // except if the moving piece is a king. + const move = moveOrSquare; + const piece = move.appear[0].p; + if (piece == V.KING || + ( + Math.abs(move.end.x-move.start.x) <= 1 && + Math.abs(move.end.y-move.start.y) <= 1 + ) + ) { + return undefined; + } + const delta = [move.end.x-move.start.x, move.end.y-move.start.y]; + let step = undefined; + if (piece == V.KNIGHT) { + const divisor = Math.min(Math.abs(delta[0]), Math.abs(delta[1])); + step = [delta[0]/divisor || 0, delta[1]/divisor || 0]; + } else { + step = [ + delta[0]/Math.abs(delta[0]) || 0, + delta[1]/Math.abs(delta[1]) || 0 + ]; + } + let res = []; + for ( + let [x,y] = [move.start.x+step[0],move.start.y+step[1]]; + x != move.end.x || y != move.end.y; + x += step[0], y += step[1] + ) { + res.push({ x: x, y: y }); + } + // Add final square to know which piece is taken en passant: + res.push(move.end); + return res; + } + + getEnpassantFen() { + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return "-"; //no en-passant + const epsq = this.epSquares[L - 1]; + if (epsq.length <= 2) return epsq.map(V.CoordsToSquare).join(""); + // Condensate path: just need initial and final squares: + return V.CoordsToSquare(epsq[0]) + V.CoordsToSquare(epsq[epsq.length - 1]); + } + + getPotentialMovesFrom([x, y]) { + let moves = super.getPotentialMovesFrom([x,y]); + // Add en-passant captures from this square: + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return moves; + const squares = this.epSquares[L - 1]; + const S = squares.length; + // Object describing the removed opponent's piece: + const pipoV = new PiPo({ + x: squares[S-1].x, + y: squares[S-1].y, + c: V.GetOppCol(this.turn), + p: this.getPiece(squares[S-1].x, squares[S-1].y) + }); + // Check if existing non-capturing moves could also capture en passant + moves.forEach(m => { + if ( + m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere + m.vanish.length <= 1 && + [...Array(S-1).keys()].some(i => { + return m.end.x == squares[i].x && m.end.y == squares[i].y; + }) + ) { + m.vanish.push(pipoV); + } + }); + // Special case of the king knight's movement: + if (this.getPiece(x, y) == V.KING) { + V.steps[V.KNIGHT].forEach(step => { + const endX = x + step[0]; + const endY = y + step[1]; + if ( + V.OnBoard(endX, endY) && + [...Array(S-1).keys()].some(i => { + return endX == squares[i].x && endY == squares[i].y; + }) + ) { + let enpassantMove = this.getBasicMove([x, y], [endX, endY]); + enpassantMove.vanish.push(pipoV); + moves.push(enpassantMove); + } + }); + } + return moves; + } + + getEnpassantCaptures([x, y], shiftX) { + const Lep = this.epSquares.length; + const squares = this.epSquares[Lep - 1]; + let moves = []; + if (!!squares) { + const S = squares.length; + const taken = squares[S-1]; + const pipoV = new PiPo({ + x: taken.x, + y: taken.y, + p: this.getPiece(taken.x, taken.y), + c: this.getColor(taken.x, taken.y) + }); + [...Array(S-1).keys()].forEach(i => { + const sq = squares[i]; + if (sq.x == x + shiftX && Math.abs(sq.y - y) == 1) { + let enpassantMove = this.getBasicMove([x, y], [sq.x, sq.y]); + enpassantMove.vanish.push(pipoV); + moves.push(enpassantMove); + } + }); + } + return moves; + } + + // Remove the "onestep" condition: knight promote to knightrider: + getPotentialKnightMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT]); + } + + filterValid(moves) { + const filteredMoves = super.filterValid(moves); + // If at least one full move made, everything is allowed: + if (this.movesCount >= 2) + return filteredMoves; + // Else, forbid captures: + return filteredMoves.filter(m => m.vanish.length == 1); + } + + isAttackedByKnight(sq, color) { + return this.isAttackedBySlideNJump( + sq, + color, + V.KNIGHT, + V.steps[V.KNIGHT] + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + + static get VALUES() { + return { + p: 1, + r: 5, + n: 4, + b: 3, + q: 9, + k: 1000 + }; + } + +}; diff --git a/variants/Evolution/class.js b/variants/Evolution/class.js new file mode 100644 index 0000000..5250089 --- /dev/null +++ b/variants/Evolution/class.js @@ -0,0 +1,34 @@ +import { ChessRules } from "@/base_rules"; + +export class EvolutionRules extends ChessRules { + + getPotentialMovesFrom([x, y]) { + let moves = super.getPotentialMovesFrom([x, y]); + const c = this.getColor(x, y); + const piece = this.getPiece(x, y); + if ( + [V.BISHOP, V.ROOK, V.QUEEN].includes(piece) && + (c == 'w' && x == 7) || (c == 'b' && x == 0) + ) { + // Move from first rank + const forward = (c == 'w' ? -1 : 1); + for (let shift of [-2, 0, 2]) { + if ( + (piece == V.ROOK && shift != 0) || + (piece == V.BISHOP && shift == 0) + ) { + continue; + } + if ( + V.OnBoard(x+2*forward, y+shift) && + this.board[x+forward][y+shift/2] != V.EMPTY && + this.getColor(x+2*forward, y+shift) != c + ) { + moves.push(this.getBasicMove([x,y], [x+2*forward,y+shift])); + } + } + } + return moves; + } + +}; diff --git a/variants/Extinction/class.js b/variants/Extinction/class.js new file mode 100644 index 0000000..e42e294 --- /dev/null +++ b/variants/Extinction/class.js @@ -0,0 +1,118 @@ +import { ChessRules } from "@/base_rules"; + +export class ExtinctionRules extends ChessRules { + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) } + ); + } + + static IsGoodPosition(position) { + if (!ChessRules.IsGoodPosition(position)) return false; + // Also check that each piece type is present + const rows = position.split("/"); + let pieces = {}; + for (let row of rows) { + for (let i = 0; i < row.length; i++) { + if (isNaN(parseInt(row[i], 10)) && !pieces[row[i]]) + pieces[row[i]] = true; + } + } + if (Object.keys(pieces).length != 12) return false; + return true; + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + const pos = V.ParseFen(fen).position; + // NOTE: no need for safety "|| []", because each piece type is present + // (otherwise game is already over!) + this.material = { + w: { + [V.KING]: pos.match(/K/g).length, + [V.QUEEN]: pos.match(/Q/g).length, + [V.ROOK]: pos.match(/R/g).length, + [V.KNIGHT]: pos.match(/N/g).length, + [V.BISHOP]: pos.match(/B/g).length, + [V.PAWN]: pos.match(/P/g).length + }, + b: { + [V.KING]: pos.match(/k/g).length, + [V.QUEEN]: pos.match(/q/g).length, + [V.ROOK]: pos.match(/r/g).length, + [V.KNIGHT]: pos.match(/n/g).length, + [V.BISHOP]: pos.match(/b/g).length, + [V.PAWN]: pos.match(/p/g).length + } + }; + } + + // TODO: verify this assertion + atLeastOneMove() { + return true; //always at least one possible move + } + + filterValid(moves) { + return moves; //there is no check + } + + getCheckSquares() { + return []; + } + + postPlay(move) { + super.postPlay(move); + // Treat the promotion case: (not the capture part) + if (move.appear[0].p != move.vanish[0].p) { + this.material[move.appear[0].c][move.appear[0].p]++; + this.material[move.appear[0].c][V.PAWN]--; + } + if (move.vanish.length == 2 && move.appear.length == 1) + //capture + this.material[move.vanish[1].c][move.vanish[1].p]--; + } + + postUndo(move) { + super.postUndo(move); + if (move.appear[0].p != move.vanish[0].p) { + this.material[move.appear[0].c][move.appear[0].p]--; + this.material[move.appear[0].c][V.PAWN]++; + } + if (move.vanish.length == 2 && move.appear.length == 1) + this.material[move.vanish[1].c][move.vanish[1].p]++; + } + + getCurrentScore() { + if (this.atLeastOneMove()) { + // Game not over? + const color = this.turn; + if ( + Object.keys(this.material[color]).some(p => { + return this.material[color][p] == 0; + }) + ) { + return this.turn == "w" ? "0-1" : "1-0"; + } + return "*"; + } + return this.turn == "w" ? "0-1" : "1-0"; //NOTE: currently unreachable... + } + + evalPosition() { + const color = this.turn; + if ( + Object.keys(this.material[color]).some(p => { + return this.material[color][p] == 0; + }) + ) { + // Very negative (resp. positive) + // if white (reps. black) pieces set is incomplete + return (color == "w" ? -1 : 1) * V.INFINITY; + } + return super.evalPosition(); + } + +}; diff --git a/variants/Fanorona/class.js b/variants/Fanorona/class.js new file mode 100644 index 0000000..e1e653a --- /dev/null +++ b/variants/Fanorona/class.js @@ -0,0 +1,342 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class FanoronaRules extends ChessRules { + + static get Options() { + return null; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get Monochrome() { + return true; + } + + static get Lines() { + let lines = []; + // Draw all inter-squares lines, shifted: + for (let i = 0; i < V.size.x; i++) + lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]); + for (let j = 0; j < V.size.y; j++) + lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]); + const columnDiags = [ + [[0.5, 0.5], [2.5, 2.5]], + [[0.5, 2.5], [2.5, 0.5]], + [[2.5, 0.5], [4.5, 2.5]], + [[4.5, 0.5], [2.5, 2.5]] + ]; + for (let j of [0, 2, 4, 6]) { + lines = lines.concat( + columnDiags.map(L => [[L[0][0], L[0][1] + j], [L[1][0], L[1][1] + j]]) + ); + } + return lines; + } + + static get Notoodark() { + return true; + } + + static GenRandInitFen() { + return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0"; + } + + setOtherVariables(fen) { + // Local stack of captures during a turn (squares + directions) + this.captures = [ [] ]; + } + + static get size() { + return { x: 5, y: 9 }; + } + + getPiece() { + return V.PAWN; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + 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 (row[i].toLowerCase() == V.PAWN) sumElts++; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + getPpath(b) { + return "Fanorona/" + b; + } + + getPPpath(m, orientation) { + // m.vanish.length >= 2, first capture gives direction + const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end); + const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y]; + const multStep = (orientation == 'w' ? 1 : -1); + const normalizedStep = [ + multStep * step[0] / Math.abs(step[0]), + multStep * step[1] / Math.abs(step[1]) + ]; + return ( + "Fanorona/arrow_" + + (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0) + ); + } + + // After moving, add stones captured in "step" direction from new location + // [x, y] to mv.vanish (if any captured stone!) + addCapture([x, y], step, move) { + let [i, j] = [x + step[0], y + step[1]]; + const oppCol = V.GetOppCol(move.vanish[0].c); + while ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN })); + i += step[0]; + j += step[1]; + } + return (move.vanish.length >= 2); + } + + getPotentialMovesFrom([x, y]) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + var c = captures[L-1]; + if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1]) + return []; + } + const oppCol = V.GetOppCol(this.turn); + let steps = V.steps[V.ROOK]; + if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); + let moves = []; + for (let s of steps) { + if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) { + // Add a move to say "I'm done capturing" + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: x - s[0], y: y - s[1] } + }) + ); + continue; + } + let [i, j] = [x + s[0], y + s[1]]; + if (captures.some(c => c.square.x == i && c.square.y == j)) continue; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + // The move is potentially allowed. Might lead to 2 different captures + let mv = super.getBasicMove([x, y], [i, j]); + const capt = this.addCapture([i, j], s, mv); + if (capt) { + moves.push(mv); + mv = super.getBasicMove([x, y], [i, j]); + } + const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv); + if (capt_bw) moves.push(mv); + // Captures take priority (if available) + if (!capt && !capt_bw && L == 0) moves.push(mv); + } + } + return moves; + } + + atLeastOneCapture() { + const color = this.turn; + const oppCol = V.GetOppCol(color); + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + // If some adjacent enemy stone, with free space to capture it, + // toward a square not already visited, through a different step + // from last one: then yes. + const c = captures[L-1]; + const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]]; + let steps = V.steps[V.ROOK]; + if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); + // TODO: half of the steps explored are redundant + for (let s of steps) { + if (s[0] == c.step[0] && s[1] == c.step[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + !V.OnBoard(i, j) || + this.board[i][j] != V.EMPTY || + captures.some(c => c.square.x == i && c.square.y == j) + ) { + continue; + } + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] != V.EMPTY && + this.getColor(i + s[0], j + s[1]) == oppCol + ) { + return true; + } + if ( + V.OnBoard(x - s[0], y - s[1]) && + this.board[x - s[0]][y - s[1]] != V.EMPTY && + this.getColor(x - s[0], y - s[1]) == oppCol + ) { + return true; + } + } + return false; + } + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + // TODO: this could be more efficient + this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2) + ) { + return true; + } + } + } + return false; + } + + static KeepCaptures(moves) { + return moves.filter(m => m.vanish.length >= 2); + } + + getPossibleMovesFrom(sq) { + let moves = this.getPotentialMovesFrom(sq); + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + if (captures.length > 0) return this.getPotentialMovesFrom(sq); + const captureMoves = V.KeepCaptures(moves); + if (captureMoves.length > 0) return captureMoves; + if (this.atLeastOneCapture()) return []; + return moves; + } + + getAllValidMoves() { + const moves = super.getAllValidMoves(); + if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves); + return moves; + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + play(move) { + const color = this.turn; + move.turn = color; //for undo + V.PlayOnBoard(this.board, move); + if (move.vanish.length >= 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.push({ + square: move.start, + step: [move.end.x - move.start.x, move.end.y - move.start.y] + }); + if (this.atLeastOneCapture()) + // There could be other captures (optional) + move.notTheEnd = true; + } + if (!move.notTheEnd) { + this.turn = V.GetOppCol(color); + this.movesCount++; + this.captures.push([]); + } + } + + undo(move) { + V.UndoOnBoard(this.board, move); + if (!move.notTheEnd) { + this.turn = move.turn; + this.movesCount--; + this.captures.pop(); + } + if (move.vanish.length >= 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.pop(); + } + } + + getCurrentScore() { + const color = this.turn; + // If no stones on board, I lose + if ( + this.board.every(b => { + return b.every(cell => { + return (cell == "" || cell[0] != color); + }); + }) + ) { + return (color == 'w' ? "0-1" : "1-0"); + } + return "*"; + } + + getComputerMove() { + const moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + const color = this.turn; + // Capture available? If yes, play it + let captures = moves.filter(m => m.vanish.length >= 2); + let mvArray = []; + while (captures.length >= 1) { + // Then just pick random captures (trying to maximize) + let candidates = captures.filter(c => !!c.notTheEnd); + let mv = null; + if (candidates.length >= 1) mv = candidates[randInt(candidates.length)]; + else mv = captures[randInt(captures.length)]; + this.play(mv); + mvArray.push(mv); + captures = (this.turn == color ? this.getAllValidMoves() : []); + } + if (mvArray.length >= 1) { + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return mvArray; + } + // Just play a random move, which if possible does not let a capture + let candidates = []; + for (let m of moves) { + this.play(m); + if (!this.atLeastOneCapture()) candidates.push(m); + this.undo(m); + } + if (candidates.length >= 1) return candidates[randInt(candidates.length)]; + return moves[randInt(moves.length)]; + } + + getNotation(move) { + if (move.appear.length == 0) return "stop"; + return ( + V.CoordsToSquare(move.start) + + V.CoordsToSquare(move.end) + + (move.vanish.length >= 2 ? "X" : "") + ); + } + +}; diff --git a/variants/Sleepy/class.js b/variants/Sleepy/class.js new file mode 100644 index 0000000..e58ceae --- /dev/null +++ b/variants/Sleepy/class.js @@ -0,0 +1,110 @@ +import ChessRules from "/base_rules.js"; +import PiPo from "/utils/PiPo.js"; +import Move from "/utils/Move.js"; + +export default class SleepyRules extends ChessRules { + + static get Options() { + return { + select: C.Options.select, + input: {}, + styles: ["cylinder"] //TODO + }; + } + + setOtherVariables(fenParsed) { + super.setOtherVariables(fenParsed); + // Stack of "last move" only for intermediate chaining + this.lastMoveEnd = []; + } + + getBasicMove([sx, sy], [ex, ey], tr) { + const L = this.lastMoveEnd.length; + const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null); + if ( + this.board[ex][ey] == "" || + this.getColor(ex, ey) == C.GetOppTurn(this.turn) + ) { + if (piece && !tr) + tr = {c: this.turn, p: piece}; + let mv = super.getBasicMove([sx, sy], [ex, ey], tr); + if (piece) + mv.vanish.pop(); //end of a chain: initial piece remains + return mv; + } + // (Self)Capture: initial, or inside a chain + const initPiece = (piece || this.getPiece(sx, sy)), + destPiece = this.getPiece(ex, ey); + let mv = new Move({ + start: {x: sx, y: sy}, + end: {x: ex, y: ey}, + appear: [ + new PiPo({ + x: ex, + y: ey, + c: this.turn, + p: (!!tr ? tr.p : initPiece) + }) + ], + vanish: [ + new PiPo({ + x: ex, + y: ey, + c: this.turn, + p: destPiece + }) + ] + }); + if (!piece) { + // Initial capture + mv.vanish.unshift( + new PiPo({ + x: sx, + y: sy, + c: this.turn, + p: initPiece + }) + ); + } + mv.chained = destPiece; //easier (no need to detect it) +// mv.drag = {c: this.turn, p: initPiece}; //TODO: doesn't work + return mv; + } + + getPiece(x, y) { + const L = this.lastMoveEnd.length; + if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y) + return this.lastMoveEnd[L-1].p; + return super.getPiece(x, y); + } + + getPotentialMovesFrom([x, y], color) { + const L = this.lastMoveEnd.length; + if ( + L >= 1 && + (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) + ) { + // A self-capture was played: wrong square + return []; + } + return super.getPotentialMovesFrom([x, y], color); + } + + isLastMove(move) { + return !move.chained; + } + + postPlay(move) { + super.postPlay(move); + if (!!move.chained) { + this.lastMoveEnd.push({ + x: move.end.x, + y: move.end.y, + p: move.chained + }); + } + else + this.lastMoveEnd = []; + } + +}; diff --git a/variants/Sleepy/rules.html b/variants/Sleepy/rules.html new file mode 100644 index 0000000..f27cd96 --- /dev/null +++ b/variants/Sleepy/rules.html @@ -0,0 +1,7 @@ +

    + You can "capture" your own pieces, and then move them from the capturing + square in the same turn, with potential chaining if the captured unit + makes a self-capture too. +

    + +

    Benjamin Auder (2021).

    diff --git a/variants/Sleepy/style.css b/variants/Sleepy/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Sleepy/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css"); -- 2.48.1 From ab2ca6784b154f0fd6183b908df124063a45f876 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 30 Jan 2025 18:26:19 +0100 Subject: [PATCH 07/16] update --- base_rules.js | 3 +-- variants/Dynamo/class.js | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/base_rules.js b/base_rules.js index 3d8e463..888fdcf 100644 --- a/base_rules.js +++ b/base_rules.js @@ -2235,8 +2235,7 @@ export default class ChessRules { // 'color' arg because some variants (e.g. Refusal) check opponent moves filterValid(moves, color) { - if (!color) - color = this.turn; + color = color || this.turn; const oppCols = this.getOppCols(color); let kingPos = this.searchKingPos(color); return moves.filter(m => { diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index 7b835e0..d726522 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -181,8 +181,8 @@ export default class DynamoRules extends ChessRules { // NOTE: for pushes, play the pushed piece first. // for pulls: play the piece doing the action first // NOTE: to push a piece out of the board, make it slide until its king - getPotentialMovesFrom([x, y]) { - const color = this.turn; + getPotentialMovesFrom([x, y], color) { + const color = color || this.turn; const sqCol = this.getColor(x, y); const pawnShift = (color == 'w' ? -1 : 1); const pawnStartRank = (color == 'w' ? 6 : 1); @@ -486,6 +486,10 @@ export default class DynamoRules extends ChessRules { return []; } + getAllPotentialMoves(color) { + + } + getSlideNJumpMoves([x, y], steps, oneStep) { let moves = []; const c = this.getColor(x, y); -- 2.48.1 From 4a2da562da7627a70cfe0bc36db231adc00afe02 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 5 Feb 2025 12:02:32 +0100 Subject: [PATCH 08/16] Dynamo --- variants/Dynamo/class.js | 170 +++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 88 deletions(-) diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index d726522..d9bb855 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -53,6 +53,17 @@ export default class DynamoRules extends ChessRules { return res; } + addExitMove(moves, [x, y], kp) { + moves.push( + new Move({ + start: { x: x, y: y }, + end: { x: kp[color][0], y: kp[color][1] }, + appear: [], + vanish: [{ x: x, y: y, c: color, p: piece }] + }) + ); + } + // Step is right, just add (push/pull) moves in this direction // Direction is assumed normalized. getMovesInDirection([x, y], [dx, dy], nbSteps, kp) { @@ -78,17 +89,8 @@ export default class DynamoRules extends ChessRules { i += dx; j += dy; } - if (!this.onBoard(i, j) && piece != 'k') { - // Add special "exit" move, by "taking king" - moves.push( - new Move({ - start: { x: x, y: y }, - end: { x: kp[0], y: kp[1] }, - appear: [], - vanish: [{ x: x, y: y, c: color, p: piece }] - }) - ); - } + if (!this.onBoard(i, j) && piece != 'k') + this.addExitMove(moves, [x, y], kp); return moves; } @@ -178,6 +180,36 @@ export default class DynamoRules extends ChessRules { ); } + // Test if a piece can suicide + canReachBorder(x, y) { + const p = this.getPiece(x, y); + switch (p) { + case 'p': + case 'k': + return false; + case 'n': + return ( + x <= 1 || x >= this.size.x - 2 || y <= 1 || y >= this.size.y - 2 + ); + } + // Slider + let steps = []; + if (['r', 'q'].includes(p)) + steps = steps.concat(this.pieces()['r'].both[0].steps); + if (['b', 'q'].includes(p)) + steps = steps.concat(this.pieces()['b'].both[0].steps); + for (let s of steps) { + let [i, j] = [x + s[0], y + s[1]]; + while (this.onBoard(i, j) && this.board[i][j] == "") { + i += s[0]; + j += s[1]; + } + if (!this.onBoard(i, j)) + return true; + } + return false; + } + // NOTE: for pushes, play the pushed piece first. // for pulls: play the piece doing the action first // NOTE: to push a piece out of the board, make it slide until its king @@ -190,7 +222,7 @@ export default class DynamoRules extends ChessRules { return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end); }; if (this.subTurn == 1) { - const kp = this.searchKingPos(color); + const kp = [ this.searchKingPos('w')[0], this.searchKingPos('b')[0] ]; const addMoves = (dir, nbSteps) => { const newMoves = this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp) @@ -199,21 +231,12 @@ export default class DynamoRules extends ChessRules { Array.prototype.push.apply(moves, newMoves); }; // Free to play any move (if piece of my color): - let moves = - sqCol == color - ? super.getPotentialMovesFrom([x, y]) - : []; - // There may be several suicide moves: keep only one - let hasExit = false; - moves = moves.filter(m => { - const suicide = (m.appear.length == 0); - if (suicide) { - if (hasExit) - return false; - hasExit = true; - } - return true; - }); + let moves = []; + if (sqCol == color) { + moves = super.getPotentialMovesFrom([x, y]) + if (this.canReachBorder(x, y)) + this.addSuicideMove(moves, [x, y], kp); + } // Structure to avoid adding moves twice (can be action & move) let movesHash = {}; moves.forEach(m => { movesHash[getMoveHash(m)] = true; }); @@ -486,50 +509,6 @@ export default class DynamoRules extends ChessRules { return []; } - getAllPotentialMoves(color) { - - } - - getSlideNJumpMoves([x, y], steps, oneStep) { - let moves = []; - const c = this.getColor(x, y); - const piece = this.getPiece(x, y); - outerLoop: for (let step of steps) { - let i = x + step[0]; - let j = y + step[1]; - while (this.onBoard(i, j) && this.board[i][j] == "") { - moves.push(this.getBasicMove([x, y], [i, j])); - if (oneStep) - continue outerLoop; - i += step[0]; - j += step[1]; - } - if (this.onBoard(i, j)) { - if (this.canTake([x, y], [i, j])) - moves.push(this.getBasicMove([x, y], [i, j])); - } - else { - // Add potential board exit (suicide), except for the king - if (piece != 'k') { - moves.push({ - start: { x: x, y: y}, - end: { x: this.kingPos[c][0], y: this.kingPos[c][1] }, - appear: [], - vanish: [ - new PiPo({ - x: x, - y: y, - c: c, - p: piece - }) - ] - }); - } - } - } - return moves; - } - // Does m2 un-do m1 ? (to disallow undoing actions) oppositeMoves(m1, m2) { const isEqual = (av1, av2) => { @@ -567,7 +546,19 @@ export default class DynamoRules extends ChessRules { } } -// TODO: re-write just for here getAllPotentialMoves() ? + getAllPotentialMoves() { + const color = this.turn; + let moves = []; + for (let i=0; i 0 && this.oppositeMoves(this.amoves[La-1], m); @@ -597,7 +588,7 @@ export default class DynamoRules extends ChessRules { } } } - this.undoOnBoard(m); + this.undo(m); return !res; }); } @@ -659,19 +650,11 @@ export default class DynamoRules extends ChessRules { } } - play(move) { -// if (this.subTurn == 1 && move.vanish.length == 0) { -// // Patch to work with old format: (TODO: remove later) -// move.ignore = true; -// return; -// } - - // In preplay ? - this.updateCastleFlags(move); - - const oppCol = C.GetOppTurn(color); - + play(move, filterValid) { + if (!filterValid) + this.updateCastleFlags(move); const color = this.turn; + const oppCol = C.GetOppTurn(color); const gotoNext = (mv) => { const L = this.firstMove.length; this.amoves.push(this.getAmove(this.firstMove[L-1], mv)); @@ -686,7 +669,7 @@ export default class DynamoRules extends ChessRules { this.subTurn = 2; this.firstMove.push(move); if ( - // Condition is true on empty arrays: //TODO: getAllPotentialMoves doesn't exist + // Condition is true on empty arrays: this.getAllPotentialMoves().every(m => { this.playOnBoard(m); const res = this.underCheck([kp], oppCol); //TODO: find kp first @@ -698,7 +681,18 @@ export default class DynamoRules extends ChessRules { gotoNext(this.getEmptyMove()); } } - this.postPlay(move); + } + + undo(move) { + this.undoOnBoard(this.board, move); + if (this.subTurn == 1) { + this.amoves.pop(); + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + } + if (move.subTurn == 1) this.firstMove.pop(); + this.subTurn = move.subTurn; + this.toOldKingPos(move); } }; -- 2.48.1 From 07d16218df6bba6571e9dcd002b4a785bcf2c51b Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 5 Feb 2025 23:14:13 +0100 Subject: [PATCH 09/16] Debug Dynamo --- variants/Dynamo/class.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index d9bb855..08519f5 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -1,3 +1,4 @@ +import Move from "/utils/Move.js"; import ChessRules from "/base_rules.js"; export default class DynamoRules extends ChessRules { @@ -57,9 +58,10 @@ export default class DynamoRules extends ChessRules { moves.push( new Move({ start: { x: x, y: y }, - end: { x: kp[color][0], y: kp[color][1] }, + end: { x: kp[0], y: kp[1] }, appear: [], - vanish: [{ x: x, y: y, c: color, p: piece }] + vanish: [ + { x: x, y: y, c: this.getColor(x, y), p: this.getPiece(x, y) }] }) ); } @@ -214,7 +216,7 @@ export default class DynamoRules extends ChessRules { // for pulls: play the piece doing the action first // NOTE: to push a piece out of the board, make it slide until its king getPotentialMovesFrom([x, y], color) { - const color = color || this.turn; + color = color || this.turn; const sqCol = this.getColor(x, y); const pawnShift = (color == 'w' ? -1 : 1); const pawnStartRank = (color == 'w' ? 6 : 1); @@ -222,7 +224,7 @@ export default class DynamoRules extends ChessRules { return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end); }; if (this.subTurn == 1) { - const kp = [ this.searchKingPos('w')[0], this.searchKingPos('b')[0] ]; + const kp = this.searchKingPos(color)[0]; const addMoves = (dir, nbSteps) => { const newMoves = this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp) @@ -235,7 +237,7 @@ export default class DynamoRules extends ChessRules { if (sqCol == color) { moves = super.getPotentialMovesFrom([x, y]) if (this.canReachBorder(x, y)) - this.addSuicideMove(moves, [x, y], kp); + this.addExitMove(moves, [x, y], kp); } // Structure to avoid adding moves twice (can be action & move) let movesHash = {}; @@ -562,13 +564,14 @@ export default class DynamoRules extends ChessRules { filterValid(moves) { const color = this.turn; - const La = this.amoves.length; + const La = this.amoves.length; //TODO: debug if (this.subTurn == 1) { return moves.filter(m => { // A move is valid either if it doesn't result in a check, // or if a second move is possible to counter the check // (not undoing a potential move + action of the opponent) this.play(m); + const kp = this.searchKingPos(color); let res = this.underCheck(color); if (this.subTurn == 2) { let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m); @@ -576,7 +579,10 @@ export default class DynamoRules extends ChessRules { const moves2 = this.getAllPotentialMoves(); for (let m2 of moves2) { this.play(m2); - const res2 = this.underCheck(color); //TODO: + square + let cur_kp = kp; + if (m2.appear[0].p == 'k') + cur_kp = [m2.appear[0].x, m2.appear[0].y]; + const res2 = this.underCheck(cur_kp, color); const amove = this.getAmove(m, m2); isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], amove); @@ -668,11 +674,15 @@ export default class DynamoRules extends ChessRules { else { this.subTurn = 2; this.firstMove.push(move); + const kp = this.searchKingPos(color); if ( // Condition is true on empty arrays: this.getAllPotentialMoves().every(m => { this.playOnBoard(m); - const res = this.underCheck([kp], oppCol); //TODO: find kp first + let cur_kp = kp; + if (m.appear[0].p == 'k') + cur_kp = [m.appear[0].x, m.appear[0].y]; + const res = this.underCheck(cur_kp, oppCol); this.undoOnBoard(m); return res; }) @@ -683,14 +693,16 @@ export default class DynamoRules extends ChessRules { } } + // For filterValid() undo(move) { this.undoOnBoard(this.board, move); if (this.subTurn == 1) { this.amoves.pop(); - this.turn = V.GetOppCol(this.turn); + this.turn = C.GetOppTurn(this.turn); this.movesCount--; } - if (move.subTurn == 1) this.firstMove.pop(); + if (move.subTurn == 1) + this.firstMove.pop(); this.subTurn = move.subTurn; this.toOldKingPos(move); } -- 2.48.1 From c5fb8354ad889ef45b2483525cd79272372be8e6 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 6 Feb 2025 11:01:58 +0100 Subject: [PATCH 10/16] update --- base_rules.js | 16 ------------ variants/Dynamo/class.js | 53 +++++++++++++--------------------------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/base_rules.js b/base_rules.js index 888fdcf..81be6fd 100644 --- a/base_rules.js +++ b/base_rules.js @@ -175,22 +175,6 @@ export default class ChessRules { return Object.values(cd).map(c => c.toString(36)).join(""); } - // c10 --> 02 (assuming 10 rows) - static SquareFromUsual(sq) { - return ( - (this.size.x - parseInt(sq.substring(1), 10)).toString(36) + - (sq.charCodeAt(0) - 97).toString(36) - ); - } - - // 02 --> c10 - static UsualFromSquare(sq) { - return ( - String.fromCharCode(parseInt(sq.charAt(1), 36) + 97) + - (this.size.x - parseInt(sq.charAt(0), 36)).toString(10) - ); - } - coordsToId(cd) { if (typeof cd.x == "number") { return ( diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index 08519f5..7744293 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -30,27 +30,14 @@ export default class DynamoRules extends ChessRules { this.subTurn = 1; // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5 // for example, and moving herself to g4. If just move: e2h5 - this.lastAction = []; - if (fenParsed.amove != '-') { - this.lastAction = fenParsed.amove.split('/').map(a => { - return { - c1: C.SquareToCoords(C.SquareFromUsual(a.substr(0, 2))), - c2: C.SquareToCoords(C.SquareFromUsual(a.substr(2, 2))) - }; - }); - } + this.amove = []; + if (fenParsed.amove != '-') + this.amove = JSON.parse(fenParsed.amove); } getPartFen(o) { let res = super.getPartFen(o); - if (o.init) - res["amove"] = '-'; - else { - res["amove"] = this.lastAction.map(a => { - C.UsualFromSquare(C.CoordsToSquare(a.c1)) + - C.UsualFromSquare(C.CoordsToSquare(a.c2)) - }).join('/'); - } + res["amove"] = (o.init ? '-' : JSON.stringify(this.amove)); return res; } @@ -539,7 +526,6 @@ export default class DynamoRules extends ChessRules { ); } - // TODO: just stack in this.lastAction instead getAmove(move1, move2) { // Just merge (one is action one is move, one may be empty) return { @@ -562,9 +548,10 @@ export default class DynamoRules extends ChessRules { return moves; } +// TODO: I over-simplified, amove need to be saved for after undos + filterValid(moves) { const color = this.turn; - const La = this.amoves.length; //TODO: debug if (this.subTurn == 1) { return moves.filter(m => { // A move is valid either if it doesn't result in a check, @@ -574,7 +561,7 @@ export default class DynamoRules extends ChessRules { const kp = this.searchKingPos(color); let res = this.underCheck(color); if (this.subTurn == 2) { - let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m); + let isOpposite = this.oppositeMoves(this.amove, m); if (res || isOpposite) { const moves2 = this.getAllPotentialMoves(); for (let m2 of moves2) { @@ -584,8 +571,7 @@ export default class DynamoRules extends ChessRules { cur_kp = [m2.appear[0].x, m2.appear[0].y]; const res2 = this.underCheck(cur_kp, color); const amove = this.getAmove(m, m2); - isOpposite = - La > 0 && this.oppositeMoves(this.amoves[La-1], amove); + isOpposite = this.oppositeMoves(this.amove, amove); this.undo(m2); if (!res2 && !isOpposite) { res = false; @@ -605,8 +591,8 @@ export default class DynamoRules extends ChessRules { super.filterValid( moves.filter(m => { // Move shouldn't undo another: - const amove = this.getAmove(this.firstMove[Lf-1], m); - return !this.oppositeMoves(this.amoves[La-1], amove); + const amove = this.getAmove(this.firstMove, m); + return !this.oppositeMoves(this.amove, amove); }) ) ); @@ -629,13 +615,12 @@ export default class DynamoRules extends ChessRules { return null; // If subTurn == 2 && square is empty && !underCheck && !isOpposite, // then return an empty move, allowing to "pass" subTurn2 - const La = this.amoves.length; - const Lf = this.firstMove.length; + const kp = this.searchKingPos(this.turn); if ( this.subTurn == 2 && - this.board[square[0]][square[1]] == V.EMPTY && - !this.underCheck(this.turn) && - (La == 0 || !this.oppositeMoves(this.amoves[La-1], this.firstMove[Lf-1])) + this.board[square[0]][square[1]] == "" && + !this.underCheck(kp, C.GetOppTurn(this.turn)) && + !this.oppositeMoves(this.amove, this.firstMove)) ) { return this.getEmptyMove(); } @@ -661,9 +646,9 @@ export default class DynamoRules extends ChessRules { this.updateCastleFlags(move); const color = this.turn; const oppCol = C.GetOppTurn(color); + move.subTurn = this.subTurn; //for undo const gotoNext = (mv) => { - const L = this.firstMove.length; - this.amoves.push(this.getAmove(this.firstMove[L-1], mv)); + this.amove = this.getAmove(this.firstMove, mv); this.turn = oppCol; this.subTurn = 1; this.movesCount++; @@ -673,7 +658,7 @@ export default class DynamoRules extends ChessRules { gotoNext(move); else { this.subTurn = 2; - this.firstMove.push(move); + this.firstMove = move; const kp = this.searchKingPos(color); if ( // Condition is true on empty arrays: @@ -697,14 +682,10 @@ export default class DynamoRules extends ChessRules { undo(move) { this.undoOnBoard(this.board, move); if (this.subTurn == 1) { - this.amoves.pop(); this.turn = C.GetOppTurn(this.turn); this.movesCount--; } - if (move.subTurn == 1) - this.firstMove.pop(); this.subTurn = move.subTurn; - this.toOldKingPos(move); } }; -- 2.48.1 From 5fba0247131d6b966e47be5c94b78e87db2d7ae3 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 2 May 2025 18:57:37 +0200 Subject: [PATCH 11/16] Fix Dynamo --- LICENSE | 2 +- base_rules.js | 2 +- variants/Dynamo/class.js | 162 ++++++++++++++++++++++++--------------- 3 files changed, 101 insertions(+), 65 deletions(-) diff --git a/LICENSE b/LICENSE index 692028c..0d46506 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC license https://opensource.org/licenses/ISC -Copyright (C) 2021-2024 Benjamin Auder +Copyright (C) 2021-2025 Benjamin Auder Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/base_rules.js b/base_rules.js index 81be6fd..912b08f 100644 --- a/base_rules.js +++ b/base_rules.js @@ -1184,7 +1184,7 @@ export default class ChessRules { // Get opponent color(s): may differ from turn (e.g. Checkered) getOppCols(color) { - return (color == "w" ? "b" : "w"); + return [ (color == "w" ? "b" : "w") ]; } // Is (x,y) on the chessboard? diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index 7744293..c220fc1 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -28,9 +28,8 @@ export default class DynamoRules extends ChessRules { setOtherVariables(fenParsed) { super.setOtherVariables(fenParsed); this.subTurn = 1; - // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5 - // for example, and moving herself to g4. If just move: e2h5 - this.amove = []; + // Last action format: arrays of (dis)appearing things. + this.amove = null; if (fenParsed.amove != '-') this.amove = JSON.parse(fenParsed.amove); } @@ -65,7 +64,7 @@ export default class DynamoRules extends ChessRules { let counter = 1; while (this.onBoard(i, j) && this.board[i][j] == "") { if (i == lastRank && piece == 'p') { - // Promotion by push or pull + // Promotion by push or pull (if suicide) this.pawnPromotions.forEach(p => { let move = super.getBasicMove([x, y], [i, j], { c: color, p: p }); moves.push(move); @@ -153,22 +152,6 @@ export default class DynamoRules extends ChessRules { return false; } - isAprioriValidVertical([x1, y1], x2) { - const piece = this.getPiece(x1, y1); - const deltaX = Math.abs(x1 - x2); - const startRank = (this.getColor(x1, y1) == 'w' ? 6 : 1); - return ( - ['q', 'r'].includes(piece) || - ( - ['k', 'p'].includes(piece) && - ( - deltaX == 1 || - (deltaX == 2 && piece == 'p' && x1 == startRank) - ) - ) - ); - } - // Test if a piece can suicide canReachBorder(x, y) { const p = this.getPiece(x, y); @@ -200,18 +183,18 @@ export default class DynamoRules extends ChessRules { } // NOTE: for pushes, play the pushed piece first. - // for pulls: play the piece doing the action first + // for pulls: play the piece doing the action first (if moving) // NOTE: to push a piece out of the board, make it slide until its king getPotentialMovesFrom([x, y], color) { color = color || this.turn; const sqCol = this.getColor(x, y); const pawnShift = (color == 'w' ? -1 : 1); const pawnStartRank = (color == 'w' ? 6 : 1); + const kp = this.searchKingPos(color)[0]; const getMoveHash = (m) => { return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end); }; if (this.subTurn == 1) { - const kp = this.searchKingPos(color)[0]; const addMoves = (dir, nbSteps) => { const newMoves = this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp) @@ -274,16 +257,21 @@ export default class DynamoRules extends ChessRules { } break; case 'r': - if (deltaX == 0 || deltaY == 0) + if (deltaX == 0 || deltaY == 0) { addMoves(step); + addMoves([-step[0], -step[1]]); //potential pull + } break; case 'b': - if (deltaX == deltaY) + if (deltaX == deltaY) { addMoves(step); + addMoves([-step[0], -step[1]]); + } break; case 'q': // All steps are valid for a queen: addMoves(step); + addMoves([-step[0], -step[1]]); break; case 'k': if (deltaX <= 1 && deltaY <= 1) @@ -296,8 +284,7 @@ export default class DynamoRules extends ChessRules { } // If subTurn == 2 then we should have a first move, // which restrict what we can play now: only in the first move direction - const L = this.firstMove.length; - const fm = this.firstMove[L-1]; + const fm = this.firstMove; if ( (fm.appear.length == 2 && fm.vanish.length == 2) || (fm.vanish[0].c == sqCol && sqCol != color) @@ -322,7 +309,7 @@ export default class DynamoRules extends ChessRules { ['p', 'k', 'n'].includes(piece) ? 1 : null; - return this.getMovesInDirection([x, y], dir, nbSteps); + return this.getMovesInDirection([x, y], dir, nbSteps, kp); } return []; } @@ -347,7 +334,8 @@ export default class DynamoRules extends ChessRules { deltaY >= 2 || (fm.vanish[0].c == color && deltaY > 0) || (fm.vanish[0].c != color && deltaY == 0) || - Math.abs(fm.end.x - fm.start.x) > deltaX || + Math.abs(fm.end.x - fm.start.x) >= 3 || + (Math.abs(fm.end.x - fm.start.x) == 2 && deltaX >= 2) || fm.end.y - fm.start.y != fm.start.y - y ) { return []; @@ -401,13 +389,13 @@ export default class DynamoRules extends ChessRules { let [i, j] = [x + dir[0], y + dir[1]]; while ( (i != fm.start.x || j != fm.start.y) && - this.board[i][j] == V.EMPTY + this.board[i][j] == "" ) { i += dir[0]; j += dir[1]; } if (i == fm.start.x && j == fm.start.y) - return this.getMovesInDirection([x, y], dir); + return this.getMovesInDirection([x, y], dir, undefined, kp); } return []; } @@ -428,7 +416,7 @@ export default class DynamoRules extends ChessRules { const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); const nbSteps = (fm.vanish[0].p == 'n' ? 1 : null); - return this.getMovesInDirection([x, y], dir, nbSteps); + return this.getMovesInDirection([x, y], dir, nbSteps, kp); } return []; }; @@ -462,7 +450,7 @@ export default class DynamoRules extends ChessRules { j += dir[1]; } if (i == fm.start.x && j == fm.start.y) - return this.getMovesInDirection([x, y], dir); + return this.getMovesInDirection([x, y], dir, undefined, kp); } return []; }; @@ -498,8 +486,64 @@ export default class DynamoRules extends ChessRules { return []; } + // NOTE: a king could move next to the enemy king + underCheck(square_s, oppCols) { + const sq = square_s[0], + oppCol = oppCols[0]; + // Look for every directions from kp + const P = this.pieces(oppCol, 0, 0); + for (const piece of ['r', 'n', 'b', 'q', 'k', 'p']) { + const stepArray = (P[piece].attack || P[piece].both); + for (const stepObj of stepArray) { + const range = stepObj.range || Math.max(this.size.x, this.size.y); + for (const s of stepObj.steps) { + let [i, j] = [sq[0] - s[0], sq[1] - s[1]]; //going backward + let rg = 1; + while (this.onBoard(i, j) && rg <= range) { + if (this.board[i][j] != "") { + if (this.getColor(i,j) == oppCol && this.getPiece(i,j) == piece) + { + // 1) Push: going forward from king + let [ii, jj] = [sq[0] + s[0], sq[1] + s[1]]; + let rg2 = 1; + while (this.onBoard(ii, jj)) { + rg2++; + if (this.board[ii][jj] != "" || rg2 > range) + break; + ii += s[0]; + jj += s[1]; + } + if (!this.onBoard(ii, jj)) + return true; + // 2) Pull: still backward (sliders only) + if (['r','b','q'].includes(piece)) { + [ii, jj] = [i - s[0], j - s[1]]; + while (this.onBoard(ii, jj)) { + if (this.board[ii][jj] != "") + break; + ii -= s[0]; + jj -= s[1]; + } + if (!this.onBoard(ii, jj)) + return true; + } + } + break; + } + i -= s[0]; + j -= s[1]; + rg++; + } + } + } + } + return false; + } //TODO: checks by pull!! + // Does m2 un-do m1 ? (to disallow undoing actions) oppositeMoves(m1, m2) { + if (!m1 || !m2) + return false; const isEqual = (av1, av2) => { for (let av of av1) { const avInAv2 = av2.find(elt => { @@ -539,7 +583,7 @@ export default class DynamoRules extends ChessRules { let moves = []; for (let i=0; i { // A move is valid either if it doesn't result in a check, // or if a second move is possible to counter the check // (not undoing a potential move + action of the opponent) - this.play(m); - const kp = this.searchKingPos(color); - let res = this.underCheck(color); + this.play(m, true); + const kp = this.searchKingPos(color)[0]; + let res = this.underCheck([kp], [oppCol]); + let isOpposite = this.oppositeMoves(this.amove, m); if (this.subTurn == 2) { - let isOpposite = this.oppositeMoves(this.amove, m); if (res || isOpposite) { const moves2 = this.getAllPotentialMoves(); for (let m2 of moves2) { - this.play(m2); + this.play(m2, true); let cur_kp = kp; - if (m2.appear[0].p == 'k') + if (m2.appear.length >= 1 && m2.appear[0].p == 'k') cur_kp = [m2.appear[0].x, m2.appear[0].y]; - const res2 = this.underCheck(cur_kp, color); + const res2 = this.underCheck([cur_kp], [oppCol]); const amove = this.getAmove(m, m2); isOpposite = this.oppositeMoves(this.amove, amove); this.undo(m2); @@ -584,9 +627,6 @@ export default class DynamoRules extends ChessRules { return !res; }); } - if (La == 0) - return super.filterValid(moves); - const Lf = this.firstMove.length; return ( super.filterValid( moves.filter(m => { @@ -607,20 +647,15 @@ export default class DynamoRules extends ChessRules { }); } - doClick(square) { - // A click to promote a piece on subTurn 2 would trigger this. - // For now it would then return [NaN, NaN] because surrounding squares - // have no IDs in the promotion modal. TODO: improve this? - if (isNaN(square[0])) - return null; + doClick(coords) { // If subTurn == 2 && square is empty && !underCheck && !isOpposite, // then return an empty move, allowing to "pass" subTurn2 const kp = this.searchKingPos(this.turn); if ( this.subTurn == 2 && - this.board[square[0]][square[1]] == "" && - !this.underCheck(kp, C.GetOppTurn(this.turn)) && - !this.oppositeMoves(this.amove, this.firstMove)) + this.board[coords.x][coords.y] == "" && + !this.underCheck(kp, [C.GetOppTurn(this.turn)]) && + !this.oppositeMoves(this.amove, this.firstMove) ) { return this.getEmptyMove(); } @@ -630,7 +665,7 @@ export default class DynamoRules extends ChessRules { updateCastleFlags(move) { if (move.start.x < 0) return; //empty move (pass subTurn 2) - const firstRank = { 'w': V.size.x - 1, 'b': 0 }; + const firstRank = { 'w': this.size.x - 1, 'b': 0 }; for (let v of move.vanish) { if (v.p == 'k') this.castleFlags[v.c] = [this.size.y, this.size.y]; @@ -644,14 +679,17 @@ export default class DynamoRules extends ChessRules { play(move, filterValid) { if (!filterValid) this.updateCastleFlags(move); + else + move.subTurn = this.subTurn; //for undo const color = this.turn; const oppCol = C.GetOppTurn(color); - move.subTurn = this.subTurn; //for undo const gotoNext = (mv) => { - this.amove = this.getAmove(this.firstMove, mv); + if (!filterValid) { + this.amove = this.getAmove(this.firstMove, mv); + this.movesCount++; + } this.turn = oppCol; this.subTurn = 1; - this.movesCount++; }; this.playOnBoard(move); if (this.subTurn == 2) @@ -665,7 +703,7 @@ export default class DynamoRules extends ChessRules { this.getAllPotentialMoves().every(m => { this.playOnBoard(m); let cur_kp = kp; - if (m.appear[0].p == 'k') + if (m.appear.length >= 1 && m.appear[0].p == 'k') cur_kp = [m.appear[0].x, m.appear[0].y]; const res = this.underCheck(cur_kp, oppCol); this.undoOnBoard(m); @@ -680,11 +718,9 @@ export default class DynamoRules extends ChessRules { // For filterValid() undo(move) { - this.undoOnBoard(this.board, move); - if (this.subTurn == 1) { + this.undoOnBoard(move); + if (this.subTurn == 1) this.turn = C.GetOppTurn(this.turn); - this.movesCount--; - } this.subTurn = move.subTurn; } -- 2.48.1 From 5421b6aec79e255691169b55c39079baac5ccb22 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 2 May 2025 19:01:45 +0200 Subject: [PATCH 12/16] Temporary fix for iOS dark mode --- iOS_fix.css | 3 +++ index.html | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 iOS_fix.css diff --git a/iOS_fix.css b/iOS_fix.css new file mode 100644 index 0000000..1a5e812 --- /dev/null +++ b/iOS_fix.css @@ -0,0 +1,3 @@ +.in-shadow { + opacity: 0.5; +} diff --git a/index.html b/index.html index 72b9091..669b406 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,18 @@ content="width=device-width, initial-scale=1"/> + -- 2.48.1 From 99522597bdbac8200fa41ba12a3a6e30f56eb306 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Mon, 5 May 2025 15:09:52 +0200 Subject: [PATCH 13/16] Dynamo cannot be played on a cylindric board --- variants/Dynamo/class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index c220fc1..5098bbb 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -7,7 +7,7 @@ export default class DynamoRules extends ChessRules { return { select: C.Options.select, input: [], - styles: ["cylinder", "doublemove", "progressive"] + styles: ["doublemove", "progressive"] }; } -- 2.48.1 From d75c8069c959d57cce528cb1de84078d261484e6 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 7 May 2025 18:02:59 +0200 Subject: [PATCH 14/16] Prepare Sleepy variant --- variants/Sleepy/class.js | 133 ++++++++++++------------------------- variants/Sleepy/rules.html | 8 +-- variants/Sleepy/style.css | 32 +++++++++ 3 files changed, 77 insertions(+), 96 deletions(-) diff --git a/variants/Sleepy/class.js b/variants/Sleepy/class.js index e58ceae..00a6009 100644 --- a/variants/Sleepy/class.js +++ b/variants/Sleepy/class.js @@ -1,110 +1,59 @@ import ChessRules from "/base_rules.js"; -import PiPo from "/utils/PiPo.js"; -import Move from "/utils/Move.js"; export default class SleepyRules extends ChessRules { - static get Options() { - return { - select: C.Options.select, - input: {}, - styles: ["cylinder"] //TODO - }; + pieces(color, x, y) { + let res = super.pieces(color, x, y); + res['s'] = {"class": "sleepy-pawn", moveas: "p"}; + res['u'] = {"class": "sleepy-rook", moveas: "r"}; + res['o'] = {"class": "sleepy-knight", moveas: "n"}; + res['c'] = {"class": "sleepy-bishop", moveas: "b"}; + res['t'] = {"class": "sleepy-queen", moveas: "q"}; + return res; } - setOtherVariables(fenParsed) { - super.setOtherVariables(fenParsed); - // Stack of "last move" only for intermediate chaining - this.lastMoveEnd = []; + static get V_PIECES() { + return ['p', 'r', 'n', 'b', 'q']; } - - getBasicMove([sx, sy], [ex, ey], tr) { - const L = this.lastMoveEnd.length; - const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null); - if ( - this.board[ex][ey] == "" || - this.getColor(ex, ey) == C.GetOppTurn(this.turn) - ) { - if (piece && !tr) - tr = {c: this.turn, p: piece}; - let mv = super.getBasicMove([sx, sy], [ex, ey], tr); - if (piece) - mv.vanish.pop(); //end of a chain: initial piece remains - return mv; - } - // (Self)Capture: initial, or inside a chain - const initPiece = (piece || this.getPiece(sx, sy)), - destPiece = this.getPiece(ex, ey); - let mv = new Move({ - start: {x: sx, y: sy}, - end: {x: ex, y: ey}, - appear: [ - new PiPo({ - x: ex, - y: ey, - c: this.turn, - p: (!!tr ? tr.p : initPiece) - }) - ], - vanish: [ - new PiPo({ - x: ex, - y: ey, - c: this.turn, - p: destPiece - }) - ] - }); - if (!piece) { - // Initial capture - mv.vanish.unshift( - new PiPo({ - x: sx, - y: sy, - c: this.turn, - p: initPiece - }) - ); - } - mv.chained = destPiece; //easier (no need to detect it) -// mv.drag = {c: this.turn, p: initPiece}; //TODO: doesn't work - return mv; + static get S_PIECES() { + return ['s', 'u', 'o', 'c', 't']; } - getPiece(x, y) { - const L = this.lastMoveEnd.length; - if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y) - return this.lastMoveEnd[L-1].p; - return super.getPiece(x, y); + // Forbid sleepy pieces to capture + canTake([x1, y1], [x2, y2]) { + return ( + this.getColor(x1, y1) !== this.getColor(x2, y2) && + (['k'].concat(V.V_PIECES)).includes(this.getPiece(x1, y1)) + ); } - getPotentialMovesFrom([x, y], color) { - const L = this.lastMoveEnd.length; - if ( - L >= 1 && - (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) - ) { - // A self-capture was played: wrong square - return []; - } - return super.getPotentialMovesFrom([x, y], color); - } - isLastMove(move) { - return !move.chained; - } + //TODO: + - postPlay(move) { - super.postPlay(move); - if (!!move.chained) { - this.lastMoveEnd.push({ - x: move.end.x, - y: move.end.y, - p: move.chained + pawnPostProcess(moves, color, oppCols) { + let res = super.pawnPostProcess(moves, color, oppCols); + if (res.length > 0 && res[0].vanish[0].p == 's') { + // Fix promotions of non-violent pawns (if any) + res.forEach(m => { + if (m.appear[0].p != 's') + m.appear[0].p = V.NV_PIECES[V.V_PIECES.indexOf(m.appear[0].p)]; }); } - else - this.lastMoveEnd = []; + return res; + } + + prePlay(move) { + super.prePlay(move); + // NOTE: drop moves already taken into account in base prePlay() + if (move.vanish.length == 2 && move.appear.length == 1) { + const normal = V.V_PIECES.includes(move.vanish[1].p); + const pIdx = + (normal ? V.V_PIECES : V.NV_PIECES).indexOf(move.vanish[1].p); + const resPiece = (normal ? V.NV_PIECES : V.V_PIECES)[pIdx]; + super.updateReserve(C.GetOppTurn(this.turn), resPiece, + this.reserve[C.GetOppTurn(this.turn)][resPiece] + 1); + } } }; diff --git a/variants/Sleepy/rules.html b/variants/Sleepy/rules.html index f27cd96..0ab3249 100644 --- a/variants/Sleepy/rules.html +++ b/variants/Sleepy/rules.html @@ -1,7 +1,7 @@

    - You can "capture" your own pieces, and then move them from the capturing - square in the same turn, with potential chaining if the captured unit - makes a self-capture too. + After moving three times a unit is very tired and falls asleep. + It cannot move anymore, and doesn't threaten enemy pieces. + It awakes with full powers when a friendly unit attacks it while moving.

    -

    Benjamin Auder (2021).

    +

    Benjamin Auder (2025).

    diff --git a/variants/Sleepy/style.css b/variants/Sleepy/style.css index a3550bc..d614763 100644 --- a/variants/Sleepy/style.css +++ b/variants/Sleepy/style.css @@ -1 +1,33 @@ @import url("/base_pieces.css"); + +piece.white.sleepy-pawn { + background-image: url('/pieces/yellow_pawn.svg'); +} +piece.white.sleepy-rook { + background-image: url('/pieces/yellow_rook.svg'); +} +piece.white.sleepy-knight { + background-image: url('/pieces/yellow_knight.svg'); +} +piece.white.sleepy-bishop { + background-image: url('/pieces/yellow_bishop.svg'); +} +piece.white.sleepy-queen { + background-image: url('/pieces/yellow_queen.svg'); +} + +piece.black.nsleepy-pawn { + background-image: url('/pieces/red_pawn.svg'); +} +piece.black.sleepy-rook { + background-image: url('/pieces/red_rook.svg'); +} +piece.black.sleepy-knight { + background-image: url('/pieces/red_knight.svg'); +} +piece.black.sleepy-bishop { + background-image: url('/pieces/red_bishop.svg'); +} +piece.black.sleepy-queen { + background-image: url('/pieces/red_queen.svg'); +} -- 2.48.1 From 8149d4fbf6e050c1b62e07a635cff5e919ede03e Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 5 Jun 2025 18:05:26 +0200 Subject: [PATCH 15/16] Fix Dynamo complete_rules. Started rewriting of Eightpieces --- base_rules.js | 11 +- variants/Dynamo/complete_rules.html | 19 +-- variants/Eightpieces/class.js | 153 +++++++++-------------- variants/Eightpieces/complete_rules.html | 84 ++++++++----- variants/Eightpieces/rules.html | 8 +- 5 files changed, 140 insertions(+), 135 deletions(-) diff --git a/base_rules.js b/base_rules.js index 912b08f..de3fa44 100644 --- a/base_rules.js +++ b/base_rules.js @@ -175,6 +175,14 @@ export default class ChessRules { return Object.values(cd).map(c => c.toString(36)).join(""); } + // b4 --> {x:4, y:1} + coordsFromUsual(sq) { + return { + x: this.size.x - parseInt(sq.charAt(1), 10), + y: sq.charCodeAt(0) - 97 + }; + } + coordsToId(cd) { if (typeof cd.x == "number") { return ( @@ -651,8 +659,7 @@ export default class ChessRules { this[arrName] = ArrayFun.init(this.size.x, this.size.y, null); if (arrName == "d_pieces") this.marks.forEach((m) => { - const formattedSquare = C.SquareFromUsual(m); - const mCoords = C.SquareToCoords(formattedSquare); + const mCoords = this.coordsFromUsual(m); addPiece(mCoords.x, mCoords.y, arrName, "mark"); }); }; diff --git a/variants/Dynamo/complete_rules.html b/variants/Dynamo/complete_rules.html index 68e9cc0..5856fc2 100644 --- a/variants/Dynamo/complete_rules.html +++ b/variants/Dynamo/complete_rules.html @@ -36,7 +36,7 @@
    Possible "pawn moves" in the initial position.
    @@ -59,10 +59,10 @@
    + data-fen='rnbqkbnr/ppp1pppp/8/3p4/8/2N5/PPPPPPPP/R1BQKBNR w 0 {"flags":"0707","amove":"-"}'>
    + data-fen='rnbqkbnr/ppp1pppp/8/8/8/2p5/PPPPPPPP/RNBQKBNR w 0 {"flags":"0707","amove":"-"}'>
    Pulling the d5 pawn to c3 (left: before, right: after). @@ -80,7 +80,7 @@
    + data-fen='rnb1qbnr/pppkpppp/3p4/8/Q1P5/5NP1/PP1PPP1P/RNB1KB1R w 0 {"flags":"0707","amove":"-"}'>
    Check: the queen threatens to pull the king off the board @@ -95,10 +95,10 @@
    + data-fen='rnbqk1nr/ppppppbp/6p1/8/3B4/1P6/P1PPPPPP/RN1QKBNR w 0 {"flags":"0707","amove":"-"}'>
    + data-fen='rnbqk1nr/pppppp1p/6p1/8/3b4/1P6/PBPPPPPP/RN1QKBNR w 0 {"flags":"0707","amove":"-"}'>
    Pushing the d4 bishop to b2 (left: before, right: after).
    @@ -119,7 +119,7 @@
    + data-fen='8/4B3/8/8/6Qk/8/4N3/K7 w 0 {"flags":"0707","amove":"-"}'>
    Dynamo checkmate ("Dynamate" :) )
    @@ -140,3 +140,8 @@ this page might help too.

    + + + + + diff --git a/variants/Eightpieces/class.js b/variants/Eightpieces/class.js index 3808a16..eb68c76 100644 --- a/variants/Eightpieces/class.js +++ b/variants/Eightpieces/class.js @@ -1,99 +1,75 @@ -import { randInt, sample } from "@/utils/alea"; -import { ChessRules, PiPo, Move } from "@/base_rules"; +//import { randInt, sample } from "@/utils/alea"; +import ChessRules from "/base_rules.js"; +//PiPo, Move ? -export class EightpiecesRules extends ChessRules { +export default class EightpiecesRules extends ChessRules { - static get JAILER() { - return "j"; - } - static get SENTRY() { - return "s"; - } - static get LANCER() { - return "l"; - } - - static get IMAGE_EXTENSION() { - // Temporarily, for the time SVG pieces are being designed: - return ".png"; - } - - // Lancer directions *from white perspective* - static get LANCER_DIRS() { + static get Options() { return { - 'c': [-1, 0], //north - 'd': [-1, 1], //N-E - 'e': [0, 1], //east - 'f': [1, 1], //S-E - 'g': [1, 0], //south - 'h': [1, -1], //S-W - 'm': [0, -1], //west - 'o': [-1, -1] //N-W + select: C.Options.select, + input: [], + styles: ["doublemove", "progressive"] }; } - static get PIECES() { - return ChessRules.PIECES - .concat([V.JAILER, V.SENTRY]) - .concat(Object.keys(V.LANCER_DIRS)); - } +// TODO: variable (setupOthers) for lancers directions (x,y) => dir ("0, 1, 2, ...") 0 = top 1 = north east... / white viewpoint + +//variable lancer_orient ... --> array size 8 x 8 (TODO?) + // - getPiece(i, j) { - const piece = this.board[i][j].charAt(1); - // Special lancer case: 8 possible orientations - if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER; - return piece; + static get LANCER_STEP() { + return { + 'N': [-1, 0], + 'NE': [-1, 1], + 'E': [0, 1], + 'SE': [1, 1], + 'S': [1, 0], + 'SO': [1, 61], + 'O': [0, -1], + 'NO': [-1, -1] + }; } - getPpath(b, color, score, orientation) { - if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b; - if (Object.keys(V.LANCER_DIRS).includes(b[1])) { - if (orientation == 'w') return "Eightpieces/tmp_png/" + b; - // Find opposite direction for adequate display: - let oppDir = ''; - switch (b[1]) { - case 'c': - oppDir = 'g'; - break; - case 'g': - oppDir = 'c'; - break; - case 'd': - oppDir = 'h'; - break; - case 'h': - oppDir = 'd'; - break; - case 'e': - oppDir = 'm'; - break; - case 'm': - oppDir = 'e'; - break; - case 'f': - oppDir = 'o'; - break; - case 'o': - oppDir = 'f'; - break; + pieces(color, x, y) { + let basePieces = super.pieces(color, x, y); + const extension = { + 'j': { + "class": "jailer", + moves: [ + { + steps: [[1, 0], [0, 1], [-1, 0], [0, -1]] + } + ] + }, + 's': { + "class": "sentry", + moves: [ + { + steps: [[1, 1], [1, -1], [-1, 1], [-1, -1]] + } + ] + }, + 'l': { + "class": "lancer", + both: [ + { + steps: [ V.LANCER_STEP [ + this.lancer_orient[(x*this.size.y+y).toString()] ] + ] + } + ] } - return "Eightpieces/tmp_png/" + b[0] + oppDir; - } - // TODO: after we have SVG pieces, remove the folder and next prefix: - return "Eightpieces/tmp_png/" + b; + }; } - getPPpath(m, orientation) { - return ( - this.getPpath( - m.appear[0].c + m.appear[0].p, - null, - null, - orientation - ) - ); + // lorient : "{z1:NO,z2:SE, ...etc}" + setOtherVariables(fenParsed) { + super.setOtherVariables(fenParsed); + this.lancer_orient = JSON.parse(fenParsed.lorient); } + //TODO: from here + static ParseFen(fen) { const fenParts = fen.split(" "); return Object.assign( @@ -102,19 +78,6 @@ export class EightpiecesRules extends ChessRules { ); } - static IsGoodFen(fen) { - if (!ChessRules.IsGoodFen(fen)) return false; - const fenParsed = V.ParseFen(fen); - // 5) Check sentry push (if any) - if ( - fenParsed.sentrypush != "-" && - !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/) - ) { - return false; - } - return true; - } - getFen() { return super.getFen() + " " + this.getSentrypushFen(); } diff --git a/variants/Eightpieces/complete_rules.html b/variants/Eightpieces/complete_rules.html index c1d12ab..d12574f 100644 --- a/variants/Eightpieces/complete_rules.html +++ b/variants/Eightpieces/complete_rules.html @@ -1,48 +1,74 @@ -p.boxed - | Three new pieces appear. All pieces are unique. + + + Eightpieces Rules + + + + +
    +

    Eightpieces Rules

    -p. +

    + Three new pieces appear. All pieces are unique. +

    + +

    There are only one rook, one bishop and one knight per side in this variant. That explains the name. The king and queen are still there, and the three remaining slots are taken by new pieces: +

    -ul - li. +
      +
    • The lancer 'L' is oriented and can only move in the direction it points, by any number of squares as long as an enemy isn't met (it can jump over friendly pieces). If an opponent' piece is found, it can be captured. After moving you can reorient the lancer. - li. +
    • +
    • The sentry 'S' moves like a bishop but doesn't capture directly. It "pushes" enemy pieces instead, either on an empty square or on other enemy pieces which are thus (self-)captured. - li. +
    • +
    • The jailer 'J' moves like a rook but also doesn't capture. It immobilizes enemy pieces which are vertically or horizontally adjacent. +
    • +
    -p. +

    On the following diagram the white sentry can push the black lancer to capture the black pawn on b4. The lancer is then immobilized by the white jailer at a4. +

    + +
    +
    +
    +
    +
    +
    Left: before white move S"push"f4. Right: after this move.
    +
    + +To reorient a stuck lancer, +
      +
    • Just after being pushed: play a move which 'capture your king".
    • +
    • Later in the game: click on the lancer.
    • +
    + +

    Complete rules

    + +

    + The rules were invented by Jeff Kubach (2020), who described them much + more precisely on the + chessvariants page. + While the summary given above may suffice to start playing, + you should read the complete rules to fully understand this variant. +

    -figure.diagram-container - .diagram.diag12 - | fen:7k/8/8/8/Jp3m2/8/3S4/K7: - .diagram.diag22 - | fen:7k/8/8/8/Jm3S2/8/8/K7: - figcaption Left: before white move S"push"f4. Right: after this move. - -p To reorient a stuck lancer, -ul - li Just after being pushed: play a move which 'capture your king". - li Later in the game: click on the lancer. - -h3 Complete rules - -p - | The rules were invented by Jeff Kubach (2020), who described them much - | more precisely on the - a(href="https://www.chessvariants.com/rules/8-piece-chess") - | chessvariants page - | . While the summary given above may suffice to start playing, - | you should read the complete rules to fully understand this variant. + + + + diff --git a/variants/Eightpieces/rules.html b/variants/Eightpieces/rules.html index 41e9d42..69ac8aa 100644 --- a/variants/Eightpieces/rules.html +++ b/variants/Eightpieces/rules.html @@ -1,3 +1,7 @@ -Three new pieces appear: the lancer is a rook with a constrained direction, the sentry moves like a bishop and pushes pieces, and the jailer immobilizes pieces orthogonally adjacent. +

    + Three new pieces appear: the lancer is a rook with a constrained direction, + the sentry moves like a bishop and pushes pieces, + and the jailer immobilizes pieces orthogonally adjacent. +

    -The goal is still to checkmate. +

    The goal is still to checkmate.

    -- 2.48.1 From 1b977e8ab4f23d66f1ef93a78b8de250829b2dc1 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 6 Jun 2025 19:55:48 +0200 Subject: [PATCH 16/16] Work on Eightpieces --- base_rules.js | 4 +-- variants/Eightpieces/class.js | 48 ++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/base_rules.js b/base_rules.js index de3fa44..26e8e49 100644 --- a/base_rules.js +++ b/base_rules.js @@ -1151,7 +1151,7 @@ export default class ChessRules { // Color of thing on square (i,j). '' if square is empty getColor(i, j) { if (typeof i == "string") - return i; //reserves + return i; //reserves : 'wb' return this.board[i][j].charAt(0); } @@ -1166,7 +1166,7 @@ export default class ChessRules { // Piece on i,j. '' if square is empty getPiece(i, j) { if (typeof j == "string") - return j; //reserves + return j; //reserves : 'bq' return this.board[i][j].charAt(1); } diff --git a/variants/Eightpieces/class.js b/variants/Eightpieces/class.js index eb68c76..c1edfb4 100644 --- a/variants/Eightpieces/class.js +++ b/variants/Eightpieces/class.js @@ -12,11 +12,6 @@ export default class EightpiecesRules extends ChessRules { }; } -// TODO: variable (setupOthers) for lancers directions (x,y) => dir ("0, 1, 2, ...") 0 = top 1 = north east... / white viewpoint - -//variable lancer_orient ... --> array size 8 x 8 (TODO?) - // - static get LANCER_STEP() { return { 'N': [-1, 0], @@ -30,6 +25,10 @@ export default class EightpiecesRules extends ChessRules { }; } + encodeSquare(x, y) { + return (x*this.size.y+y).toString(); + } + pieces(color, x, y) { let basePieces = super.pieces(color, x, y); const extension = { @@ -54,7 +53,7 @@ export default class EightpiecesRules extends ChessRules { both: [ { steps: [ V.LANCER_STEP [ - this.lancer_orient[(x*this.size.y+y).toString()] ] + this.lancer_orient[this.encodeSquare(x, y)] ] ] } ] @@ -62,30 +61,35 @@ export default class EightpiecesRules extends ChessRules { }; } + get pawnPromotions() { + // TODO: lancer orientation = backward vertical from promotion square? + return ['q', 'r', 'n', 'b', 'j', 's', 'l']; + } + // lorient : "{z1:NO,z2:SE, ...etc}" setOtherVariables(fenParsed) { super.setOtherVariables(fenParsed); this.lancer_orient = JSON.parse(fenParsed.lorient); } - //TODO: from here - - static ParseFen(fen) { - const fenParts = fen.split(" "); - return Object.assign( - ChessRules.ParseFen(fen), - { sentrypush: fenParts[5] } + getPartFen(o) { + return Object.assign({}, + super.getPartFen(o), + { + "lorient": o.init ? "TODO" : this.getLorientFen(), + "sentrypush": o.init ? "-" : this.getSentrypushFen() + } ); } - getFen() { - return super.getFen() + " " + this.getSentrypushFen(); + getLorientFen() { + // TODO: use this.lancer_orient to output {z1:NO,z2:SE, ...etc} + return ""; } - getFenForRepeat() { - return super.getFenForRepeat() + "_" + this.getSentrypushFen(); - } + + // TODO: from here --> L1500 in base -- moves generation getSentrypushFen() { const L = this.sentryPush.length; if (!this.sentryPush[L-1]) return "-"; @@ -97,6 +101,8 @@ export default class EightpiecesRules extends ChessRules { .join(""); } + + setOtherVariables(fen) { super.setOtherVariables(fen); // subTurn == 2 only when a sentry moved, and is about to push something @@ -188,6 +194,12 @@ export default class EightpiecesRules extends ChessRules { return super.canTake([x1, y1], [x2, y2]); } + + + + + + // Is piece on square (x,y) immobilized? isImmobilized([x, y]) { const color = this.getColor(x, y); -- 2.48.1