From: Benjamin Auder Date: Wed, 11 Mar 2020 17:05:40 +0000 (+0100) Subject: Work on Eightpieces draft. Fix Grand deterministic initial position X-Git-Url: https://git.auder.net/doc/current/bundles/framework/css/%7B%7B%20pkg.url%20%7D%7D?a=commitdiff_plain;h=90e814b6717b1ba932bba0e52958f54f814a2503;p=vchess.git Work on Eightpieces draft. Fix Grand deterministic initial position --- diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 2bfdc997..1c5eabca 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -428,6 +428,7 @@ export const ChessRules = class ChessRules { this.INIT_COL_ROOK = { w: [-1, -1], b: [-1, -1] }; this.kingPos = { w: [-1, -1], b: [-1, -1] }; //squares of white and black king const fenRows = V.ParseFen(fen).position.split("/"); + const startRow = { 'w': V.size.x - 1, 'b': 0 }; for (let i = 0; i < fenRows.length; i++) { let k = 0; //column index on board for (let j = 0; j < fenRows[i].length; j++) { @@ -441,12 +442,16 @@ export const ChessRules = class ChessRules { this.INIT_COL_KING["w"] = k; break; case "r": - if (this.INIT_COL_ROOK["b"][0] < 0) this.INIT_COL_ROOK["b"][0] = k; - else this.INIT_COL_ROOK["b"][1] = k; + if (i == startRow['b']) { + if (this.INIT_COL_ROOK["b"][0] < 0) this.INIT_COL_ROOK["b"][0] = k; + else this.INIT_COL_ROOK["b"][1] = k; + } break; case "R": - if (this.INIT_COL_ROOK["w"][0] < 0) this.INIT_COL_ROOK["w"][0] = k; - else this.INIT_COL_ROOK["w"][1] = k; + if (i == startRow['w']) { + if (this.INIT_COL_ROOK["w"][0] < 0) this.INIT_COL_ROOK["w"][0] = k; + else this.INIT_COL_ROOK["w"][1] = k; + } break; default: { const num = parseInt(fenRows[i].charAt(j)); diff --git a/client/src/translations/en.js b/client/src/translations/en.js index c03a0976..2e69f302 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -162,6 +162,7 @@ export const translations = { "Captures reborn": "Captures reborn", "Change colors": "Change colors", "Dangerous collisions": "Dangerous collisions", + "Each piece is unique": "Each piece is unique", "Exotic captures": "Exotic captures", "Explosive captures": "Explosive captures", "In the shadow": "In the shadow", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 5a4e8dff..944cbff9 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -162,6 +162,7 @@ export const translations = { "Captures reborn": "Las capturas renacen", "Change colors": "Cambiar colores", "Dangerous collisions": "Colisiones peligrosas", + "Each piece is unique": "Cada pieza es única", "Exotic captures": "Capturas exóticas", "Explosive captures": "Capturas explosivas", "In the shadow": "En la sombra", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index a685d55c..5e3108be 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -162,6 +162,7 @@ export const translations = { "Captures reborn": "Les captures renaissent", "Change colors": "Changer les couleurs", "Dangerous collisions": "Collisions dangeureuses", + "Each piece is unique": "Chaque pièce est unique", "Exotic captures": "Captures exotiques", "Explosive captures": "Captures explosives", "In the shadow": "Dans l'ombre", diff --git a/client/src/translations/rules/Eightpieces/en.pug b/client/src/translations/rules/Eightpieces/en.pug new file mode 100644 index 00000000..db673e5a --- /dev/null +++ b/client/src/translations/rules/Eightpieces/en.pug @@ -0,0 +1,34 @@ +p.boxed + | Pawns start on the 7th rank. Move a knight to promote them. + +p. + ...Only the initial position changes, but this makes a huge difference. + In particular, castling would be rather pointless so it's disabled here. + En-passant captures are impossible because all pawns already reached 7th rank. + +h3 About the initial position + +p. + Since truly random start can allow a mate in 3 with a knight, + the kings have at least one knight neighbor in the initial position. + This allows to move free out of potential check from the very beginning. + +p. + A less constraining condition would be to require the two knights to stand on + two squares of different colors, but it's not enough as proved by the + following diagram. + White can mate in 3: 1.Nc6 followed by Nb4 threatening both a2 and d3. + +figure.diagram-container + .diagram + | fen:RBN1BRRQ/PPPPPPP/8/4n3/8/8/Nppppppp/brkbqr1n: + figcaption After 1.Nc6 Nf3 2.Nb4 Ne5 (covers d3 but not a2) 3.Nxa2# + +p Note: in the standard initial position, kings and knights are not neighbors. + +h3 Source + +p + | See for example the + a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") Upside down chess + |  page on chessvariants.com. diff --git a/client/src/translations/rules/Eightpieces/es.pug b/client/src/translations/rules/Eightpieces/es.pug new file mode 100644 index 00000000..80d76fdc --- /dev/null +++ b/client/src/translations/rules/Eightpieces/es.pug @@ -0,0 +1,36 @@ +p.boxed + | Las piezas comienzan en la séptima fila. Mueve un caballo para promocionar uno. + +p. + ...Solo cambia la posición inicial, pero es una gran diferencia. + En particular, el enroque no sería interesante y, por lo tanto, está deshabilitado aquí. + Las capturas en passant también son imposibles porque todos los peones + ya están en la 7ma fila. + +h3 Acerca de la posición inicial + +p. + Una disposición de piezas completamente al azar puede permitir un mate en 3 + con un caballo: es por eso que el rey siempre está al lado de a menos + un caballo al comienzo del juego. Esto le permite liberarse de jaques tan pronto + primer movimiento. + +p. + Para ilustrar este fenómeno, los blancos pueden mate en 3 en la posición + del siguiente diagrama: 1.Na6 seguido de Nc5 luego Nd3#. + Si el caballo negro estuviera en g1, una defensa sería posible con Nf3-Ne5; + pero con un caballo al lado del rey se ofrecen más opciones. + +figure.diagram-container + .diagram + | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: + figcaption Posición inicial estándar después de 1.Na6 + +p Nota: en la posición inicial habitual, los reyes y los caballos no son vecinos. + +h3 Fuente + +p + | Ver por ejemplo la página + a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") ajedrez al revés + |  en chessvariants.com. diff --git a/client/src/translations/rules/Eightpieces/fr.pug b/client/src/translations/rules/Eightpieces/fr.pug new file mode 100644 index 00000000..0631994b --- /dev/null +++ b/client/src/translations/rules/Eightpieces/fr.pug @@ -0,0 +1,36 @@ +p.boxed + | Les pions démarrent sur la 7eme rangée. Déplacez un cavalier pour en promouvoir un. + +p. + ...Seule la position de départ change, mais c'est une énorme différence. + En particulier, le roque serait sans intérêt et est donc désactivé ici. + Les captures en passant sont également impossible car tous les pions + sont déjà sur la 7eme rangée. + +h3 Au sujet de la position initiale + +p. + Un placement complètement aléatoire des pièces peut permettre un mat en 3 + à l'aide d'un cavalier : c'est pourquoi le roi est toujours à côté d'au moins + un cavalier en début de partie. Cela permet de se dégager des échecs dès le + premier coup. + +p. + Pour illustrer ce phénomène, les blancs peuvent mater en 3 dans la position + du diagramme suivant : 1.Na6 suivi de Nc5 puis Nd3#. + Si le cavalier noir était en g1 une défense serait possible par Nf3-Ne5 ; + mais avec un cavalier voisin du roi plus d'options sont offertes. + +figure.diagram-container + .diagram + | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: + figcaption Standard initial position after 1.Na6 + +p Note : dans la position initiale habituelle, rois et cavaliers ne sont pas voisins. + +h3 Source + +p + | Voir par exemple la page + a(href="https://www.chessvariants.com/diffsetup.dir/upside.html") Échecs Upside down + |  sur chessvariants.com. diff --git a/client/src/translations/rules/Upsidedown/en.pug b/client/src/translations/rules/Upsidedown/en.pug index d6af4b1e..db673e5a 100644 --- a/client/src/translations/rules/Upsidedown/en.pug +++ b/client/src/translations/rules/Upsidedown/en.pug @@ -24,6 +24,8 @@ figure.diagram-container | fen:RBN1BRRQ/PPPPPPP/8/4n3/8/8/Nppppppp/brkbqr1n: figcaption After 1.Nc6 Nf3 2.Nb4 Ne5 (covers d3 but not a2) 3.Nxa2# +p Note: in the standard initial position, kings and knights are not neighbors. + h3 Source p diff --git a/client/src/translations/rules/Upsidedown/es.pug b/client/src/translations/rules/Upsidedown/es.pug index 9d9dad8e..80d76fdc 100644 --- a/client/src/translations/rules/Upsidedown/es.pug +++ b/client/src/translations/rules/Upsidedown/es.pug @@ -26,6 +26,8 @@ figure.diagram-container | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: figcaption Posición inicial estándar después de 1.Na6 +p Nota: en la posición inicial habitual, los reyes y los caballos no son vecinos. + h3 Fuente p diff --git a/client/src/translations/rules/Upsidedown/fr.pug b/client/src/translations/rules/Upsidedown/fr.pug index c1812a83..0631994b 100644 --- a/client/src/translations/rules/Upsidedown/fr.pug +++ b/client/src/translations/rules/Upsidedown/fr.pug @@ -26,6 +26,8 @@ figure.diagram-container | fen:R1BQKBNR/PPPPPPP/N7/8/8/8/pppppppp/rnbqkbrn c5,d3: figcaption Standard initial position after 1.Na6 +p Note : dans la position initiale habituelle, rois et cavaliers ne sont pas voisins. + h3 Source p diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index 03d7e418..4f9ff632 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -41,16 +41,15 @@ export const VariantRules = class CrazyhouseRules extends ChessRules { getFen() { return ( - super.getFen() + " " + this.getReserveFen() + " " + this.getPromotedFen() + super.getFen() + " " + + this.getReserveFen() + " " + + this.getPromotedFen() ); } getFenForRepeat() { return ( - this.getBaseFen() + "_" + - this.getTurnFen() + "_" + - this.getFlagsFen() + "_" + - this.getEnpassantFen() + "_" + + super.getFenForRepeat() + "_" + this.getReserveFen() + "_" + this.getPromotedFen() ); diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js index 8fe89359..8240dd2a 100644 --- a/client/src/variants/Eightpieces.js +++ b/client/src/variants/Eightpieces.js @@ -38,10 +38,33 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } getPpath(b) { - return ( - ([V.JAILER, V.SENTRY, V.LANCER].includes(b[1]) - ? "Eightpieces/" : "") + b - ); + if ([V.JAILER, V.SENTRY].concat(Object.keys(V.LANCER_DIRS)).includes(b[1])) + return "Eightpieces/" + b; + return b; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign(ChessRules.ParseFen(fen), { + sentrypath: fenParts[5] + }); + } + + getFen() { + return super.getFen() + " " + this.getSentrypathFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getSentrypathFen(); + } + + getSentrypathFen() { + const L = this.sentryPath.length; + if (!this.sentryPath[L-1]) return "-"; + let res = ""; + this.sentryPath[L-1].forEach(coords => + res += V.CoordsToSquare(coords) + ","); + return res.slice(0, -1); } setOtherVariables(fen) { @@ -49,7 +72,15 @@ export const VariantRules = class EightpiecesRules extends ChessRules { // subTurn == 2 only when a sentry moved, and is about to push something this.subTurn = 1; // Stack pieces' forbidden squares after a sentry move at each turn - this.sentryPath = []; + const parsedFen = V.ParseFen(fen); + if (parsedFen.sentrypath == "-") this.sentryPath = [null]; + else { + this.sentryPath = [ + parsedFen.sentrypath.split(",").map(sq => { + return V.SquareToCoords(sq); + }) + ]; + } } canTake([x1,y1], [x2, y2]) { @@ -61,23 +92,44 @@ export const VariantRules = class EightpiecesRules extends ChessRules { static GenRandInitFen(randomness) { // TODO: special conditions + return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -"; } - // TODO: rook + jailer + // Scan kings, rooks and jailers scanKingsRooks(fen) { + this.INIT_COL_KING = { w: -1, b: -1 }; + this.INIT_COL_ROOK = { w: -1, b: -1 }; + this.INIT_COL_JAILER = { w: -1, b: -1 }; this.kingPos = { w: [-1, -1], b: [-1, -1] }; const fenRows = V.ParseFen(fen).position.split("/"); + const startRow = { 'w': V.size.x - 1, 'b': 0 }; for (let i = 0; i < fenRows.length; i++) { - let k = 0; //column index on board + let k = 0; for (let j = 0; j < fenRows[i].length; j++) { switch (fenRows[i].charAt(j)) { case "k": - case "l": this.kingPos["b"] = [i, k]; + this.INIT_COL_KING["b"] = k; break; case "K": - case "L": this.kingPos["w"] = [i, k]; + this.INIT_COL_KING["w"] = k; + break; + case "r": + if (i == startRow['b'] && this.INIT_COL_ROOK["b"] < 0) + this.INIT_COL_ROOK["b"] = k; + break; + case "R": + if (i == startRow['w'] && this.INIT_COL_ROOK["w"] < 0) + this.INIT_COL_ROOK["w"] = k; + break; + case "j": + if (i == startRow['b'] && this.INIT_COL_JAILER["b"] < 0) + this.INIT_COL_JAILER["b"] = k; + break; + case "J": + if (i == startRow['w'] && this.INIT_COL_JAILER["w"] < 0) + this.INIT_COL_JAILER["w"] = k; break; default: { const num = parseInt(fenRows[i].charAt(j)); @@ -90,23 +142,137 @@ export const VariantRules = class EightpiecesRules extends ChessRules { } getPotentialMovesFrom([x,y]) { - // if subTurn == 2, allow only + // if subturn == 1, normal situation, allow moves except walking back on sentryPath, + // if last element isn't null in sentryPath array + // if subTurn == 2, allow only the end of the path (occupied by a piece) to move + // + // TODO: special pass move: take jailer with king, only if king immobilized + // Move(appear:[], vanish:[], start == king and end = jailer (for animation)) + // + // TODO: post-processing if sentryPath forbid some moves. + // + add all lancer possible orientations + // (except if just after a push: allow all movements from init square then) + // Test if last sentryPath ends at our position: if yes, OK } - // getPotentialMoves, isAttacked: TODO - getPotentialCastleMoves(sq) { //TODO: adapt, with jailer + // Adapted: castle with jailer possible + getCastleMoves([x, y]) { + const c = this.getColor(x, y); + const firstRank = (c == "w" ? V.size.x - 1 : 0); + if (x != firstRank || y != this.INIT_COL_KING[c]) + return []; + + const oppCol = V.GetOppCol(c); + let moves = []; + let i = 0; + // King, then rook or jailer: + const finalSquares = [ + [2, 3], + [V.size.y - 2, V.size.y - 3] + ]; + castlingCheck: for ( + let castleSide = 0; + castleSide < 2; + castleSide++ + ) { + if (!this.castleFlags[c][castleSide]) continue; + // Rook (or jailer) and king are on initial position + + const finDist = finalSquares[castleSide][0] - y; + let step = finDist / Math.max(1, Math.abs(finDist)); + i = y; + do { + if ( + this.isAttacked([x, i], [oppCol]) || + (this.board[x][i] != V.EMPTY && + (this.getColor(x, i) != c || + ![V.KING, V.ROOK].includes(this.getPiece(x, i)))) + ) { + continue castlingCheck; + } + i += step; + } while (i != finalSquares[castleSide][0]); + + step = castleSide == 0 ? -1 : 1; + const rookOrJailerPos = + castleSide == 0 + ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]) + : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]); + for (i = y + step; i != rookOrJailerPos; i += step) + if (this.board[x][i] != V.EMPTY) continue castlingCheck; + + // Nothing on final squares, except maybe king and castling rook or jailer? + for (i = 0; i < 2; i++) { + if ( + this.board[x][finalSquares[castleSide][i]] != V.EMPTY && + this.getPiece(x, finalSquares[castleSide][i]) != V.KING && + finalSquares[castleSide][i] != rookOrJailerPos + ) { + continue castlingCheck; + } + } + + // If this code is reached, castle is valid + const castlingPiece = this.getPiece(firstRank, rookOrJailerPos); + moves.push( + new Move({ + appear: [ + new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }), + new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c }) + ], + vanish: [ + new PiPo({ x: x, y: y, p: V.KING, c: c }), + new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c }) + ], + end: + Math.abs(y - rookOrJailerPos) <= 2 + ? { x: x, y: rookOrJailerPos } + : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) } + }) + ); + } + + return moves; } updateVariables(move) { - // TODO: stack sentryPath if subTurn == 2 --> all squares between move.start et move.end, sauf si c'est un pion + super.updateVariables(move); + if (this.subTurn == 2) { + // A piece is pushed: + // TODO: push array of squares between start and end of move, included + // (except if it's a pawn) + this.sentryPath.push([]); //TODO + this.subTurn = 1; + } else { + if (move.appear.length == 0 && move.vanish.length == 0) { + // Special sentry move: subTurn <- 2, and then move pushed piece + this.subTurn = 2; + } + // Else: normal move. + } } - // TODO: special pass move: take jailer with king + play(move) { + move.flags = JSON.stringify(this.aggregateFlags()); + this.epSquares.push(this.getEpSquare(move)); + V.PlayOnBoard(this.board, move); + // TODO: turn changes only if not a sentry push or subTurn == 2 + //this.turn = V.GetOppCol(this.turn); + this.movesCount++; + this.updateVariables(move); + } - // subTurn : if sentry moved to some enemy piece --> enregistrer déplacement sentry, subTurn == 2, puis déplacer pièce adverse --> 1st 1/2 of turn, vanish sentry tout simplement. - // --> le turn ne change pas ! - // 2nd half: move only - // FEN flag: sentryPath from init pushing to final enemy square --> forbid some moves (getPotentialMoves) + undo(move) { + this.epSquares.pop(); + this.disaggregateFlags(JSON.parse(move.flags)); + V.UndoOnBoard(this.board, move); + // TODO: here too, take care of turn. If undoing when subTurn == 2, + // do not change turn (this shouldn't happen anyway). + // ==> normal undo() should be ok. + //this.turn = V.GetOppCol(this.turn); + this.movesCount--; + this.unupdateVariables(move); + } static get VALUES() { return Object.assign( diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js index 4ea521e0..b8a0838f 100644 --- a/client/src/variants/Grand.js +++ b/client/src/variants/Grand.js @@ -40,13 +40,7 @@ export const VariantRules = class GrandRules extends ChessRules { } getFenForRepeat() { - return ( - this.getBaseFen() + "_" + - this.getTurnFen() + "_" + - this.getFlagsFen() + "_" + - this.getEnpassantFen() + "_" + - this.getCapturedFen() - ); + return super.getFenForRepeat() + "_" + this.getCapturedFen(); } getCapturedFen() { @@ -328,8 +322,9 @@ export const VariantRules = class GrandRules extends ChessRules { static GenRandInitFen(randomness) { if (randomness == 0) { - return "rnbqkmcbnr/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/RNBQKMCBNR " + - "w 0 1111 - 00000000000000"; + // No castling in the official initial setup + return "r8r/1nbqkmcbn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKMCBN1/R8R " + + "w 0 0000 - 00000000000000"; } let pieces = { w: new Array(10), b: new Array(10) }; diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js index 82895f35..e31badbe 100644 --- a/client/src/variants/Recycle.js +++ b/client/src/variants/Recycle.js @@ -14,7 +14,7 @@ export const VariantRules = class RecycleRules extends ChessRules { static ParseFen(fen) { const fenParts = fen.split(" "); return Object.assign(ChessRules.ParseFen(fen), { - reserve: fenParts[5], + reserve: fenParts[5] }); } @@ -30,19 +30,11 @@ export const VariantRules = class RecycleRules extends ChessRules { } getFen() { - return ( - super.getFen() + " " + this.getReserveFen() - ); + return super.getFen() + " " + this.getReserveFen(); } getFenForRepeat() { - return ( - this.getBaseFen() + "_" + - this.getTurnFen() + "_" + - this.getFlagsFen() + "_" + - this.getEnpassantFen() + "_" + - this.getReserveFen() - ); + return super.getFenForRepeat() + "_" + this.getReserveFen(); } getReserveFen() { diff --git a/server/db/populate.sql b/server/db/populate.sql index b151b596..4060a442 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -17,6 +17,7 @@ insert or ignore into Variants (name,description) values ('Crazyhouse', 'Captures reborn'), ('Cylinder', 'Neverending rows'), ('Dark', 'In the shadow'), + ('Eightpieces', 'Each piece is unique'), ('Enpassant', 'Capture en passant'), ('Extinction', 'Capture all of a kind'), ('Grand', 'Big board'),