From: Benjamin Auder Date: Thu, 17 Dec 2020 23:23:52 +0000 (+0100) Subject: Draft of Synochess (+ Empire ready) X-Git-Url: https://git.auder.net/images/assets/doc/html/current/parser.js?a=commitdiff_plain;h=1e8a838649e16509c80c9933d99d78856e11b5c2;p=vchess.git Draft of Synochess (+ Empire ready) --- diff --git a/TODO b/TODO index 784a65ff..3146c31b 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,6 @@ Implement Wildebeest castle rules => (1, 2, 3 or 4 squares slide; randomized: may be impossible >1, but possible >4...) -getRepetitionStatus() lose or draw... (for some variants) +Fix Omega squares' color (queen on its color: need new static customization "getSquareColor()") Embedded rules language not updated when language is set (in Analyse, Game and Problems) If new live game starts in background, "new game" notify OK but not first move (not too serious however) @@ -8,6 +8,15 @@ On smartphone for Teleport, Chakart, Weiqi and some others: option "confirm move (=> comme pour corr) + option "confirm moves in corr games"? NEW VARIANTS: +https://www.pychess.org/variant/manchu +https://www.pychess.org/variant/dobutsu +https://www.pychess.org/variant/cambodian +https://www.pychess.org/variant/makpong +https://www.pychess.org/variant/janggi +https://www.pychess.org/variant/kyotoshogi +https://www.pychess.org/variant/hoppelpoppel +https://musketeerchess.net/games/musketeer/index.php Attention règle de promotion + SVG / PNG +(https://www.pychess.org/variant/shogun) Isardam (type B) : https://echekk.fr/spip.php?page=article&id_article=280 https://www.reddit.com/r/TotemChess/comments/imi3v7/totem_rules/ https://www.chessvariants.com/other.dir/fugue.html diff --git a/client/public/images/pieces/Synochess/ba.svg b/client/public/images/pieces/Synochess/ba.svg new file mode 100644 index 00000000..60841e14 --- /dev/null +++ b/client/public/images/pieces/Synochess/ba.svg @@ -0,0 +1,254 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Synochess/bc.svg b/client/public/images/pieces/Synochess/bc.svg new file mode 120000 index 00000000..a8dd04b7 --- /dev/null +++ b/client/public/images/pieces/Synochess/bc.svg @@ -0,0 +1 @@ +../Shako/bc.svg \ No newline at end of file diff --git a/client/public/images/pieces/Synochess/be.svg b/client/public/images/pieces/Synochess/be.svg new file mode 120000 index 00000000..4595ef89 --- /dev/null +++ b/client/public/images/pieces/Synochess/be.svg @@ -0,0 +1 @@ +../Shako/be.svg \ No newline at end of file diff --git a/client/public/images/pieces/Synochess/bk.svg b/client/public/images/pieces/Synochess/bk.svg new file mode 100644 index 00000000..4de7f56d --- /dev/null +++ b/client/public/images/pieces/Synochess/bk.svg @@ -0,0 +1,273 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Synochess/bn.svg b/client/public/images/pieces/Synochess/bn.svg new file mode 120000 index 00000000..63b24583 --- /dev/null +++ b/client/public/images/pieces/Synochess/bn.svg @@ -0,0 +1 @@ +../bn.svg \ No newline at end of file diff --git a/client/public/images/pieces/Synochess/br.svg b/client/public/images/pieces/Synochess/br.svg new file mode 100644 index 00000000..92482881 --- /dev/null +++ b/client/public/images/pieces/Synochess/br.svg @@ -0,0 +1,238 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/images/pieces/Synochess/bs.svg b/client/public/images/pieces/Synochess/bs.svg new file mode 100644 index 00000000..98ce6b3f --- /dev/null +++ b/client/public/images/pieces/Synochess/bs.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 7827af16..0430c68f 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -208,135 +208,153 @@ export default { // Some variants have more than sizeY reserve pieces (Clorange: 10) const reserveSquareNb = Math.max(sizeY, V.RESERVE_PIECES.length); let myReservePiecesArray = []; - for (let i = 0; i < V.RESERVE_PIECES.length; i++) { - const qty = this.vr.reserve[playingColor][V.RESERVE_PIECES[i]]; - myReservePiecesArray.push( + if (!!this.vr.reserve[playingColor]) { + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + const qty = this.vr.reserve[playingColor][V.RESERVE_PIECES[i]]; + myReservePiecesArray.push( + h( + "div", + { + "class": { board: true, ["board" + reserveSquareNb]: true }, + attrs: { id: getSquareId({ x: sizeX + shiftIdx, y: i }) }, + style: { opacity: qty > 0 ? 1 : 0.35 } + }, + [ + h("img", { + // NOTE: class "reserve" not used currently + "class": { piece: true, reserve: true }, + attrs: { + src: + "/images/pieces/" + + this.vr.getReservePpath(i, playingColor, orientation) + + ".svg" + } + }), + h( + "sup", + { + "class": { "reserve-count": true }, + style: { top: "calc(100% + 5px)" } + }, + [ qty ] + ) + ] + ) + ); + } + } + let oppReservePiecesArray = []; + const oppCol = V.GetOppCol(playingColor); + if (!!this.vr.reserve[oppCol]) { + for (let i = 0; i < V.RESERVE_PIECES.length; i++) { + const qty = this.vr.reserve[oppCol][V.RESERVE_PIECES[i]]; + oppReservePiecesArray.push( + h( + "div", + { + "class": { board: true, ["board" + reserveSquareNb]: true }, + attrs: { id: getSquareId({ x: sizeX + (1 - shiftIdx), y: i }) }, + style: { opacity: qty > 0 ? 1 : 0.35 } + }, + [ + h("img", { + "class": { piece: true, reserve: true }, + attrs: { + src: + "/images/pieces/" + + this.vr.getReservePpath(i, oppCol, orientation) + + ".svg" + } + }), + h( + "sup", + { + "class": { "reserve-count": true }, + style: { top: "calc(100% + 5px)" } + }, + [ qty ] + ) + ] + ) + ); + } + } + const myReserveTop = ( + (playingColor == 'w' && orientation == 'b') || + (playingColor == 'b' && orientation == 'w') + ); + const hasReserveTop = ( + (myReserveTop && !!this.vr.reserve[playingColor]) || + (!myReserveTop && !!this.vr.reserve[oppCol]) + ); + // "var" because must be reachable from outside this block + var hasReserveBottom = ( + (myReserveTop && !!this.vr.reserve[oppCol]) || + (!myReserveTop && !!this.vr.reserve[playingColor]) + ); + // Center reserves, assuming same number of pieces for each side: + const nbReservePieces = myReservePiecesArray.length; + const marginLeft = + ((100 - nbReservePieces * (100 / reserveSquareNb)) / 2) + "%"; + if (hasReserveTop) { + var reserveTop = h( "div", { - "class": { board: true, ["board" + reserveSquareNb]: true }, - attrs: { id: getSquareId({ x: sizeX + shiftIdx, y: i }) }, - style: { opacity: qty > 0 ? 1 : 0.35 } + "class": { + game: true, + "reserve-div": true + }, + style: { + "margin-left": marginLeft + } }, [ - h("img", { - // NOTE: class "reserve" not used currently - "class": { piece: true, reserve: true }, - attrs: { - src: - "/images/pieces/" + - this.vr.getReservePpath(i, playingColor, orientation) + - ".svg" - } - }), h( - "sup", + "div", { - "class": { "reserve-count": true }, - style: { top: "calc(100% + 5px)" } + "class": { + row: true, + "reserve-row": true + } }, - [ qty ] + myReserveTop ? myReservePiecesArray : oppReservePiecesArray ) ] - ) - ); + ); } - let oppReservePiecesArray = []; - const oppCol = V.GetOppCol(playingColor); - for (let i = 0; i < V.RESERVE_PIECES.length; i++) { - const qty = this.vr.reserve[oppCol][V.RESERVE_PIECES[i]]; - oppReservePiecesArray.push( + if (hasReserveBottom) { + var reserveBottom = h( "div", { - "class": { board: true, ["board" + reserveSquareNb]: true }, - attrs: { id: getSquareId({ x: sizeX + (1 - shiftIdx), y: i }) }, - style: { opacity: qty > 0 ? 1 : 0.35 } + "class": { + game: true, + "reserve-div": true + }, + style: { + "margin-left": marginLeft + } }, [ - h("img", { - "class": { piece: true, reserve: true }, - attrs: { - src: - "/images/pieces/" + - this.vr.getReservePpath(i, oppCol, orientation) + - ".svg" - } - }), h( - "sup", + "div", { - "class": { "reserve-count": true }, - style: { top: "calc(100% + 5px)" } + "class": { + row: true, + "reserve-row": true + } }, - [ qty ] + myReserveTop ? oppReservePiecesArray : myReservePiecesArray ) ] - ) - ); + ); } - const myReserveTop = ( - (playingColor == 'w' && orientation == 'b') || - (playingColor == 'b' && orientation == 'w') - ); - // Center reserves, assuming same number of pieces for each side: - const nbReservePieces = myReservePiecesArray.length; - const marginLeft = - ((100 - nbReservePieces * (100 / reserveSquareNb)) / 2) + "%"; - const reserveTop = - h( - "div", - { - "class": { - game: true, - "reserve-div": true - }, - style: { - "margin-left": marginLeft - } - }, - [ - h( - "div", - { - "class": { - row: true, - "reserve-row": true - } - }, - myReserveTop ? myReservePiecesArray : oppReservePiecesArray - ) - ] - ); - var reserveBottom = - h( - "div", - { - "class": { - game: true, - "reserve-div": true - }, - style: { - "margin-left": marginLeft - } - }, - [ - h( - "div", - { - "class": { - row: true, - "reserve-row": true - } - }, - myReserveTop ? oppReservePiecesArray : myReservePiecesArray - ) - ] - ); - elementArray.push(reserveTop); + if (hasReserveTop) elementArray.push(reserveTop); } elementArray.push(gameDiv); - if (!!this.vr.reserve) elementArray.push(reserveBottom); + if (!!this.vr.reserve && hasReserveBottom) + elementArray.push(reserveBottom); const boardElt = document.getElementById("gamePosition"); // boardElt might be undefine (at first drawing) if (this.choices.length > 0 && !!boardElt) { @@ -425,7 +443,8 @@ export default { touchend: this.mouseup } }; - } else { + } + else { onEvents = { on: { mousedown: this.mousedown, diff --git a/client/src/translations/en.js b/client/src/translations/en.js index a523ff82..fbcad341 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -193,7 +193,9 @@ export const translations = { "Dangerous collisions": "Dangerous collisions", "Double moves (v1)": "Double moves (v1)", "Double moves (v2)": "Double moves (v2)", + "Dynasty versus Kingdom": "Dynasty versus Kingdom", "Each piece is unique": "Each piece is unique", + "Empire versus Kingdom": "Empire versus Kingdom", "Enter the disco": "Enter the disco", "Exchange pieces' positions": "Exchange pieces' positions", "Exotic captures": "Exotic captures", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index 9fd30769..22b20a91 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -193,7 +193,9 @@ export const translations = { "Dangerous collisions": "Colisiones peligrosas", "Double moves (v1)": "Jugadas doble (v1)", "Double moves (v2)": "Jugadas doble (v2)", + "Dynasty versus Kingdom": "Dinastía contra Reino", "Each piece is unique": "Cada pieza es única", + "Empire versus Kingdom": "Imperio contra Reino", "Enter the disco": "Entrar en la discoteca", "Exchange pieces' positions": "Intercambiar posiciones de piezas", "Exotic captures": "Capturas exóticas", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index 466137b4..e3e53795 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -193,7 +193,9 @@ export const translations = { "Dangerous collisions": "Collisions dangeureuses", "Double moves (v1)": "Coups doubles (v1)", "Double moves (v2)": "Coups doubles (v2)", + "Dynasty versus Kingdom": "Dynastie contre Royaume", "Each piece is unique": "Chaque pièce est unique", + "Empire versus Kingdom": "Empire contre Royaume", "Enter the disco": "Entrez dans la boîte", "Exchange pieces' positions": "Échangez les positions des pièces", "Exotic captures": "Captures exotiques", diff --git a/client/src/translations/rules/Synochess/en.pug b/client/src/translations/rules/Synochess/en.pug index 21203baa..a0b20362 100644 --- a/client/src/translations/rules/Synochess/en.pug +++ b/client/src/translations/rules/Synochess/en.pug @@ -1 +1,57 @@ p.boxed TODO + +p. + Synochess is a chess variant designed in 2020 by Couch Tomato. + The idea of the game was to create a variant where the western chess army + can fight against the xiangqi or Chinese chess army in a fair manner. + In this game, the white army represents the western chess side and is + called the Kingdom, while the black army represents an amalgamation of + xiangqi and janggi (Korean chess) and is called the Dynasty. + +p. + The name Synochess is based off an earlier name, Sinochess, but it was + changed as the Dynasty became less "Sino" (as in Chinese) and more a + mixture of Chinese and Korean. Instead, the prefix syn- means together, + and the game represents two different historic branches of chess coming + together into one. + +ul + li The Kingdom (white) always moves first. + li The Dynasty (black) cannot castle. + li. + Kingdom pawns can only promote to their own pieces (queen, rook, knight, + bishop). Dynasty soldiers do not promote. + +h3 Special Rules + +ul + li. + King Faceoff — As in Xiangqi, Kings may not face each other + (on a file or rank) without intervening pieces. + li. + Reinforcement Soldiers — Black starts with two soldiers in hand. + Instead of moving a piece on the board, the Dynasty player can drop a + soldier onto an open square in rank 5 (Dynasty's 4th rank). + li. + Campmate — A king that reaches the final rank (without moving + into check) wins the game. + li. + Stalemate — As in Xiangqi, stalemates + and moves repetitions are a loss. + +h3 Dynasty Pieces + +p. + There are four new units unique to the Dynasty: 6 Soldiers (2 start in + hand), 2 Cannons, 2 Elephants, and 1 Advisor. +p. + The Chariots are equal to the Rooks and use the same abbreviation (R) + — the difference is purely cosmetic. Similarly, the Kings are the + same, but just appear different. + +p. + Details and diagrams of each piece are below.   + +p TODO: continue + +p https://www.pychess.org/variant/synochess diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug index 92cab1ea..156b4b55 100644 --- a/client/src/translations/variants/en.pug +++ b/client/src/translations/variants/en.pug @@ -85,8 +85,10 @@ p Standard pieces versus a team of different pieces. - var varlist = [ "Colorbound", + "Empire", "Horde", - "Orda" + "Orda", + "Synochess" ] ul for v in varlist @@ -319,7 +321,6 @@ p (Partial) Game evolution in time and space. "Sittuyin", "Xiangqi" ] - //Chinese chess (TODO) ul for v in varlist li #[a(href="/#/variants/"+v) #{v}] diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug index 11aaa5aa..890c081b 100644 --- a/client/src/translations/variants/es.pug +++ b/client/src/translations/variants/es.pug @@ -89,8 +89,10 @@ p Piezas estándar contra un equipo de diferentes piezas. - var varlist = [ "Colorbound", + "Empire", "Horde", - "Orda" + "Orda", + "Synochess" ] ul for v in varlist @@ -330,7 +332,6 @@ p Evolución (parcial) del juego en espacio y tiempo. "Sittuyin", "Xiangqi" ] - //Chinese chess (TODO) ul for v in varlist li #[a(href="/#/variants/"+v) #{v}] diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug index 831545f7..59ba797f 100644 --- a/client/src/translations/variants/fr.pug +++ b/client/src/translations/variants/fr.pug @@ -88,8 +88,10 @@ p Pièces standard contre une équipe de pièces différentes. - var varlist = [ "Colorbound", + "Empire", "Horde", - "Orda" + "Orda", + "Synochess" ] ul for v in varlist @@ -329,7 +331,6 @@ p Évolution (partielle) du jeu dans l'espace et le temps. "Sittuyin", "Xiangqi" ] - //Chinese chess (TODO) ul for v in varlist li #[a(href="/#/variants/"+v) #{v}] diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js index 9d5834fb..db9cb577 100644 --- a/client/src/variants/Crazyhouse.js +++ b/client/src/variants/Crazyhouse.js @@ -166,10 +166,9 @@ export class CrazyhouseRules extends ChessRules { } getPotentialMovesFrom([x, y]) { - if (x >= V.size.x) { + if (x >= V.size.x) // Reserves, outside of board: x == sizeX(+1) return this.getReserveMoves([x, y]); - } // Standard moves return super.getPotentialMovesFrom([x, y]); } diff --git a/client/src/variants/Synochess.js b/client/src/variants/Synochess.js index b8021100..e5c3213b 100644 --- a/client/src/variants/Synochess.js +++ b/client/src/variants/Synochess.js @@ -1,7 +1,536 @@ -import { ChessRules } from "@/base_rules"; +import { ChessRules, Move, PiPo } from "@/base_rules"; export class SynochessRules extends ChessRules { - // TODO + static get LoseOnRepetition() { + return true; + } + + static IsGoodFlags(flags) { + // Only white can castle + return !!flags.match(/^[a-z]{2,2}$/); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check reserves + if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-2]$/)) + return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[5] } + ); + } + + static GenRandInitFen(randomness) { + if (randomness == 0) + return "rneakenr/8/1c4c1/1ss2ss1/8/8/PPPPPPPP/RNBQKBNR w 0 ah - 2"; + + // Mapping kingdom --> dynasty: + const piecesMap = { + 'r': 'r', + 'n': 'n', + 'b': 'e', + 'q': 'a', + 'k': 'k' + }; + + // Always symmetric (randomness = 1), because open files. + const baseFen = ChessRules.GenRandInitFen(1); + return ( + baseFen.substr(0, 8).split("").map(p => piecesMap[p]).join("") + + "/8/1c4c1/1ss2ss1/" + baseFen.substr(22, 28) + " - 2" + ); + } + + getReserveFen() { + return (!!this.reserve ? this.reserve["b"][V.SOLDIER] : 0); + } + + getFen() { + return super.getFen() + " " + this.getReserveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getReserveFen(); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + // Also init reserve (used by the interface to show landable soldiers) + const reserve = parseInt(V.ParseFen(fen).reserve, 10); + if (reserve > 0) this.reserve = { 'b': { [V.SOLDIER]: reserve } }; + } + + getColor(i, j) { + if (i >= V.size.x) return 'b'; + return this.board[i][j].charAt(0); + } + + getPiece(i, j) { + if (i >= V.size.x) return V.SOLDIER; + return this.board[i][j].charAt(1); + } + + getReservePpath(index, color) { + // Only one piece type: soldier + return "Synochess/" + color + V.SOLDIER; + } + + static get RESERVE_PIECES() { + return [V.SOLDIER]; + } + + getReserveMoves(x) { + const color = this.turn; + if (!this.reserve || this.reserve[color][V.SOLDIER] == 0) return []; + let moves = []; + for (let i = 0; i < V.size.y; i++) { + if (this.board[3][i] == V.EMPTY) { + let mv = new Move({ + appear: [ + new PiPo({ + x: 3, + y: i, + c: color, + p: V.SOLDIER + }) + ], + vanish: [], + start: { x: x, y: 0 }, //a bit artificial... + end: { x: 3, y: i } + }); + moves.push(mv); + } + } + return moves; + } + + getPpath(b) { + return (ChessRules.PIECES.includes(b[1]) ? "" : "Synochess/") + b; + } + + getFlagsFen() { + return this.castleFlags['w'].map(V.CoordToColumn).join(""); + } + + setFlags(fenflags) { + this.castleFlags = { 'w': [-1, -1] }; + for (let i = 0; i < 2; i++) + this.castleFlags['w'][i] = V.ColumnToCoord(fenflags.charAt(i)); + } + + static get ELEPHANT() { + return "e"; + } + + static get CANNON() { + return "c"; + } + + static get SOLDIER() { + return "s"; + } + + static get ADVISOR() { + return "a"; + } + + static get PIECES() { + return ( + ChessRules.PIECES.concat([V.ELEPHANT, V.ADVISOR, V.SOLDIER, V.CANNON]) + ); + } + + static get steps() { + return Object.assign( + {}, + ChessRules.steps, + { + e: [ + [-1, -1], + [-1, 1], + [1, -1], + [1, 1], + [-2, -2], + [-2, 2], + [2, -2], + [2, 2] + ] + } + ); + } + + getPotentialMovesFrom(sq) { + if (sq[0] >= V.size.x) + // Reserves, outside of board: x == sizeX(+1) + return this.getReserveMoves(sq[0]); + let moves = []; + const piece = this.getPiece(sq[0], sq[1]); + switch (piece) { + case V.CANNON: + moves = this.getPotentialCannonMoves(sq); + break; + case V.ELEPHANT: + moves = this.getPotentialElephantMoves(sq); + break; + case V.ADVISOR: + moves = this.getPotentialAdvisorMoves(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: from here, copy/paste from EmpireChess + // 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); + // check == -1 if (row, or col) unchecked, 1 if checked and occupied, + // 0 if checked and clear + let check = [-1, -1]; + return moves.filter(m => { + if ( + m.end.y != this.kingPos[oppCol][1] && + m.end.x != this.kingPos[oppCol][0] + ) { + return true; + } + // 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] = + [this.kingPos[color][1], 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] = + [this.kingPos[color][0], 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; + }); + } + + getPotentialAdvisorMoves(sq) { + return super.getSlideNJumpMoves( + sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep"); + } + + getPotentialKingMoves([x, y]) { + if (this.getColor(x, y) == 'w') return super.getPotentialKingMoves([x, y]); + // Dynasty doesn't castle: + return super.getSlideNJumpMoves( + [x, y], + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ); + } + + 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, "oneStep"); + } + + getPotentialElephantMoves([x, y]) { + return this.getSlideNJumpMoves([x, y], V.steps[V.ELEPHANT], "oneStep"); + } + + // NOTE: (mostly) duplicated from Shako (TODO?) + getPotentialCannonMoves([x, y]) { + const oppCol = V.GetOppCol(this.turn); + let moves = []; + // Look in every direction until an obstacle (to jump) is met + for (const step of V.steps[V.ROOK]) { + 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])); + i += step[0]; + j += step[1]; + } + // Then, search for an enemy (if jumped piece isn't a cannon) + if (V.OnBoard(i, j) && this.getPiece(i, j) != V.CANNON) { + i += step[0]; + j += 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.getColor(i, j) == oppCol) + moves.push(this.getBasicMove([x, y], [i, j])); + } + } + return moves; + } + + isAttacked(sq, color) { + return ( + super.isAttackedByRook(sq, color) || + super.isAttackedByKnight(sq, color) || + super.isAttackedByKing(sq, color) || + ( + color == 'w' && + ( + super.isAttackedByPawn(sq, color) || + super.isAttackedByBishop(sq, color) || + super.isAttackedByQueen(sq, color) + ) + ) || + ( + color == 'b' && + ( + this.isAttackedByCannon(sq, color) || + this.isAttackedBySoldier(sq, color) || + this.isAttackedByAdvisor(sq, color) || + this.isAttackedByElephant(sq, color) + ) + ) + ); + } + + // NOTE: (mostly) duplicated from Shako (TODO?) + isAttackedByCannon([x, y], color) { + // Reversed process: is there an obstacle in line, + // and a cannon next in the same line? + for (const step of V.steps[V.ROOK]) { + 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.getPiece(i, j) != V.CANNON) { + // Keep looking in this direction + i += step[0]; + j += 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.getPiece(i, j) == V.CANNON && + this.getColor(i, j) == color + ) { + return true; + } + } + } + return false; + } + + isAttackedByAdvisor(sq, color) { + return ( + super.isAttackedBySlideNJump( + sq, color, V.ADVISOR, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep" + ) + ); + } + + isAttackedByElephant(sq, color) { + return ( + this.isAttackedBySlideNJump( + sq, color, V.ELEPHANT, V.steps[V.ELEPHANT], "oneStep" + ) + ); + } + + 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]], "oneStep"); + } + + getAllValidMoves() { + let moves = super.getAllPotentialMoves(); + const color = this.turn; + if (!!this.reserve && color == 'b') + moves = moves.concat(this.getReserveMoves(V.size.x + 1)); + return this.filterValid(moves); + } + + atLeastOneMove() { + if (!super.atLeastOneMove()) { + if (!!this.reserve && this.turn == 'b') { + let moves = this.filterValid(this.getReserveMoves(V.size.x + 1)); + if (moves.length > 0) return true; + } + return false; + } + return true; + } + + 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"); + } + + updateCastleFlags(move, piece) { + // Only white can castle: + const firstRank = 0; + if (piece == V.KING && move.appear[0].c == 'w') + this.castleFlags['w'] = [8, 8]; + else if ( + move.start.x == firstRank && + this.castleFlags['w'].includes(move.start.y) + ) { + const flagIdx = (move.start.y == this.castleFlags['w'][0] ? 0 : 1); + this.castleFlags['w'][flagIdx] = 8; + } + else if ( + move.end.x == firstRank && + this.castleFlags['w'].includes(move.end.y) + ) { + const flagIdx = (move.end.y == this.castleFlags['w'][0] ? 0 : 1); + this.castleFlags['w'][flagIdx] = 8; + } + } + + postPlay(move) { + super.postPlay(move); + // After black move, turn == 'w': + if (!!this.reserve && this.turn == 'w' && move.vanish.length == 0) + if (--this.reserve['b'][V.SOLDIER] == 0) this.reserve = null; + } + + postUndo(move) { + super.postUndo(move); + if (this.turn == 'b' && move.vanish.length == 0) { + if (!this.reserve) this.reserve = { 'b': { [V.SOLDIER]: 1 } }; + else this.reserve['b'][V.SOLDIER]++; + } + } + + static get VALUES() { + return Object.assign( + { + s: 2, + a: 2.75, + e: 2.75, + c: 3 + }, + ChessRules.VALUES + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + + evalPosition() { + let evaluation = super.evalPosition(); + if (this.turn == 'b') + // Add reserves: + evaluation += this.reserve['b'][V.SOLDIER] * V.VALUES[V.SOLDIER]; + return evaluation; + } }; diff --git a/server/db/populate.sql b/server/db/populate.sql index 18d92693..47a6f3cf 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -49,6 +49,7 @@ insert or ignore into Variants (name, description) values ('Doublemove2', 'Double moves (v2)'), ('Dynamo', 'Push and pull'), ('Eightpieces', 'Each piece is unique'), + ('Empire', 'Empire versus Kingdom'), ('Enpassant', 'Capture en passant'), ('Evolution', 'Faster development'), ('Extinction', 'Capture all of a kind'), @@ -110,6 +111,7 @@ insert or ignore into Variants (name, description) values ('Suction', 'Attract opposite king'), ('Swap', 'Dangerous captures'), ('Switching', 'Exchange pieces'' positions'), + ('Synochess', 'Dynasty versus Kingdom'), ('Takenmake', 'Prolongated captures'), ('Teleport', 'Reposition pieces'), ('Tencubed', 'Four new pieces'),