From: Benjamin Auder Date: Tue, 16 Feb 2021 21:42:26 +0000 (+0100) Subject: Add Pandemonium X-Git-Url: https://git.auder.net/%7B%7B%20asset%28%27mixstore/css/store/table.css%27%29%20%7D%7D?a=commitdiff_plain;h=723262f9b77ed0f916e5b9fcbfbae5ddfe94925c;p=vchess.git Add Pandemonium --- diff --git a/client/public/images/pieces/Pandemonium/ba.svg b/client/public/images/pieces/Pandemonium/ba.svg new file mode 120000 index 00000000..c301d868 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/ba.svg @@ -0,0 +1 @@ +../bq.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bc.svg b/client/public/images/pieces/Pandemonium/bc.svg new file mode 120000 index 00000000..78e54f8d --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bc.svg @@ -0,0 +1 @@ +../Perfect/bs.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bd.svg b/client/public/images/pieces/Pandemonium/bd.svg new file mode 120000 index 00000000..09e6ea3e --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bd.svg @@ -0,0 +1 @@ +../Alice/bu.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bg.svg b/client/public/images/pieces/Pandemonium/bg.svg new file mode 120000 index 00000000..c301d868 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bg.svg @@ -0,0 +1 @@ +../bq.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bh.svg b/client/public/images/pieces/Pandemonium/bh.svg new file mode 120000 index 00000000..b30a26ad --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bh.svg @@ -0,0 +1 @@ +../Alice/bc.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bm.svg b/client/public/images/pieces/Pandemonium/bm.svg new file mode 120000 index 00000000..d3aaacf7 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bm.svg @@ -0,0 +1 @@ +../Perfect/be.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bs.svg b/client/public/images/pieces/Pandemonium/bs.svg new file mode 120000 index 00000000..1200186b --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bs.svg @@ -0,0 +1 @@ +../Alice/bo.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/bw.svg b/client/public/images/pieces/Pandemonium/bw.svg new file mode 120000 index 00000000..c301d868 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/bw.svg @@ -0,0 +1 @@ +../bq.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wa.svg b/client/public/images/pieces/Pandemonium/wa.svg new file mode 120000 index 00000000..aed155fe --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wa.svg @@ -0,0 +1 @@ +../wq.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wc.svg b/client/public/images/pieces/Pandemonium/wc.svg new file mode 120000 index 00000000..b0eb8b97 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wc.svg @@ -0,0 +1 @@ +../Perfect/ws.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wd.svg b/client/public/images/pieces/Pandemonium/wd.svg new file mode 120000 index 00000000..c1403b33 --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wd.svg @@ -0,0 +1 @@ +../Alice/wu.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wg.svg b/client/public/images/pieces/Pandemonium/wg.svg new file mode 120000 index 00000000..aed155fe --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wg.svg @@ -0,0 +1 @@ +../wq.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wh.svg b/client/public/images/pieces/Pandemonium/wh.svg new file mode 120000 index 00000000..d23af91d --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wh.svg @@ -0,0 +1 @@ +../Alice/wc.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/wm.svg b/client/public/images/pieces/Pandemonium/wm.svg new file mode 120000 index 00000000..f47feb0e --- /dev/null +++ b/client/public/images/pieces/Pandemonium/wm.svg @@ -0,0 +1 @@ +../Perfect/we.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/ws.svg b/client/public/images/pieces/Pandemonium/ws.svg new file mode 120000 index 00000000..4a85712d --- /dev/null +++ b/client/public/images/pieces/Pandemonium/ws.svg @@ -0,0 +1 @@ +../Alice/wo.svg \ No newline at end of file diff --git a/client/public/images/pieces/Pandemonium/ww.svg b/client/public/images/pieces/Pandemonium/ww.svg new file mode 120000 index 00000000..aed155fe --- /dev/null +++ b/client/public/images/pieces/Pandemonium/ww.svg @@ -0,0 +1 @@ +../wq.svg \ No newline at end of file diff --git a/client/src/translations/en.js b/client/src/translations/en.js index ca3b13a6..4d93adc7 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -269,6 +269,7 @@ export const translations = { "New fairy pieces": "New fairy pieces", "No paralyzed pieces": "No paralyzed pieces", "No-check mode": "No-check mode", + "Noise and confusion": "Noise and confusion", "Non-conformism and utopia": "Non-conformism and utopia", "Occupy the enemy palace": "Occupy the enemy palace", "Paralyzed pieces": "Paralyzed pieces", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 30095bfe..a07a5c12 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -269,6 +269,7 @@ export const translations = { "New fairy pieces": "Nuevas piezas magicas", "No paralyzed pieces": "No piezas paralizadas", "No-check mode": "Modo sin jaque", + "Noise and confusion": "Ruido y confusión", "Non-conformism and utopia": "No-conformismo y utopía", "Occupy the enemy palace": "Ocupar el palacio enemigo", "Paralyzed pieces": "Piezas paralizadas", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 0ebc8175..247b46f0 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -269,6 +269,7 @@ export const translations = { "New fairy pieces": "Nouvelles pièces féériques", "No paralyzed pieces": "Pas de pièces paralysées", "No-check mode": "Mode sans échec", + "Noise and confusion": "Bruit et confusion", "Non-conformism and utopia": "Non-conformisme et utopie", "Occupy the enemy palace": "Occuper le palais ennemi", "Paralyzed pieces": "Pièces paralysées", diff --git a/client/src/translations/rules/Pandemonium/en.pug b/client/src/translations/rules/Pandemonium/en.pug new file mode 100644 index 00000000..f5d518d6 --- /dev/null +++ b/client/src/translations/rules/Pandemonium/en.pug @@ -0,0 +1,63 @@ +p.boxed + | All pieces can promote. Captured units can be dropped later. + | 10x10 board. Some new pieces. + +figure.diagram-container + .diagram + | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR: + figcaption Initial deterministic position. + +p. + At the very first move, players may decide to swap positions of adjacent + knights and bishops. Either on both sides, or on one only. To bypass this + step (or end it after a first swap), move your king to the opponent's king. + +figure.showPieces.text-center + img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px") + img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px") + figcaption Cardinal = Bishop + Knight, Marshal = Rook + Knight. + +p. + Known pieces move as usual, with one exception: the pawn only promotes + to a queen (named "Gilding" in this game). Additionally, all pieces can + promote — except queen and king: +ul + li Rook promotes into Dragon = Rook + King. + li Knight promotes into Scepter = Knight + King. + li Bishop promotes into Horse = Bishop + King. + li Cardinal promotes into Queen (called "Whole" here). + li Marshal promotes into Queen (called "Apricot" here). +p. + All these promotions are optional. They are available after a move ending + at or starting from the last rank. + +p. + Each captured piece is first returned to its unpromoted version (if + applicable), and then added to a reserve. It can be dropped later + in place of a move. + +h3 Some details + +p. + Pawns can initially advance three squares. + On the third rank, they can still advance two squares. + A dropped pawn cannot give checkmate. + +p. + While castling, the king moves three squares lateraly. + Castling is possible even if the king or the rooks moved. + However, it can only be done once. + +p. + If after a move both kings are facing each other (on a rank or file) + without intervening pieces, then the player who made the move loses. + +h3 More information + +p + | See the + a(href="https://www.chessvariants.com/rules/pandemonium") + | chessvariants page + | . + +p Inventor: Daphne Snowmoon (2020). diff --git a/client/src/translations/rules/Pandemonium/es.pug b/client/src/translations/rules/Pandemonium/es.pug new file mode 100644 index 00000000..8aed7fe1 --- /dev/null +++ b/client/src/translations/rules/Pandemonium/es.pug @@ -0,0 +1,65 @@ +p.boxed + | Todas las piezas pueden promocionarse. Las unidades capturadas son + | en paracaídas más tarde. Tablero de ajedrez 10x10. Algunas piezas nuevas. + +figure.diagram-container + .diagram + | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR: + figcaption Posición inicial determinista. + +p. + En el primer movimiento, los jugadores pueden decidir intercambiar + posiciones de caballos y alfiles adyacentes. O en ambos lados o solo en uno. + Para omitir este paso (o finalizar después de un primer intercambio), + lleva a tu rey al rey contrario. + +figure.showPieces.text-center + img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px") + img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px") + figcaption Cardenal = Alfil + Caballo, Mariscal = Torre + Caballo. + +p. + Las piezas conocidas se mueven como de costumbre, con una excepción: + los peones solo se promueven en una reina (llamado "Gilding" en este juego). + Además, todas las piezas pueden promocionarse — excepto rey y reina: +ul + li Tower ascendido a Dragón = Torre + Rey. + li Caballo ascendido a Cetro = Cavalier + Rey. + li Alfil ascendido a Jinete = Alfil + Rey. + li Cardenal ascendido a Dama (llamado "Whole" aquí). + li Mariscal ascendió a Dama (aquí se llama "Apricot"). +p. + Todas estas promociones son opcionales. Están disponibles después de un + movimiento que termina o comienza en la última fila. + +p. + Cada pieza capturada se devuelve primero a su forma no promocionada (el caso + aplicable), luego se agrega a una reserva. Se puede lanzar en paracaídas + más tarde en lugar de una jugada. + +h3 Algunos detalles. + +p. + Los peones pueden avanzar inicialmente tres espacios. + En la tercera fila, todavía pueden avanzar dos espacios. + Un peón lanzado en paracaídas no puede dar jaque mate. + +p. + Durante el enroque, el rey mueve tres casillas hacia los lados. + El enroque es posible incluso si el rey o las torres se han movido. + Sin embargo, solo se puede realizar una vez. + +p. + Si después de una jugada los dos reyes se encuentran cara a cara (en fila + o una columna) sin piezas intermedias, entonces el jugador que hizo + el movimiento pierde. + +h3 Más información + +p + | Ver la + a(href="https://www.chessvariants.com/rules/pandemonium") + | página chessvariants + | . + +p Inventor: Daphne Snowmoon (2020). diff --git a/client/src/translations/rules/Pandemonium/fr.pug b/client/src/translations/rules/Pandemonium/fr.pug new file mode 100644 index 00000000..bdd6d3b5 --- /dev/null +++ b/client/src/translations/rules/Pandemonium/fr.pug @@ -0,0 +1,65 @@ +p.boxed + | Toutes les pièces peuvent être promues. Les unités capturées sont + | parachutées plus tard. Échiquier 10x10. Quelques nouvelles pièces. + +figure.diagram-container + .diagram + | fen:rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR: + figcaption Position initiale déterministe. + +p. + Au tout premier coup, les joueurs peuvent décider d'échanger les positions + de cavaliers et fous adjacents. Soit des deux côtés, soit d'un seul. + Pour sauter cette étape (ou terminer après un premier échange), + amenez votre roi sur le roi adverse. + +figure.showPieces.text-center + img(src="/images/pieces/Pandemonium/wc.svg" style="width:60px") + img(src="/images/pieces/Pandemonium/bm.svg" style="width:60px") + figcaption Cardinal = Fou + Cavalier, Maréchal = Tour + Cavalier. + +p. + Les pièces connues se déplacent comme d'habitude, à une exception près : + les pions ne se promeuvent qu'en dame (appelée "Gilding" dans ce jeu). + De plus, toutes les pièces peuvent se promouvoir — sauf roi et dame : +ul + li Tour promue en Dragon = Tour + Roi. + li Cavalier promu en Sceptre = Cavalier + Roi. + li Fou promu en Cheval = Fou + Roi. + li Cardinal promu en Dame (appelée "Whole" ici). + li Maréchal promu en Dame (appelée "Apricot" ici). +p. + Toutes ces promotions sont optionnelles. Elles sont disponibles après un + coup terminant ou commençant sur la dernière rangée. + +p. + Chaque pièce capturée est d'abord ramenée à sa forme non promue (le cas + échéant), puis ajoutée à une réserve. Elle peut être parachutée plus tard + à la place d'un coup. + +h3 Quelques détails. + +p. + Les pions peuvent initialement avancer de trois cases. + Sur la troisième rangée, ils peuvent encore avancer de deux cases. + Un pion parachuté ne peut pas donner échec et mat. + +p. + Lors du roque, le roi se déplace de trois cases latéralement. + Le roque est possible même si le roi ou les tours ont bougé. + Cependant, il ne peut être exécuté qu'une fois. + +p. + Si après un coup les deux rois se retrouvent face à face (sur une rangée + ou une colonne) sans pièces intermédiaires, alors le joueur ayant effectué + le coup perd. + +h3 Plus d'information + +p + | Voir la + a(href="https://www.chessvariants.com/rules/pandemonium") + | page chessvariants + | . + +p Inventeur : Daphne Snowmoon (2020). diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug index eed4c980..966f96f5 100644 --- a/client/src/translations/variants/en.pug +++ b/client/src/translations/variants/en.pug @@ -471,6 +471,7 @@ p. "Iceage", "Kingsmaker", "Magnetic", + "Pandemonium", "Refusal", "Relayup", "Rollerball", diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug index 01efa971..90196f68 100644 --- a/client/src/translations/variants/es.pug +++ b/client/src/translations/variants/es.pug @@ -481,6 +481,7 @@ p. "Iceage", "Kingsmaker", "Magnetic", + "Pandemonium", "Refusal", "Relayup", "Rollerball", diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug index 3566c882..7729aaf0 100644 --- a/client/src/translations/variants/fr.pug +++ b/client/src/translations/variants/fr.pug @@ -479,6 +479,7 @@ p. "Iceage", "Kingsmaker", "Magnetic", + "Pandemonium", "Refusal", "Relayup", "Rollerball", diff --git a/client/src/variants/Omega.js b/client/src/variants/Omega.js index a4276ed7..a688a2ea 100644 --- a/client/src/variants/Omega.js +++ b/client/src/variants/Omega.js @@ -309,18 +309,12 @@ export class OmegaRules extends ChessRules { return moves; } - addPawnMoves([x1, y1], [x2, y2], moves, promotions) { - let finalPieces = [V.PAWN]; + addPawnMoves([x1, y1], [x2, y2], moves) { const color = this.turn; const lastRank = (color == "w" ? 1 : V.size.x - 2); - if (x2 == lastRank) { - // promotions arg: special override for Hiddenqueen variant - if (!!promotions) finalPieces = promotions; - else if (!!V.PawnSpecs.promotions) finalPieces = V.PawnSpecs.promotions; - } - let tr = null; + const finalPieces = (x2 == lastRank ? V.PawnSpecs.promotions : [V.PAWN]); for (let piece of finalPieces) { - tr = (piece != V.PAWN ? { c: color, p: piece } : null); + const tr = (piece != V.PAWN ? { c: color, p: piece } : null); moves.push(this.getBasicMove([x1, y1], [x2, y2], tr)); } } diff --git a/client/src/variants/Pandemonium.js b/client/src/variants/Pandemonium.js new file mode 100644 index 00000000..34dc77e1 --- /dev/null +++ b/client/src/variants/Pandemonium.js @@ -0,0 +1,785 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class PandemoniumRules extends ChessRules { + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { + threeSquares: true, + promotions: [V.GILDING] + } + ); + } + + static get GILDING() { + return "g"; + } + + static get SCEPTER() { + return "s"; + } + + static get HORSE() { + return "h"; + } + + static get DRAGON() { + return "d"; + } + + static get CARDINAL() { + return "c"; + } + + static get WHOLE() { + return "w"; + } + + static get MARSHAL() { + return "m"; + } + + static get APRICOT() { + return "a"; + } + + static get PIECES() { + return ( + ChessRules.PIECES.concat([ + V.GILDING, V.SCEPTER, V.HORSE, V.DRAGON, + V.CARDINAL, V.WHOLE, V.MARSHAL, V.APRICOT]) + ); + } + + getPpath(b) { + const prefix = (ChessRules.PIECES.includes(b[1]) ? "" : "Pandemonium/"); + return prefix + b; + } + + static get size() { + return { x: 10, y: 10}; + } + + getColor(i, j) { + if (i >= V.size.x) return i == V.size.x ? "w" : "b"; + return this.board[i][j].charAt(0); + } + + getPiece(i, j) { + if (i >= V.size.x) return V.RESERVE_PIECES[j]; + return this.board[i][j].charAt(1); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + // Sub-turn is useful only at first move... + this.subTurn = 1; + // Also init reserves (used by the interface to show landable pieces) + const reserve = + V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10)); + this.reserve = { + w: { + [V.PAWN]: reserve[0], + [V.ROOK]: reserve[1], + [V.KNIGHT]: reserve[2], + [V.BISHOP]: reserve[3], + [V.QUEEN]: reserve[4], + [V.CARDINAL]: reserve[5], + [V.MARSHAL]: reserve[6], + }, + b: { + [V.PAWN]: reserve[7], + [V.ROOK]: reserve[8], + [V.KNIGHT]: reserve[9], + [V.BISHOP]: reserve[10], + [V.QUEEN]: reserve[11], + [V.CARDINAL]: reserve[12], + [V.MARSHAL]: reserve[13] + } + }; + } + + static IsGoodEnpassant(enpassant) { + if (enpassant != "-") { + const squares = enpassant.split(","); + if (squares.length > 2) return false; + for (let sq of squares) { + if (!sq.match(/[a-j0-9]/)) return false; + } + } + return true; + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // Check reserves + if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{14,14}$/)) + return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[5] } + ); + } + + getFen() { + return super.getFen() + " " + this.getReserveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getReserveFen(); + } + + getReserveFen() { + let counts = new Array(14); + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]]; + counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]]; + } + return counts.join(""); + } + + setFlags(fenflags) { + // white a-castle, h-castle, king pos, then same for black. + this.castleFlags = { w: [-1, -1, -1], b: [-1, -1, -1] }; + for (let i = 0; i < 6; i++) { + this.castleFlags[i < 3 ? "w" : "b"][i % 3] = + V.ColumnToCoord(fenflags.charAt(i)); + } + } + + static GenRandInitFen(randomness) { + // No randomization here for now (but initial setup choice) + return ( + "rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR " + + "w 0 ajeaje - 00000000000000" + ); + // TODO later: randomization too --> 2 bishops, not next to each other. + // then knights next to bishops. Then other pieces (...). + } + + getEnpassantFen() { + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return "-"; //no en-passant + let res = ""; + this.epSquares[L - 1].forEach(sq => { + res += V.CoordsToSquare(sq) + ","; + }); + return res.slice(0, -1); //remove last comma + } + + getEpSquare(moveOrSquare) { + if (!moveOrSquare) return undefined; + if (typeof moveOrSquare === "string") { + const square = moveOrSquare; + if (square == "-") return undefined; + let res = []; + square.split(",").forEach(sq => { + res.push(V.SquareToCoords(sq)); + }); + return res; + } + // Argument is a move: + const move = moveOrSquare; + const [sx, sy, ex] = [move.start.x, move.start.y, move.end.x]; + if (this.getPiece(sx, sy) == V.PAWN && Math.abs(sx - ex) >= 2) { + const step = (ex - sx) / Math.abs(ex - sx); + let res = [{ + x: sx + step, + y: sy + }]; + if (sx + 2 * step != ex) { + // 3-squares jump + res.push({ + x: sx + 2 * step, + y: sy + }); + } + return res; + } + return undefined; //default + } + + getReservePpath(index, color) { + const p = V.RESERVE_PIECES[index]; + const prefix = (ChessRules.PIECES.includes(p) ? "" : "Pandemonium/"); + return prefix + color + p;; + } + + // Ordering on reserve pieces + static get RESERVE_PIECES() { + return ( + [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.CARDINAL, V.MARSHAL] + ); + } + + getReserveMoves([x, y]) { + const color = this.turn; + const p = V.RESERVE_PIECES[y]; + if (this.reserve[color][p] == 0) return []; + const bounds = (p == V.PAWN ? [1, V.size.x - 1] : [0, V.size.x]); + let moves = []; + for (let i = bounds[0]; i < bounds[1]; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] == V.EMPTY) { + let mv = new Move({ + appear: [ + new PiPo({ + x: i, + y: j, + c: color, + p: p + }) + ], + vanish: [], + start: { x: x, y: y }, //a bit artificial... + end: { x: i, y: j } + }); + if (p == V.PAWN) { + // Do not drop on checkmate: + this.play(mv); + const res = ( + this.underCheck(oppCol) && !this.atLeastOneMove("noReserve") + ); + this.undo(mv); + if (res) continue; + } + moves.push(mv); + } + } + } + return moves; + } + + static get PromoteMap() { + return { + r: 'd', + n: 's', + b: 'h', + c: 'w', + m: 'a' + }; + } + + getPotentialMovesFrom([x, y]) { + const c = this.getColor(x, y); + const oppCol = V.GetOppCol(c); + if (this.movesCount <= 1) { + if (this.kingPos[c][0] == x && this.kingPos[c][1] == y) { + // Pass (if setup is ok) + return [ + new Move({ + appear: [], + vanish: [], + start: { x: this.kingPos[c][0], y: this.kingPos[c][1] }, + end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] } + }) + ]; + } + const firstRank = (this.movesCount == 0 ? 9 : 0); + // TODO: initDestFile currently hardcoded for deterministic setup + const initDestFile = new Map([[1, 2], [8, 7]]); + // Only option is knight / bishop swap: + if (x == firstRank && !!initDestFile.get(y)) { + const destFile = initDestFile.get(y); + return [ + new Move({ + appear: [ + new PiPo({ + x: x, + y: destFile, + c: c, + p: V.KNIGHT + }), + new PiPo({ + x: x, + y: y, + c: c, + p: V.BISHOP + }) + ], + vanish: [ + new PiPo({ + x: x, + y: y, + c: c, + p: V.KNIGHT + }), + new PiPo({ + x: x, + y: destFile, + c: c, + p: V.BISHOP + }) + ], + start: { x: x, y: y }, + end: { x: x, y: destFile } + }) + ]; + } + return []; + } + // Normal move (after initial setup) + if (x >= V.size.x) return this.getReserveMoves(x, y); + const p = this.getPiece(x, y); + const sq = [x, y]; + let moves = []; + if (ChessRules.PIECES.includes(p)) + moves = super.getPotentialMovesFrom(sq); + if ([V.GILDING, V.APRICOT, V.WHOLE].includes(p)) + moves = super.getPotentialQueenMoves(sq); + switch (p) { + case V.SCEPTER: + moves = this.getPotentialScepterMoves(sq); + break; + case V.HORSE: + moves = this.getPotentialHorseMoves(sq); + break; + case V.DRAGON: + moves = this.getPotentialDragonMoves(sq); + break; + case V.CARDINAL: + moves = this.getPotentialCardinalMoves(sq); + break; + case V.MARSHAL: + moves = this.getPotentialMarshalMoves(sq); + break; + } + // Maybe apply promotions: + if (Object.keys(V.PromoteMap).includes(p)) { + const promoted = V.PromoteMap[p]; + const lastRank = (c == 'w' ? 0 : 9); + let promotions = []; + moves.forEach(m => { + if (m.start.x == lastRank || m.end.x == lastRank) { + let pMove = JSON.parse(JSON.stringify(m)); + pMove.appear[0].p = promoted; + promotions.push(pMove); + } + }); + Array.prototype.push.apply(moves, promotions); + } + return moves; + } + + getPotentialPawnMoves([x, y]) { + const color = this.turn; + const shiftX = V.PawnSpecs.directions[color]; + let moves = []; + if (this.board[x + shiftX][y] == V.EMPTY) { + this.addPawnMoves([x, y], [x + shiftX, y], moves); + if ((color == 'w' && x >= V.size.x - 3) || (color == 'b' && x <= 3)) { + if (this.board[x + 2 * shiftX][y] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y])); + if ( + ( + (color == 'w' && x >= V.size.x - 2) || + (color == 'b' && x <= 2) + ) + && + this.board[x + 3 * shiftX][y] == V.EMPTY + ) { + moves.push(this.getBasicMove([x, y], [x + 3 * shiftX, y])); + } + } + } + } + for (let shiftY of [-1, 1]) { + if (y + shiftY >= 0 && y + shiftY < V.size.y) { + if ( + this.board[x + shiftX][y + shiftY] != V.EMPTY && + this.canTake([x, y], [x + shiftX, y + shiftY]) + ) { + this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves); + } + } + } + Array.prototype.push.apply( + moves, + this.getEnpassantCaptures([x, y], shiftX) + ); + return moves; + } + + getPotentialMarshalMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep") + ); + } + + getPotentialCardinalMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep") + ); + } + + getPotentialScepterMoves(sq) { + const steps = + V.steps[V.KNIGHT].concat(V.steps[V.BISHOP]).concat(V.steps[V.ROOK]); + return this.getSlideNJumpMoves(sq, steps, "oneStep"); + } + + getPotentialHorseMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.ROOK], "oneStep")); + } + + getPotentialDragonMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat( + this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], "oneStep")); + } + + getEnpassantCaptures([x, y], shiftX) { + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; + let moves = []; + if (!!epSquare) { + for (let epsq of epSquare) { + // TODO: some redundant checks + if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) { + let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]); + // WARNING: the captured pawn may be diagonally behind us, + // if it's a 3-squares jump and we take on 1st passing square + const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX; + enpassantMove.vanish.push({ + x: px, + y: epsq.y, + p: "p", + c: this.getColor(px, epsq.y) + }); + moves.push(enpassantMove); + } + } + } + return moves; + } + + getPotentialKingMoves(sq) { + // Initialize with normal moves + let moves = this.getSlideNJumpMoves( + sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ); + const c = this.turn; + if ( + this.castleFlags[c][0] < V.size.y || + this.castleFlags[c][1] < V.size.y + ) { + moves = moves.concat(this.getCastleMoves(sq)); + } + return moves; + } + + getCastleMoves([x, y]) { + const c = this.getColor(x, y); + if ( + ((c == 'w' && x == 9) || (c == 'b' && x == 0)) && + y == this.castleFlags[c][2] + ) { + const finalSquares = [ + [1, 2], + [7, 6] + ]; + return super.getCastleMoves([x, y], finalSquares, false, [V.ROOK]); + } + return []; + } + + isAttacked(sq, color) { + return ( + this.isAttackedByPawn(sq, color) || + this.isAttackedByRook(sq, color) || + this.isAttackedByKnight(sq, color) || + this.isAttackedByBishop(sq, color) || + this.isAttackedByKing(sq, color) || + this.isAttackedByQueens(sq, color) || + this.isAttackedByScepter(sq, color) || + this.isAttackedByDragon(sq, color) || + this.isAttackedByHorse(sq, color) || + this.isAttackedByMarshal(sq, color) || + this.isAttackedByCardinal(sq, color) + ); + } + + isAttackedByQueens([x, y], color) { + // pieces: because queen = gilding = whole = apricot + const pieces = [V.QUEEN, V.GILDING, V.WHOLE, V.APRICOT]; + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + 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) { + rx += step[0]; + ry += step[1]; + } + if ( + V.OnBoard(rx, ry) && + this.board[rx][ry] != V.EMPTY && + pieces.includes(this.getPiece(rx, ry)) && + this.getColor(rx, ry) == color + ) { + return true; + } + } + return false; + } + + isAttackedByScepter(sq, color) { + const steps = + V.steps[V.KNIGHT].concat(V.steps[V.ROOK]).concat(V.steps[V.BISHOP]); + return ( + super.isAttackedBySlideNJump(sq, color, steps, V.SCEPTER, "oneStep") + ); + } + + isAttackedByHorse(sq, color) { + return ( + super.isAttackedBySlideNJump(sq, color, V.steps[V.BISHOP], V.HORSE) || + super.isAttackedBySlideNJump( + sq, color, V.steps[V.ROOK], V.HORSE, "oneStep") + ); + } + + isAttackedByDragon(sq, color) { + return ( + super.isAttackedBySlideNJump(sq, color, V.steps[V.ROOK], V.DRAGON) || + super.isAttackedBySlideNJump( + sq, color, V.steps[V.BISHOP], V.DRAGON, "oneStep") + ); + } + + isAttackedByMarshal(sq, color) { + return ( + super.isAttackedBySlideNJump(sq, color, V.MARSHAL, V.steps[V.ROOK]) || + super.isAttackedBySlideNJump( + sq, + color, + V.MARSHAL, + V.steps[V.KNIGHT], + "oneStep" + ) + ); + } + + isAttackedByCardinal(sq, color) { + return ( + super.isAttackedBySlideNJump(sq, color, V.CARDINAL, V.steps[V.BISHOP]) || + super.isAttackedBySlideNJump( + sq, + color, + V.CARDINAL, + V.steps[V.KNIGHT], + "oneStep" + ) + ); + } + + getAllValidMoves() { + let moves = super.getAllPotentialMoves(); + const color = this.turn; + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + moves = moves.concat( + this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i]) + ); + } + return this.filterValid(moves); + } + + atLeastOneMove(noReserve) { + if (!super.atLeastOneMove()) { + if (!noReserve) { + // Search one reserve move + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + let moves = this.filterValid( + this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i]) + ); + if (moves.length > 0) return true; + } + } + return false; + } + return true; + } + + // Reverse 'PromoteMap' + static get P_CORRESPONDANCES() { + return { + d: 'r', + s: 'n', + h: 'b', + w: 'c', + a: 'm' + }; + } + + static MayDecode(piece) { + if (Object.keys(V.P_CORRESPONDANCES).includes(piece)) + return V.P_CORRESPONDANCES[piece]; + return piece; + } + + play(move) { + move.subTurn = this.subTurn; //much easier + if (this.movesCount >= 2 || this.subTurn == 2 || move.vanish.length == 0) { + this.turn = V.GetOppCol(this.turn); + this.subTurn = 1; + this.movesCount++; + } + else this.subTurn = 2; + move.flags = JSON.stringify(this.aggregateFlags()); + this.epSquares.push(this.getEpSquare(move)); + V.PlayOnBoard(this.board, move); + this.postPlay(move); + } + + updateCastleFlags(move, piece) { + if (move.appear.length == 2) { + // Castling (only move which disable flags) + this.castleFlags[move.appear[0].c][0] = 10; + this.castleFlags[move.appear[0].c][1] = 10; + } + } + + postPlay(move) { + if (move.vanish.length == 0 && move.appear.length == 0) return; + super.postPlay(move); + const color = move.appear[0].c; + if (move.vanish.length == 0) + // Drop unpromoted piece: + this.reserve[color][move.appear[0].p]--; + else if (move.vanish.length == 2) + // May capture a promoted piece: + this.reserve[color][V.MayDecode(move.vanish[1].p)]++; + } + + undo(move) { + this.epSquares.pop(); + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + if (this.movesCount >= 2 || this.subTurn == 1 || move.vanish.length == 0) { + this.turn = V.GetOppCol(this.turn); + this.movesCount--; + } + this.subTurn = move.subTurn; + this.postUndo(move); + } + + postUndo(move) { + if (move.vanish.length == 0 && move.appear.length == 0) return; + super.postUndo(move); + const color = move.appear[0].c; + if (move.vanish.length == 0) + this.reserve[color][move.appear[0].p]++; + else if (move.vanish.length == 2) + this.reserve[color][V.MayDecode(move.vanish[1].p)]--; + } + + getCurrentScore() { + const c = this.turn, + oppCol = V.GetOppCol(this.turn); + let facingKings = false; + if ( + this.kingPos[c][0] == this.kingPos[oppCol][0] || + this.kingPos[c][1] == this.kingPos[oppCol][1] + ) { + facingKings = true; + let step = [ + this.kingPos[oppCol][0] - this.kingPos[c][0], + this.kingPos[oppCol][1] - this.kingPos[c][1] + ]; + if (step[0] != 0) step[0] /= Math.abs(step[0]); + else step[1] /= Math.abs(step[1]); + let [x, y] = + [ this.kingPos[c][0] + step[0], this.kingPos[c][1] + step[1] ]; + while (x != this.kingPos[oppCol][0] || y != this.kingPos[oppCol][1]) { + if (this.board[x][y] != V.EMPTY) { + facingKings = false; + break; + } + x += step[0]; + y += step[1]; + } + } + if (facingKings) return (c == "w" ? "1-0" : "0-1"); + if (!this.atLeastOneMove()) return (c == "w" ? "0-1" : "1-0"); + return "*"; + } + + static get VALUES() { + return Object.assign( + { + g: 9, + s: 5, + h: 6, + d: 7, + c: 7, + w: 9, + m: 8, + a: 9 + }, + ChessRules.VALUES + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + + getComputerMove() { + if (this.movesCount <= 1) { + // Special case: swap and pass at random + const moves1 = this.getAllValidMoves(); + const m1 = moves1[randInt(moves1.length)]; + this.play(m1); + if (m1.vanish.length == 0) { + this.undo(m1); + return m1; + } + const moves2 = this.getAllValidMoves(); + const m2 = moves2[randInt(moves2.length)]; + this.undo(m1); + return [m1, m2]; + } + return super.getComputerMove(); + } + + evalPosition() { + let evaluation = super.evalPosition(); + // Add reserves: + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + const p = V.RESERVE_PIECES[i]; + evaluation += this.reserve["w"][p] * V.VALUES[p]; + evaluation -= this.reserve["b"][p] * V.VALUES[p]; + } + return evaluation; + } + + getNotation(move) { + if (move.vanish.length == 0) { + if (move.appear.length == 0) return "pass"; + const pieceName = + (move.appear[0].p == V.PAWN ? "" : move.appear[0].p.toUpperCase()); + return pieceName + "@" + V.CoordsToSquare(move.end); + } + if (move.appear.length == 2) { + if (move.appear[0].p != V.KING) + return V.CoordsToSquare(move.start) + "S" + V.CoordsToSquare(move.end); + return (move.end.y < move.start.y ? "0-0" : "0-0-0"); + } + let notation = super.getNotation(move); + if (move.vanish[0].p != V.PAWN && move.appear[0].p != move.vanish[0].p) + // Add promotion indication: + notation += "=" + move.appear[0].p.toUpperCase(); + return notation; + } + +}; diff --git a/client/src/variants/Shogi.js b/client/src/variants/Shogi.js index 9104ed92..ac98835a 100644 --- a/client/src/variants/Shogi.js +++ b/client/src/variants/Shogi.js @@ -282,7 +282,9 @@ export class ShogiRules extends ChessRules { if (p == V.PAWN) { // Do not drop on checkmate: this.play(mv); - const res = (this.underCheck(oppCol) && !this.atLeastOneMove()); + const res = ( + this.underCheck(oppCol) && !this.atLeastOneMove("noReserve") + ); this.undo(mv); if (res) continue; } @@ -548,14 +550,16 @@ export class ShogiRules extends ChessRules { return this.filterValid(moves); } - atLeastOneMove() { + atLeastOneMove(noReserve) { if (!super.atLeastOneMove()) { - // Search one reserve move - for (let i = 0; i < V.RESERVE_PIECES.length; i++) { - let moves = this.filterValid( - this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i]) - ); - if (moves.length > 0) return true; + if (!noReserve) { + // Search one reserve move + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + let moves = this.filterValid( + this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i]) + ); + if (moves.length > 0) return true; + } } return false; } diff --git a/server/db/populate.sql b/server/db/populate.sql index f9c9a332..c08dc9e3 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -116,6 +116,7 @@ insert or ignore into Variants (name, description) values ('Pacifist1', 'Convert & support (v1)'), ('Pacifist2', 'Convert & support (v2)'), ('Pacosako', 'Dance with the King'), + ('Pandemonium', 'Noise and confusion'), ('Parachute', 'Landing on the board'), ('Pawnmassacre', 'Pieces upside down'), ('Pawns', 'Reach the last rank (v1)'),