From: Benjamin Auder Date: Mon, 4 Jan 2021 16:44:48 +0000 (+0100) Subject: Finish Pacosako + add GameStat table to know how many live games are played X-Git-Url: https://git.auder.net/variants/current/doc/css/vendor/mini-custom.min.css?a=commitdiff_plain;h=4a2093139089632727de4f510127ef186cab528e;p=vchess.git Finish Pacosako + add GameStat table to know how many live games are played --- diff --git a/.gitignore b/.gitignore index 4b9bd216..48f8e455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Various files /server/db/vchess.sqlite +/server/db/dbconnect.py +/server/db/__pycache__/ /server/config/parameters.js /server/fallback/* !/server/fallback/README diff --git a/client/public/variants/Pacosako/manual.pdf b/client/public/variants/Pacosako/manual.pdf new file mode 100644 index 00000000..64d37899 --- /dev/null +++ b/client/public/variants/Pacosako/manual.pdf @@ -0,0 +1 @@ +#$# git-fat 4f0df77ae573ed9f84315aa2284edf8e74133553 967280 diff --git a/client/src/base_rules.js b/client/src/base_rules.js index 770561bd..69549525 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -275,7 +275,7 @@ export const ChessRules = class ChessRules { // En-passant square, if any getEpSquare(moveOrSquare) { - if (!moveOrSquare) return undefined; + if (!moveOrSquare) return undefined; //TODO: necessary line?! if (typeof moveOrSquare === "string") { const square = moveOrSquare; if (square == "-") return undefined; @@ -1073,7 +1073,8 @@ export const ChessRules = class ChessRules { V.OnBoard(rx, ry) && this.board[rx][ry] != V.EMPTY && this.getPiece(rx, ry) == piece && - this.getColor(rx, ry) == color + this.getColor(rx, ry) == color && + this.canTake([rx, ry], [x, y]) ) { return true; } diff --git a/client/src/styles/_rules.sass b/client/src/styles/_rules.sass index 2f765184..e8f670e1 100644 --- a/client/src/styles/_rules.sass +++ b/client/src/styles/_rules.sass @@ -34,6 +34,11 @@ p.boxed background-color: #FFCC66 padding: 5px +.warning + background-color: lightyellow + color: red + font-weight: bold + .bigfont font-size: 1.2em diff --git a/client/src/translations/rules/Pacosako/en.pug b/client/src/translations/rules/Pacosako/en.pug index 371b00b8..a280c6be 100644 --- a/client/src/translations/rules/Pacosako/en.pug +++ b/client/src/translations/rules/Pacosako/en.pug @@ -1,13 +1,102 @@ -p.boxed TODO +p.boxed + | "Capturing" a piece creates an union, + | which your opponent can still use on his turn. + | Enter an union to release your piece. -p WARNING 1: totally buggish right now. - -p WARNING 2: this variant may in the end not be playable here at all - will depend on the author decision. +p. + The variant's name means "Chess of Peace" in Esperanto. + Paco-Sako was invented by Felix Albers in 2017, and further developped + also by Rolf Kreibaum and Raimond Fluijt. p - a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") Video - |  showing gameplay. See also - a(href="http://pacosako.com/") the main website - | , and the associated - a(href="http://pacoplay.com/") playing area + | You can learn more about the variant's history and buy nice dedicated + | pieces (and boards) on the official website + a(href="http://pacosako.com/") pacosako.com + | . The variant is playable online at + a(href="http://pacoplay.com/") pacoplay.com | . + br + | Consequently, Paco-Sako is + span.warning not garanteed to remain playable on vchess.club. + br + | You're invited to play over there instead :-) + | Besides, they have cuter unions' drawings. + +h3 Basic rules + +p. + There are no captures in this game: only unions of pieces, + which are released when replaced by another friendly piece. + The goal is to create an union with the enemy king. + I like to think of unions as "pieces dancing together", so both + terms will be used on this page. + +figure.showPieces.text-center + img(src="/images/pieces/Pacosako/wc.png") + img(src="/images/pieces/Pacosako/bc.png") + img(src="/images/pieces/Pacosako/bt.png") + img(src="/images/pieces/Pacosako/wv.png") + figcaption Some union pieces. + +p. + At each turn, a player chooses either one of his pieces or an union piece; + let's write this piece A. +ul + li. + Case 1: A is a dancing piece. Then, it's only allowed to move + to a vacant square according to our piece's type. + li. + Case 2: A is a standard piece. + It can then be moved anywhere but on our own (normal) pieces. + "Capturing" an enemy piece creates an union composed of both pieces. + "Capturing" an union releases our piece formerly in union, + which has to be moved immediately by the same player. It can in turn + release another piece, thus following a chain of unions. + +p + | This may appear confusing at first reading, but is simpler than it seems. + | See for example this + a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") gameplay video + | , or another one from the same YouTube channel. + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/2q5/8/4O3/2w2B2/8/5K2: + .diagram.diag22 + | fen:4k3/8/2Y5/8/4s3/2S5/8/5K2: + figcaption Before and after the chaining move Bxe4, Ne4xc3, Qc3xc6. + +h3 Special moves, additional notes + +p. + "Capturing" an union en passant releases our dancing piece from the + intermediate square. + +p Promotion occur when any pawn (in union or not) reaches its final rank. + +p. + Attacks on the king are ignored in this implementation: you can run + or remain into "check". So, castling conditions are quite permissive. + Also, if you form an union with your king but end dancing with the + other king on the other end of the chain, the game is a draw. + span.warning This does not follow (at all) the official rules. + +figure.diagram-container + .diagram.diag12 + | fen:rnbq1r2/1ppppp1k/p6p/4P1OP/1PPP3c/3B4/P2V1PP1/R2QK1N1: + .diagram.diag22 + | fen:rnbq1r2/1ppppp1k/p7/4P1dP/1PPPn2c/3B4/P2V1PP1/R2QK1N1: + figcaption. + Left: Bd3(+) can be covered by Right: h6xg5 (releasing the knight), Ne4. + +p. + Canceling an union move is forbidden. For example if a bishop is + dancing with a queen, and makes the move e5 to g3, the other player cannot + move it back to e5 just after. This is also non-official. + +h3 More information + +p + | The authors wrote + a(href="/variants/Pacosako/manual.pdf") a manual + |  with many more diagrams and explanations. diff --git a/client/src/translations/rules/Pacosako/es.pug b/client/src/translations/rules/Pacosako/es.pug index 371b00b8..3e4fb99c 100644 --- a/client/src/translations/rules/Pacosako/es.pug +++ b/client/src/translations/rules/Pacosako/es.pug @@ -1,13 +1,106 @@ -p.boxed TODO +p.boxed + | "Capturar" una pieza crea una unión, que tu oponente aún puede + | utilizar en turno. Entra en una unión para entregar tu pieza. -p WARNING 1: totally buggish right now. - -p WARNING 2: this variant may in the end not be playable here at all - will depend on the author decision. +p. + El nombre de la variante significa "El Ajedrez de la Paz" en Esperanto. + Paco-Sako fue inventado por Felix Albers en 2017, y luego desarrollado + también por Rolf Kreibaum y Raimond Fluijt. p - a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") Video - |  showing gameplay. See also - a(href="http://pacosako.com/") the main website - | , and the associated - a(href="http://pacoplay.com/") playing area + | Puede obtener más información sobre el historial de la variante y comprar + | piezas bastante dedicadas (y tableros) en el sitio web oficial + a(href="http://pacosako.com/") pacosako.com + | . La variante se puede jugar en línea en + a(href="http://pacoplay.com/") pacoplay.com | . + br + | Por tanto, Paco-Sako + span.warning no se garantiza que siga siendo jugable en vchess.club. + br + | Puedes ir a jugar allí en su lugar :-) + | Además, los diseños de las uniones son más lindos. + +h3 Reglas básicas + +p. + No hay capturas en este juego: solo uniones de piezas, + que se emiten cuando otras piezas amigas los reemplazan. + El objetivo es crear una unión con el rey contrario. + Me gusta pensar en las uniones en términos de "piezas bailando juntas", + por lo tanto, ambos términos se utilizarán en esta página. + +figure.showPieces.text-center + img(src="/images/pieces/Pacosako/wc.png") + img(src="/images/pieces/Pacosako/bc.png") + img(src="/images/pieces/Pacosako/bt.png") + img(src="/images/pieces/Pacosako/wv.png") + figcaption Algunas uniones de piezas. + +p. + Cada turno, un jugador selecciona una de sus piezas o + una pieza-unión; denotar por A. +ul + li. + Caso 1: A es una pieza bailando. Entonces ella solo puede moverse + a una casilla vacía dependiendo del tipo de nuestra pieza. + li. + Caso 2: A es una pieza estándar. + Luego se puede mover a cualquier lugar excepto en nuestras propias piezas + (normales). "Capturar" una pieza enemiga crea una unión formada por + dos piezas. "Capturar" una unión libera nuestra pieza en ella, y + debe ser movido inmediatamente por el mismo jugador. Ella puede + a su vez entregan otras piezas, siguiendo así una cadena de uniones. + +p + | Esto puede parecer indigerible en la primera lectura, pero es más fácil + | de lo que parece. Ver por ejemplo esto + a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") video de gameplay + | , u otro del mismo canal de YouTube. + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/2q5/8/4O3/2w2B2/8/5K2: + .diagram.diag22 + | fen:4k3/8/2Y5/8/4s3/2S5/8/5K2: + figcaption. + Antes y después del movimiento de "encadenamiento" Bxe4, Ne4xc3, Qc3xc6. + +h3 Movimientos especiales, notas adicionales + +p. + "Capturar" una unión en passant libera nuestra pieza bailando despues la + casilla intermedia. + +p. + Una promoción tiene lugar cuando cualquier peón (posiblemente en unión) + llegó a su última fila. + +p. + Los ataques al rey se ignoran en esta implementación: + puede ir o permanecer en "jaque". Por lo tanto, las condiciones de enroque + son encontrar relajado. + Si formas una unión con tu rey pero terminas bailando con + el rey oponente en el otro extremo de la cadena, el juego se empata. + span.warning Esto no sigue las reglas oficiales (en absoluto). + +figure.diagram-container + .diagram.diag12 + | fen:rnbq1r2/1ppppp1k/p6p/4P1OP/1PPP3c/3B4/P2V1PP1/R2QK1N1: + .diagram.diag22 + | fen:rnbq1r2/1ppppp1k/p7/4P1dP/1PPPn2c/3B4/P2V1PP1/R2QK1N1: + figcaption. + Izquierda: Bd3(+) puede ser bloqueado por + Derecha: h6xg5 (liberando al caballo), Ne4. + +p. + Está prohibido cancelar una jugada de unión. Por ejemplo, si un alfil + baila con una reina y hace un movimiento de e5 a g3, el otro jugador no + puede lo reemplace en e5 inmediatamente después. Esto tampoco es oficial. + +h3 Más información + +p + | Los autores escribieron + a(href="/variants/Pacosako/manual.pdf") un manual + |  con muchos más diagramas y explicaciones. diff --git a/client/src/translations/rules/Pacosako/fr.pug b/client/src/translations/rules/Pacosako/fr.pug index 371b00b8..8442962c 100644 --- a/client/src/translations/rules/Pacosako/fr.pug +++ b/client/src/translations/rules/Pacosako/fr.pug @@ -1,13 +1,105 @@ -p.boxed TODO +p.boxed + | "Capturer" une pièce crée une union, que votre adversaire peut encore + | utiliser sur son tour. Entrez dans une union pour délivrer votre pièce. -p WARNING 1: totally buggish right now. - -p WARNING 2: this variant may in the end not be playable here at all - will depend on the author decision. +p. + Le nom de la variante signifie "Les Échecs de la Paix" en Esperanto. + Paco-Sako a été inventée par Felix Albers en 2017, et développée ensuite + également par Rolf Kreibaum et Raimond Fluijt. p - a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") Video - |  showing gameplay. See also - a(href="http://pacosako.com/") the main website - | , and the associated - a(href="http://pacoplay.com/") playing area + | Vous pouvez en apprendre plus sur l'histoire de la variante et acheter + | de jolies pièces (et échiquiers) dédiées sur le site officiel + a(href="http://pacosako.com/") pacosako.com + | . La variante est jouable en ligne sur + a(href="http://pacoplay.com/") pacoplay.com | . + br + | Par conséquent, Paco-Sako n'est + span.warning pas garantie de rester jouable sur vchess.club. + br + | Vous êtes invités à plutôt aller y jouer là-bas :-) + | En outre, les dessins des unions sont plus mignons. + +h3 Règles de base + +p. + Il n'y a pas de captures dans ce jeu : seulement des unions de pièces, + qui sont délivrées quand d'autres pièces amies les remplacent. + L'objectif est de créer une union avec le roi adverse. + J'aime penser aux unions en terme de "pièces dansant ensemble", + donc les deux termes seront utilisés sur cette page. + +figure.showPieces.text-center + img(src="/images/pieces/Pacosako/wc.png") + img(src="/images/pieces/Pacosako/bc.png") + img(src="/images/pieces/Pacosako/bt.png") + img(src="/images/pieces/Pacosako/wv.png") + figcaption Quelques pièces unions. + +p. + À chaque tour, un joueur sélectionne l'une de ses pièces ou + une pièce-union ; notons la A. +ul + li. + Cas 1 : A est une pièce dansante. Alors, elle ne peut que se déplacer + vers une case vide selon le type de notre pièce. + li. + Cas 2 : A est une pièce standard. + Elle peut alors être déplacée n'importe où sauf sur nos propres pièces + (normales). "Capturer" une pièce ennemie crée une union composée des + deux pièces. "Capturer" une union libère notre pièce s'y trouvant, et + celle-ci doit être déplacée immédiatement par le même joueur. Elle peut + à son tour délivrer d'autres pièces, suivant ainsi une chaines d'unions. + +p + | Ceci peut paraître indigeste à première lecture, mais c'est plus simple + | que ça en a l'air. Voyez par exemple cette + a(href="https://www.youtube.com/watch?v=tQ2JLsFvfxI") vidéo de gameplay + | , ou une autre de la même chaîne YouTube. + +figure.diagram-container + .diagram.diag12 + | fen:4k3/8/2q5/8/4O3/2w2B2/8/5K2: + .diagram.diag22 + | fen:4k3/8/2Y5/8/4s3/2S5/8/5K2: + figcaption Avant et après le coup "chaînant" Bxe4, Ne4xc3, Qc3xc6. + +h3 Coups spéciaux, notes additionnelles + +p. + "Capturer" une union en passant libère notre pièce dansante depuis la + case intermédiaire. + +p. + Une promotion a lieu quand n'importe quel pion (éventuellement en union) + atteint sa dernière rangée. + +p. + Les attaques sur le roi sont ignorées dans cette implémentation : vous + pouvez aller ou rester en "échec". Ainsi, les conditions du roque se + retrouvent assouplie. + Si vous formez une union avec votre roi mais terminez par danser avec + le roi adverse à l'autre bout de la chaîne, la partie est nulle. + span.warning Cela ne suit pas (du tout) les règles officielles. + +figure.diagram-container + .diagram.diag12 + | fen:rnbq1r2/1ppppp1k/p6p/4P1OP/1PPP3c/3B4/P2V1PP1/R2QK1N1: + .diagram.diag22 + | fen:rnbq1r2/1ppppp1k/p7/4P1dP/1PPPn2c/3B4/P2V1PP1/R2QK1N1: + figcaption. + Gauche : Bd3(+) peut être paré par + Droite : h6xg5 (libérant le cavalier), Ne4. + +p. + Annuler un coup d'union est interdit. Par exemple, si un fou danse avec + une dame, et effectue un déplacement de e5 en g3, l'autre joueur ne peut + pas la replacer en e5 immédiatement après. Ceci est également non-officiel. + +h3 Plus d'information + +p + | Les auteurs ont écrit + a(href="/variants/Pacosako/manual.pdf") un manuel + |  avec bien plus de diagrammes et d'explications. diff --git a/client/src/variants/Pacosako.js b/client/src/variants/Pacosako.js index e935240c..2048d2c0 100644 --- a/client/src/variants/Pacosako.js +++ b/client/src/variants/Pacosako.js @@ -65,23 +65,33 @@ export class PacosakoRules extends ChessRules { return "Pacosako/" + b; } - getPPath(m) { + getPPpath(m) { if (ChessRules.PIECES.includes(m.appear[0].p)) return super.getPPpath(m); // For an union, show only relevant piece: // The color must be deduced from the move: reaching final rank of who? - const color = (m.appear[0].x == 0 ? 'b' : 'w'); - const up = this.getUnionPieces(color, m.appear[0].p); - return color + up[color]; + const color = (m.appear[0].x == 0 ? 'w' : 'b'); + const up = this.getUnionPieces(m.appear[0].c, m.appear[0].p); + return "Pacosako/" + color + up[color]; } canTake([x1, y1], [x2, y2]) { - const c1 = this.getColor(x1, y1); - const c2 = this.getColor(x2, y2); - return (c1 != 'u' && c2 != c1); + const p1 = this.board[x1][y1].charAt(1); + if (!(ChessRules.PIECES.includes(p1))) return false; + const p2 = this.board[x2][y2].charAt(1); + if (!(ChessRules.PIECES.includes(p2))) return true; + const c1 = this.board[x1][y1].charAt(0); + const c2 = this.board[x2][y2].charAt(0); + return (c1 != c2); } canIplay(side, [x, y]) { - return this.turn == side && this.getColor(x, y) != V.GetOppCol(side); + return ( + this.turn == side && + ( + !(ChessRules.PIECES.includes(this.board[x][y].charAt(1))) || + this.board[x][y].charAt(0) == side + ) + ); } scanKings(fen) { @@ -110,12 +120,73 @@ export class PacosakoRules extends ChessRules { super.setOtherVariables(fen); // Stack of "last move" only for intermediate chaining this.lastMoveEnd = [null]; + // Local stack of non-capturing union moves: + this.umoves = []; + const umove = V.ParseFen(fen).umove; + if (umove == "-") this.umoves.push(null); + else { + this.umoves.push({ + start: ChessRules.SquareToCoords(umove.substr(0, 2)), + end: ChessRules.SquareToCoords(umove.substr(2)) + }); + } + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParts = fen.split(" "); + if (fenParts.length != 6) return false; + if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/)) + return false; + return true; + } + + getUmove(move) { + if ( + move.vanish.length == 1 && + !(ChessRules.PIECES.includes(move.appear[0].p)) + ) { + // An union moving + return { start: move.start, end: move.end }; + } + return null; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { umove: fenParts[5] } + ); + } + + static GenRandInitFen(randomness) { + // Add empty umove + return ChessRules.GenRandInitFen(randomness) + " -"; + } + + getUmoveFen() { + const L = this.umoves.length; + return ( + !this.umoves[L - 1] + ? "-" + : ChessRules.CoordsToSquare(this.umoves[L - 1].start) + + ChessRules.CoordsToSquare(this.umoves[L - 1].end) + ); + } + + getFen() { + return super.getFen() + " " + this.getUmoveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getUmoveFen(); } getColor(i, j) { const p = this.board[i][j].charAt(1); if (ChessRules.PIECES.includes(p)) return super.getColor(i, j); - return 'u'; //union + return this.turn; //union: I can use it, so it's "my" color... } getPiece(i, j, color) { @@ -135,6 +206,7 @@ export class PacosakoRules extends ChessRules { }; } + // p1: white piece, p2: black piece getUnionCode(p1, p2) { let uIdx = ( Object.values(V.UNIONS).findIndex(v => v[0] == p1 && v[1] == p2) @@ -149,31 +221,49 @@ export class PacosakoRules extends ChessRules { } getBasicMove([sx, sy], [ex, ey], tr) { - const initColor = this.board[sx][sy].charAt(0); - const initPiece = this.board[sx][sy].charAt(1); + const L = this.lastMoveEnd.length; + const lm = this.lastMoveEnd[L-1]; + const piece = (!!lm ? lm.p : null); + const initColor = (!!piece ? this.turn : this.board[sx][sy].charAt(0)); + const initPiece = (piece || this.board[sx][sy].charAt(1)); + const c = this.turn; + const oppCol = V.GetOppCol(c); + if (!!tr && !(ChessRules.PIECES.includes(initPiece))) { + // Transformation computed without taking union into account + const up = this.getUnionPieces(initColor, initPiece); + let args = [tr.p, up[oppCol]]; + if (c == 'b') args = args.reverse(); + const cp = this.getUnionCode(args[0], args[1]); + tr.c = cp.c; + tr.p = cp.p; + } // 4 cases : moving // - union to free square (other cases are illegal: return null) // - normal piece to free square, // to enemy normal piece, or // to union (releasing our piece) let mv = new Move({ - vanish: [ + start: { x: sx, y: sy }, + end: { x: ex, y: ey }, + vanish: [] + }); + if (!piece) { + mv.vanish = [ new PiPo({ x: sx, y: sy, c: initColor, p: initPiece }) - ], - end: { x: ex, y: ey } - }); + ]; + } // Treat free square cases first: if (this.board[ex][ey] == V.EMPTY) { mv.appear = [ new PiPo({ x: ex, y: ey, - c: initColor, + c: !!tr ? tr.c : initColor, p: !!tr ? tr.p : initPiece }) ]; @@ -192,7 +282,9 @@ export class PacosakoRules extends ChessRules { ); if (ChessRules.PIECES.includes(destPiece)) { // Normal piece: just create union - const cp = this.getUnionCode(!!tr ? tr.p : initPiece, destPiece); + let args = [!!tr ? tr.p : initPiece, destPiece]; + if (c == 'b') args = args.reverse(); + const cp = this.getUnionCode(args[0], args[1]); mv.appear = [ new PiPo({ x: ex, @@ -205,9 +297,9 @@ export class PacosakoRules extends ChessRules { } // Releasing a piece in an union: keep track of released piece const up = this.getUnionPieces(destColor, destPiece); - const c = this.turn; - const oppCol = V.GetOppCol(c); - const cp = this.getUnionCode(!!tr ? tr.p : initPiece, up[oppCol]) + let args = [!!tr ? tr.p : initPiece, up[oppCol]]; + if (c == 'b') args = args.reverse(); + const cp = this.getUnionCode(args[0], args[1]); mv.appear = [ new PiPo({ x: ex, @@ -220,16 +312,13 @@ export class PacosakoRules extends ChessRules { return mv; } - getPotentialMoves([x, y]) { + getPotentialMovesFrom([x, y]) { const L = this.lastMoveEnd.length; const lm = this.lastMoveEnd[L-1]; - let piece = null; + if (!!lm && (x != lm.x || y != lm.y)) return []; + const piece = (!!lm ? lm.p : this.getPiece(x, y)); if (!!lm) { - if (x != lm.x || y != lm.y) return []; - piece = lm.p; - } - if (!!piece) { - var unionOnBoard = this.board[x][y]; + var saveSquare = this.board[x][y]; this.board[x][y] = this.turn + piece; } let baseMoves = []; @@ -256,80 +345,245 @@ export class PacosakoRules extends ChessRules { // When a pawn in an union reaches final rank with a non-standard // promotion move: apply promotion anyway let moves = []; + const c = this.turn; + const oppCol = V.GetOppCol(c); + const oppLastRank = (c == 'w' ? 7 : 0); baseMoves.forEach(m => { - // (move to first rank, which is last rank for opponent [pawn]), should show promotion choices. - //if (m. //bring enemy pawn to his first rank ==> union types involved... color... - moves.push(m); //TODO + if ( + m.end.x == oppLastRank && + ['c', 'd', 'e', 'f', 'g'].includes(m.appear[0].p) + ) { + // Move to first rank, which is last rank for opponent's pawn. + // => Show promotion choices. + // Find our piece in union (not a pawn) + const up = this.getUnionPieces(m.appear[0].c, m.appear[0].p); + // merge with all potential promotion pieces + push (loop) + for (let promotionPiece of [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]) { + let args = [up[c], promotionPiece]; + if (c == 'b') args = args.reverse(); + const cp = this.getUnionCode(args[0], args[1]); + let cpMove = JSON.parse(JSON.stringify(m)); + cpMove.appear[0].c = cp.c; + cpMove.appear[0].p = cp.p; + moves.push(cpMove); + } + } + else { + if ( + m.vanish.length > 0 && + m.vanish[0].p == V.PAWN && + m.start.y != m.end.y && + this.board[m.end.x][m.end.y] == V.EMPTY + ) { + if (!!lm) + // No en-passant inside a chaining + return; + // Fix en-passant capture: union type, maybe released piece too + const cs = [m.end.x + (c == 'w' ? 1 : -1), m.end.y]; + const color = this.board[cs[0]][cs[1]].charAt(0); + const code = this.board[cs[0]][cs[1]].charAt(1); + if (code == V.PAWN) { + // Simple en-passant capture (usual: just form union) + m.appear[0].c = 'w'; + m.appear[0].p = 'a'; + } + else { + // An union pawn + something juste moved two squares + const up = this.getUnionPieces(color, code); + m.released = up[c]; + let args = [V.PAWN, up[oppCol]]; + if (c == 'b') args = args.reverse(); + const cp = this.getUnionCode(args[0], args[1]); + m.appear[0].c = cp.c; + m.appear[0].p = cp.p; + } + } + moves.push(m); + } }); - if (!!piece) this.board[x][y] = unionOnBoard; + if (!!lm) this.board[x][y] = saveSquare; return moves; } + getEpSquare(moveOrSquare) { + if (typeof moveOrSquare === "string") { + const square = moveOrSquare; + if (square == "-") return undefined; + return V.SquareToCoords(square); + } + const move = moveOrSquare; + const s = move.start, + e = move.end; + const oppCol = V.GetOppCol(this.turn); + if ( + s.y == e.y && + Math.abs(s.x - e.x) == 2 && + this.getPiece(s.x, s.y, oppCol) == V.PAWN + ) { + return { + x: (s.x + e.x) / 2, + y: s.y + }; + } + return undefined; + } + + // Does m2 un-do m1 ? (to disallow undoing union moves) + oppositeMoves(m1, m2) { + return ( + !!m1 && + !(ChessRules.PIECES.includes(m2.appear[0].p)) && + m2.vanish.length == 1 && + m1.start.x == m2.end.x && + m1.end.x == m2.start.x && + m1.start.y == m2.end.y && + m1.end.y == m2.start.y + ); + } + + // Do not consider checks for now (TODO) + underCheck() { + return false; + } + getCheckSquares() { + return []; + } + filterValid(moves) { + if (moves.length == 0) return []; + const L = this.umoves.length; //at least 1: init from FEN + return moves.filter(m => !this.oppositeMoves(this.umoves[L - 1], m)); + } + play(move) { this.epSquares.push(this.getEpSquare(move)); // Check if the move is the last of the turn: all cases except releases - move.last = ( - move.vanish.length == 1 || - ChessRules.PIECES.includes(move.vanish[1].p) - ); - if (move.last) { + if (!move.released) { // No more union releases available this.turn = V.GetOppCol(this.turn); this.movesCount++; this.lastMoveEnd.push(null); } - else { - const color = this.board[move.end.x][move.end.y].charAt(0); - const oldUnion = this.board[move.end.x][move.end.y].charAt(1); - const released = this.getUnionPieces(color, oldUnion)[this.turn]; - this.lastMoveEnd.push(Object.assign({}, move.end, { p: released })); - } + else this.lastMoveEnd.push(Object.assign({ p: move.released }, move.end)); V.PlayOnBoard(this.board, move); + this.umoves.push(this.getUmove(move)); this.postPlay(move); } + postPlay(move) { + if (move.vanish.length == 0) + // A piece released just moved. Cannot be the king. + return; + const c = move.vanish[0].c; + const piece = move.vanish[0].p; + if (piece == V.KING) + this.kingPos[c] = [move.appear[0].x, move.appear[0].y]; + this.updateCastleFlags(move, piece); + } + undo(move) { this.epSquares.pop(); V.UndoOnBoard(this.board, move); this.lastMoveEnd.pop(); - if (move.last) { + if (!move.released) { this.turn = V.GetOppCol(this.turn); this.movesCount--; } + this.umoves.pop(); this.postUndo(move); } + postUndo(move) { + if (this.getPiece(move.start.x, move.start.y) == V.KING) + this.kingPos[this.turn] = [move.start.x, move.start.y]; + } + getCurrentScore() { // Check kings: if one is dancing, the side lost + // But, if both dancing, let's say it's a draw :-) const [kpW, kpB] = [this.kingPos['w'], this.kingPos['b']]; - if (this.board[kpB[0]][kpB[1]].charAt(1) != 'k') return "1-0"; - if (this.board[kpW[0]][kpW[1]].charAt(1) != 'k') return "0-1"; + const atKingPlace = [ + this.board[kpW[0]][kpW[1]].charAt(1), + this.board[kpB[0]][kpB[1]].charAt(1) + ]; + if (!atKingPlace.includes('k')) return "1/2"; + if (atKingPlace[0] != 'k') return "0-1"; + if (atKingPlace[1] != 'k') return "1-0"; return "*"; } getComputerMove() { - let moves = this.getAllValidMoves(); - if (moves.length == 0) return null; - // Just play random moves (for now at least. TODO?) - let mvArray = []; - while (moves.length > 0) { - const mv = moves[randInt(moves.length)]; - mvArray.push(mv); - this.play(mv); - if (!mv.last) - // A piece was just released from an union - moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]); - else break; + let initMoves = this.getAllValidMoves(); + if (initMoves.length == 0) return null; + // Loop until valid move is found (no blocked pawn released...) + 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.released) + // A piece was just released from an union + 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.released) return (mvArray.length > 1 ? mvArray : mvArray[0]); } - for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); - return (mvArray.length > 1 ? mvArray : mvArray[0]); } // NOTE: evalPosition() is wrong, but unused since bot plays at random getNotation(move) { - // TODO: in case of enemy pawn promoted, add "=..." in the end - return super.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]; + let piece = null; + if (!lm && move.vanish.length == 0) + // When importing a game, the info move.released is lost + piece = move.appear[0].p; + else piece = (!!lm ? lm.p : move.vanish[0].p); + if (!(ChessRules.PIECES.includes(piece))) { + // Decode (moving) union + const up = this.getUnionPieces( + move.vanish.length > 0 ? move.vanish[0].c : move.appear[0].c, piece); + piece = up[c] + } + + // 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) { + const up = this.getUnionPieces(move.appear[0].c, move.appear[0].p); + notation += "=" + up[c].toUpperCase(); + } + else if ( + move.end.x == firstLastRank[0] && + move.vanish.length > 0 && + ['c', 'd', 'e', 'f', 'g'].includes(move.vanish[0].p) + ) { + // We promoted an opponent's pawn + const oppCol = V.GetOppCol(c); + const up = this.getUnionPieces(move.appear[0].c, move.appear[0].p); + notation += "=" + up[oppCol].toUpperCase(); + } + + return notation; } }; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index d88ac1e2..ef3819b1 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -514,7 +514,8 @@ export default { "DELETE", { data: { gid: this.game.id } } ); - } else { + } + else { // Live game GameStorage.update(this.gameRef, { delchat: true }); } @@ -578,7 +579,8 @@ export default { // For self multi-connects tests: this.newConnect[data.from[0]] = true; this.send("askidentity", { target: data.from[0] }); - } else { + } + else { this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true }; this.$forceUpdate(); //TODO: shouldn't be required } @@ -674,9 +676,8 @@ export default { ) { this.send("asklastate", { target: user.sid }); counter++; - } else { - clearInterval(this.askLastate); } + else clearInterval(this.askLastate); }, 1500 ); @@ -874,13 +875,15 @@ export default { gameInfo.players.some(p => p.sid == this.st.user.sid) ) { this.addAndGotoLiveGame(gameInfo); - } else if ( + } + else if ( gameType == "corr" && this.st.user.id > 0 && gameInfo.players.some(p => p.id == this.st.user.id) ) { this.$router.push("/game/" + gameInfo.id); - } else { + } + else { this.rematchId = gameInfo.id; document.getElementById("modalRules").checked = false; document.getElementById("modalScore").checked = false; @@ -972,7 +975,8 @@ export default { // Just got last move from him this.$refs["basegame"].play(data.lastMove, "received"); this.processMove(data.lastMove); - } else { + } + else { if (!!this.clockUpdate) clearInterval(this.clockUpdate); this.re_setClocks(); } @@ -994,7 +998,8 @@ export default { : "Three repetitions"; this.send("draw", { data: message }); this.gameOver("1/2", message); - } else if (this.drawOffer == "") { + } + else if (this.drawOffer == "") { // No effect if drawOffer == "sent" if (this.game.mycolor != this.vr.turn) { alert(this.st.tr["Draw offer only in your turn"]); @@ -1008,7 +1013,8 @@ export default { this.gameRef, { drawOffer: this.game.mycolor } ); - } else this.updateCorrGame({ drawOffer: this.game.mycolor }); + } + else this.updateCorrGame({ drawOffer: this.game.mycolor }); } }, addAndGotoLiveGame: function(gameInfo, callback) { @@ -1075,7 +1081,8 @@ export default { } ); } - } else if (this.rematchOffer == "") { + } + else if (this.rematchOffer == "") { this.rematchOffer = "sent"; this.send("rematchoffer", { data: true }); if (this.game.type == "live") { @@ -1083,8 +1090,10 @@ export default { this.gameRef, { rematchOffer: this.game.mycolor } ); - } else this.updateCorrGame({ rematchOffer: this.game.mycolor }); - } else if (this.rematchOffer == "sent") { + } + else this.updateCorrGame({ rematchOffer: this.game.mycolor }); + } + else if (this.rematchOffer == "sent") { // Toggle rematch offer (on --> off) this.rematchOffer = ""; this.send("rematchoffer", { data: false }); @@ -1093,7 +1102,8 @@ export default { this.gameRef, { rematchOffer: '' } ); - } else this.updateCorrGame({ rematchOffer: 'n' }); + } + else this.updateCorrGame({ rematchOffer: 'n' }); } }, abortGame: function() { @@ -1162,11 +1172,10 @@ export default { { clocks: game.clocks } ); } - } else { - if (!!game.initime) - // It's my turn: clocks not updated yet - game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } + else if (!!game.initime) + // It's my turn: clocks not updated yet + game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } else // gtype == "import" @@ -1329,7 +1338,8 @@ export default { currentTurn == "w" ? "0-1" : "1-0", "Time" ); - } else { + } + else { this.$set( this.virtualClocks, colorIdx, diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 03d34119..ebb46753 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -441,7 +441,8 @@ export default { } } ); - } else addChallenges(); + } + else addChallenges(); } } ); @@ -701,7 +702,8 @@ export default { // For self multi-connects tests: this.newConnect[data.from[0]] = true; this.send("askidentity", { target: data.from[0], page: page }); - } else { + } + else { this.people[data.from[0]].tmpIds[data.from[1]] = { page: page, focus: true }; this.$forceUpdate(); //TODO: shouldn't be required @@ -733,7 +735,8 @@ export default { "all" ); } - } else { + } + else { // Remove the matching live game if now unreachable const gid = data.page.match(/[a-zA-Z0-9]+$/)[0]; // Corr games are always reachable: @@ -948,7 +951,8 @@ export default { ); }); this.games = this.games.concat(moreGames); - } else this.hasMore = false; + } + else this.hasMore = false; } } ); @@ -1016,7 +1020,8 @@ export default { position: parsedFen.position //,orientation: parsedFen.turn }); - } else this.newchallenge.diag = ""; + } + else this.newchallenge.diag = ""; }, newChallFromPreset(pchall) { this.partialResetNewchallenge(); @@ -1154,7 +1159,8 @@ export default { if (ctype == "live") { // Live challenges have a random ID finishAddChallenge(null); - } else { + } + else { // Correspondence game: send challenge to server ajax( "/challenges", @@ -1193,7 +1199,8 @@ export default { else // Corr challenge: just remove the challenge this.send("deletechallenge_s", { data: { cid: c.id } }); - } else { + } + else { const oppsid = this.getOppsid(c); if (!!oppsid) this.send("refusechallenge", { data: c.id, target: oppsid }); @@ -1300,7 +1307,14 @@ export default { if (c.type == "live") { notifyNewgame(); this.startNewGame(gameInfo); - } else { + // Increment game stats counter in DB + ajax( + "/gamestat", + "POST", + { data: { vid: gameInfo.vid } } + ); + } + else { // corr: game only on server ajax( "/games", diff --git a/server/db/create.sql b/server/db/create.sql index 15417b19..e4035a15 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -44,6 +44,12 @@ create table Challenges ( foreign key (vid) references Variants(id) ); +create table GameStat ( + vid integer, + total integer default 0, + foreign key (vid) references Variants(id) +); + create table Games ( id integer primary key, vid integer, diff --git a/server/db/dbconnect.py.dist b/server/db/dbconnect.py.dist new file mode 100644 index 00000000..9a86a193 --- /dev/null +++ b/server/db/dbconnect.py.dist @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import sqlite3 +from sqlite3 import Error + +vchess_db_path = "/path/to/vchess.sqlite" + +def create_connection(): + """ + Create a database connection to the vchess SQLite database + :return: Connection object or None + """ + + conn = None + try: + conn = sqlite3.connect(vchess_db_path) + except Error as e: + print(e) + + return conn diff --git a/server/db/sync_gamestat.py b/server/db/sync_gamestat.py new file mode 100755 index 00000000..0800656c --- /dev/null +++ b/server/db/sync_gamestat.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# Manually (for now: TODO) add an entry in GameStat when a variant is added + +from dbconnect import create_connection + +def sync_gamestat(): + """ + (Incrementally) Synchronize GameStat table from Variants update + """ + + conn = create_connection() + cur = conn.cursor() + + cur.execute("SELECT max(vid) FROM GameStat"); + vid_max = cur.fetchone()[0] or 0 + cur.execute("SELECT id FROM Variants WHERE id > ?", (vid_max,)) + rows = cur.fetchall() + for variant in rows: + cur.execute("INSERT INTO GameStat(vid) VALUES (?)", (variant[0],)) + + conn.commit() + cur.close() + +sync_gamestat() diff --git a/server/models/Game.js b/server/models/Game.js index 02ef2d16..4a8c517a 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -47,6 +47,16 @@ const GameModel = { ); }, + incrementCounter: function(vid, cb) { + db.serialize(function() { + let query = + "UPDATE GameStat " + + "SET total = total + 1 " + + "WHERE vid = " + vid; + db.run(query, cb); + }); + }, + create: function(vid, fen, randomness, cadence, players, cb) { db.serialize(function() { let query = diff --git a/server/routes/games.js b/server/routes/games.js index 42c15c99..ede0e6dd 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -5,6 +5,14 @@ const GameModel = require('../models/Game'); const access = require("../utils/access"); const params = require("../config/parameters"); +router.post("/gamestat", access.ajax, (req,res) => { + const vid = req.body.vid; + if (!!vid && !!vid.toString().match(/^[0-9]+$/)) { + GameModel.incrementCounter(vid); + res.json({}); + } +}); + // From main hall, start game between players 0 and 1 router.post("/games", access.logged, access.ajax, (req,res) => { const gameInfo = req.body.gameInfo;