From: Benjamin Auder Date: Mon, 8 Feb 2021 20:20:57 +0000 (+0100) Subject: Add Convert variant X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/css/store/pieces/cb.svg?a=commitdiff_plain;h=1006b211894bdc0624e2fd332ac78322bf8ca0ee;p=vchess.git Add Convert variant --- diff --git a/client/src/translations/en.js b/client/src/translations/en.js index cd10d039..ca3b13a6 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -199,6 +199,7 @@ export const translations = { "Chinese Chess": "Chinese Chess", "Convert & support (v1)": "Convert & support (v1)", "Convert & support (v2)": "Convert & support (v2)", + "Convert enemy pieces": "Convert enemy pieces", "Cross the river": "Cross the river", "Dance with the King": "Dance with the King", "Dangerous captures": "Dangerous captures", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 11d64a48..30095bfe 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -199,6 +199,7 @@ export const translations = { "Chinese Chess": "Ajedrez chino", "Convert & support (v1)": "Convertir & apoyar (v1)", "Convert & support (v2)": "Convertir & apoyar (v2)", + "Convert enemy pieces": "Convierte piezas opuestas", "Cross the river": "Cruza el río", "Dance with the King": "Baila con el Rey", "Dangerous captures": "Capturas peligrosas", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 4d134fc2..0ebc8175 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -199,6 +199,7 @@ export const translations = { "Chinese Chess": "Échecs chinois", "Convert & support (v1)": "Convertir & soutenir (v1)", "Convert & support (v2)": "Convertir & soutenir (v2)", + "Convert enemy pieces": "Convertissez les pièces adverses", "Cross the river": "Traversez la rivière", "Dance with the King": "Dansez avec le Roi", "Dangerous captures": "Captures dangeureuses", diff --git a/client/src/translations/rules/Convert/en.pug b/client/src/translations/rules/Convert/en.pug new file mode 100644 index 00000000..164eef14 --- /dev/null +++ b/client/src/translations/rules/Convert/en.pug @@ -0,0 +1,30 @@ +p.boxed. + Capturing an enemy piece changes its color. + The converted piece must be moved immediatly. + +p. + Each capture transforms the captured piece into a friendly one + (changing color). Since it cannot remain on the capturing square, + it has to be moved immediately. This newly converted piece can now + capture (and thus "convert") another enemy piece, and so on. + +p. + Castling is impossible if any square traversed by the king is controlled + (directly or through a chain). Checks are ignored otherwise. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5: + figcaption Black cannot castle because of the "check" Bxd5, Bxb7, Pxc8 + +p. + The goal is to convert the enemy king. Note that capturing a king is + possible only if a king move is then available. However, a piece gives + check (concerning castle) even if it's impossible. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4: + figcaption Black wins from the first diagram: Bxe3, Nxc2, Pxb1=R, Qxc1, Ke1 + +p If in a chain a converted piece cannot move, then you must reset the move. diff --git a/client/src/translations/rules/Convert/es.pug b/client/src/translations/rules/Convert/es.pug new file mode 100644 index 00000000..73f62218 --- /dev/null +++ b/client/src/translations/rules/Convert/es.pug @@ -0,0 +1,36 @@ +p.boxed. + La captura de una pieza enemiga cambia su color. + La pieza convertida debe moverse inmediatamente. + +p. + Cada captura transforma la pieza capturada en una pieza amiga (cambiando + de color). Como no puede permanecer en la casilla de captura, + debe moverse de inmediato. La pieza recién convertida puede + luego capturar (y así "convertir") una otra pieza opuesta, etc. + +p. + El enroque es imposible si se controla una casilla cruzada por el rey + (directamente o mediante una cadena). + Los jaques se ignoran el resto del tiempo. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5: + figcaption Las negras no pueden enrocar debido al "jaque" Bxd5, Bxb7, Pxc8 + +p. + El objetivo es convertir al rey contrario. Tenga en cuenta que la captura + de un rey solo es posible si puedes realizar un movimiento de rey después. + Sin embargo, una pieza da jaque (en cuanto al enroque) + incluso si es imposible. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4: + figcaption. + Las negras ganan desde el primer diagrama: + Bxe3, Nxc2, Pxb1 = R, Qxc1, Ke1 + +p. + Si en una cadena una pieza convertida no se puede mover, + entonces tienes que empezar de nuevo. diff --git a/client/src/translations/rules/Convert/fr.pug b/client/src/translations/rules/Convert/fr.pug new file mode 100644 index 00000000..3e90a8c5 --- /dev/null +++ b/client/src/translations/rules/Convert/fr.pug @@ -0,0 +1,35 @@ +p.boxed. + Capturer une pièce ennemie change sa couleur. + La pièce convertie doit être déplacée immédiatement. + +p. + Chaque capture transforme la pièce capturée en une pièce amie (changeant + de couleur). Puisqu'elle ne peut pas rester sur la case de capture, elle + doit tout de suite être déplacée. La pièce nouvellement convertie peut + alors capturer (et ainsi "convertir") une autre pièce adverse, etc. + +p. + Le roque est impossible si une case traversée par le roi est contrôlée + (directement ou via une chaîne). Les échecs sont ignorés le reste du temps. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5: + figcaption Les noirs ne peuvent roquer à cause de l'"échec" Bxd5, Bxb7, Pxc8 + +p. + L'objectof est de convertir le roi adverse. Notez qu'une capture de roi + n'est possible que si vous pouvez effectuer un coup de roi ensuite. + Cependant, une pièce donne échec (concernant le roque) + même si c'est impossible. + +figure.diagram-container + .diagram + | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4: + figcaption. + Les noirs gagnent depuis le premier diagramme: + Bxe3, Nxc2, Pxb1=R, Qxc1, Ke1 + +p. + Si dans une chaîne une pièce convertie ne peut pas bouger, + alors vous devez recommencer le coup. diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug index a895843b..eed4c980 100644 --- a/client/src/translations/variants/en.pug +++ b/client/src/translations/variants/en.pug @@ -462,6 +462,7 @@ p. "Ambiguous", "Bario", "Bicolour", + "Convert", "Evolution", "Forward", "Fusion", diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug index 609ac032..01efa971 100644 --- a/client/src/translations/variants/es.pug +++ b/client/src/translations/variants/es.pug @@ -472,6 +472,7 @@ p. "Ambiguous", "Bario", "Bicolour", + "Convert", "Evolution", "Forward", "Fusion", diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug index 6c846970..3566c882 100644 --- a/client/src/translations/variants/fr.pug +++ b/client/src/translations/variants/fr.pug @@ -470,6 +470,7 @@ p. "Ambiguous", "Bario", "Bicolour", + "Convert", "Evolution", "Forward", "Fusion", diff --git a/client/src/variants/Convert.js b/client/src/variants/Convert.js new file mode 100644 index 00000000..297d08db --- /dev/null +++ b/client/src/variants/Convert.js @@ -0,0 +1,267 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class ConvertRules extends ChessRules { + + setOtherVariables(fen) { + super.setOtherVariables(fen); + // Stack of "last move" only for intermediate chaining + this.lastMoveEnd = [null]; + } + + getBasicMove([sx, sy], [ex, ey], tr) { + const L = this.lastMoveEnd.length; + const lm = this.lastMoveEnd[L-1]; + const piece = (!!lm ? lm.p : null); + const c = this.turn; + if (this.board[ex][ey] == V.EMPTY) { + if (!!piece && !tr) tr = { c: c, p: piece } + let mv = super.getBasicMove([sx, sy], [ex, ey], tr); + if (!!piece) mv.vanish.pop(); + return mv; + } + // Capture: initial, or inside a chain + const initPiece = (piece || this.getPiece(sx, sy)); + const oppCol = V.GetOppCol(c); + const oppPiece = 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: c, + p: (!!tr ? tr.p : initPiece) + }) + ], + vanish: [ + new PiPo({ + x: ex, + y: ey, + c: oppCol, + p: oppPiece + }) + ] + }); + if (!piece) { + // Initial capture + mv.vanish.unshift( + new PiPo({ + x: sx, + y: sy, + c: c, + p: initPiece + }) + ); + } + // TODO: This "converted" indication isn't needed in fact, + // because it can be deduced from the move itself. + mv.end.converted = oppPiece; + return mv; + } + + getPotentialMovesFrom([x, y], asA) { + const L = this.lastMoveEnd.length; + if (!!this.lastMoveEnd[L-1]) { + if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) + // A capture was played: wrong square + return []; + asA = this.lastMoveEnd[L-1].p; + } + switch (asA || this.getPiece(x, y)) { + case V.PAWN: return super.getPotentialPawnMoves([x, y]); + case V.ROOK: return super.getPotentialRookMoves([x, y]); + case V.KNIGHT: return super.getPotentialKnightMoves([x, y]); + case V.BISHOP: return super.getPotentialBishopMoves([x, y]); + case V.QUEEN: return super.getPotentialQueenMoves([x, y]); + case V.KING: return super.getPotentialKingMoves([x, y]); + } + return []; + } + + getPossibleMovesFrom(sq) { + const L = this.lastMoveEnd.length; + let asA = undefined; + if (!!this.lastMoveEnd[L-1]) { + if ( + sq[0] != this.lastMoveEnd[L-1].x || + sq[1] != this.lastMoveEnd[L-1].y + ) { + return []; + } + asA = this.lastMoveEnd[L-1].p; + } + return this.filterValid(this.getPotentialMovesFrom(sq, asA)); + } + + isAttacked_aux([x, y], color, explored) { + if (explored.some(sq => sq[0] == x && sq[1] == y)) + // Start of an infinite loop: exit + return false; + explored.push([x, y]); + if (super.isAttacked([x, y], color)) return true; + // Maybe indirect "chaining" attack: + const myColor = this.turn + let res = false; + let toCheck = []; //check all but king (no need) + // Pawns: + const shiftToPawn = (myColor == 'w' ? -1 : 1); + for (let yShift of [-1, 1]) { + const [i, j] = [x + shiftToPawn, y + yShift]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + // NOTE: no need to check color (no enemy pawn can take directly) + this.getPiece(i, j) == V.PAWN + ) { + toCheck.push([i, j]); + } + } + // Knights: + V.steps[V.KNIGHT].forEach(s => { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getPiece(i, j) == V.KNIGHT + ) { + toCheck.push([i, j]); + } + }); + // Sliders: + V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => { + let [i, j] = [x + s[0], y + s[1]]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += s[0]; + j += s[1]; + } + if (!V.OnBoard(i, j)) return; + const piece = this.getPiece(i, j); + if ( + piece == V.QUEEN || + (piece == V.ROOK && (s[0] == 0 || s[1] == 0)) || + (piece == V.BISHOP && (s[0] != 0 && s[1] != 0)) + ) { + toCheck.push([i, j]); + } + }); + for (let ij of toCheck) { + if (this.isAttacked_aux(ij, color, explored)) return true; + } + return false; + } + + isAttacked([x, y], color) { + let explored = []; + return this.isAttacked_aux([x, y], color, explored); + } + + filterValid(moves) { + // No "checks" (except to forbid castle) + return moves; + } + + getCheckSquares() { + return []; + } + + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); + this.epSquares.push(this.getEpSquare(move)); + V.PlayOnBoard(this.board, move); + if (!move.end.converted) { + // Not a capture: change turn + this.turn = V.GetOppCol(this.turn); + this.movesCount++; + this.lastMoveEnd.push(null); + } + else { + this.lastMoveEnd.push( + Object.assign({}, move.end, { p: move.end.converted }) + ); + } + this.postPlay(move); + } + + postPlay(move) { + const c = (!move.end.converted ? V.GetOppCol(this.turn) : this.turn); + const piece = move.appear[0].p; + if (piece == V.KING) { + this.kingPos[c][0] = move.appear[0].x; + this.kingPos[c][1] = move.appear[0].y; + } + super.updateCastleFlags(move, piece, c); + } + + undo(move) { + this.disaggregateFlags(JSON.parse(move.flags)); + this.epSquares.pop(); + this.lastMoveEnd.pop(); + V.UndoOnBoard(this.board, move); + if (!move.end.converted) { + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + } + super.postUndo(move); + } + + getCurrentScore() { + const color = this.turn; + const kp = this.kingPos[color]; + if (this.getColor(kp[0], kp[1]) != color) + return (color == "w" ? "0-1" : "1-0"); + if (!super.atLeastOneMove()) return "1/2"; + return "*"; + } + + getComputerMove() { + let initMoves = this.getAllValidMoves(); + if (initMoves.length == 0) return null; + // Loop until valid move is found (no blocked pawn conversion...) + while (true) { + let moves = JSON.parse(JSON.stringify(initMoves)); + let mvArray = []; + let mv = null; + // Just play random moves (for now at least. TODO?) + while (moves.length > 0) { + mv = moves[randInt(moves.length)]; + mvArray.push(mv); + this.play(mv); + if (!!mv.end.converted) + // A piece was just converted + moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]); + else break; + } + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + if (!mv.end.released) return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + return null; //never reached + } + + getNotation(move) { + if (move.appear.length == 2 && move.appear[0].p == V.KING) + return (move.end.y < move.start.y ? "0-0-0" : "0-0"); + const c = this.turn; + const L = this.lastMoveEnd.length; + const lm = this.lastMoveEnd[L-1]; + const piece = (!lm ? move.appear[0].p : lm.p); + // Basic move notation: + let notation = piece.toUpperCase(); + if ( + this.board[move.end.x][move.end.y] != V.EMPTY || + (piece == V.PAWN && move.start.y != move.end.y) + ) { + notation += "x"; + } + const finalSquare = V.CoordsToSquare(move.end); + notation += finalSquare; + + // Add potential promotion indications: + const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]); + if (move.end.x == firstLastRank[1] && piece == V.PAWN) + notation += "=" + move.appear[0].p.toUpperCase(); + return notation; + } + +}; diff --git a/client/src/variants/Otage.js b/client/src/variants/Otage.js index 451059aa..79157157 100644 --- a/client/src/variants/Otage.js +++ b/client/src/variants/Otage.js @@ -355,7 +355,7 @@ export class OtageRules extends ChessRules { } let baseMoves = []; const c = this.turn; - switch (piece || this.getPiece(x, y)) { + switch (piece) { case V.PAWN: { const firstRank = (c == 'w' ? 7 : 0); baseMoves = this.getPotentialPawnMoves([x, y]).filter(m => { diff --git a/client/src/variants/Pacosako.js b/client/src/variants/Pacosako.js index 0b27be4a..4745f631 100644 --- a/client/src/variants/Pacosako.js +++ b/client/src/variants/Pacosako.js @@ -372,7 +372,7 @@ export class PacosakoRules extends ChessRules { } let baseMoves = []; const c = this.turn; - switch (piece || this.getPiece(x, y)) { + switch (piece) { case V.PAWN: { const firstRank = (c == 'w' ? 7 : 0); baseMoves = this.getPotentialPawnMoves([x, y]).filter(m => { diff --git a/client/src/variants/Takenmake.js b/client/src/variants/Takenmake.js index 9549c2a9..d37edf6c 100644 --- a/client/src/variants/Takenmake.js +++ b/client/src/variants/Takenmake.js @@ -13,10 +13,9 @@ export class TakenmakeRules extends ChessRules { const L = this.lastMoveEnd.length; if (!asA && !!this.lastMoveEnd[L-1]) { asA = this.lastMoveEnd[L-1].p; - if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) { + if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) // A capture was played: wrong square return []; - } } let moves = []; const piece = this.getPiece(x, y); diff --git a/server/db/populate.sql b/server/db/populate.sql index 9b7842be..f9c9a332 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -47,6 +47,7 @@ insert or ignore into Variants (name, description) values ('Circular', 'Run forward'), ('Clorange', 'A Clockwork Orange'), ('Colorbound', 'The colorbound clobberers'), + ('Convert', 'Convert enemy pieces'), ('Coregal', 'Two royal pieces'), ('Coronation', 'Long live the Queen'), ('Crazyhouse', 'Captures reborn'),