From 7c05a5f2297bea540c700ebceb0cc8b03a7f6775 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Tue, 19 Jan 2021 03:29:08 +0100 Subject: [PATCH] Add Gomoku + Atarigo --- client/public/images/pieces/Gomoku/bp.svg | 1 + client/public/images/pieces/Gomoku/wp.svg | 1 + client/src/base_rules.js | 6 +- client/src/components/Board.vue | 9 +- client/src/styles/_board_squares_img.sass | 4 + client/src/translations/about/en.pug | 1 + client/src/translations/about/es.pug | 1 + client/src/translations/about/fr.pug | 1 + client/src/translations/en.js | 1 + client/src/translations/es.js | 1 + client/src/translations/fr.js | 1 + client/src/translations/rules/Atarigo/en.pug | 52 +++++ client/src/translations/rules/Atarigo/es.pug | 52 +++++ client/src/translations/rules/Atarigo/fr.pug | 52 +++++ client/src/translations/rules/Gomoku/en.pug | 16 +- client/src/translations/rules/Gomoku/es.pug | 16 +- client/src/translations/rules/Gomoku/fr.pug | 16 +- client/src/translations/variants/en.pug | 1 + client/src/translations/variants/es.pug | 1 + client/src/translations/variants/fr.pug | 1 + client/src/variants/Atarigo.js | 225 +++++++++++++++++++ client/src/variants/Atomic2.js | 2 +- client/src/variants/Bario.js | 2 +- client/src/variants/Chakart.js | 2 +- client/src/variants/Emergo.js | 6 + client/src/variants/Gomoku.js | 214 +++++++++++++++++- client/src/variants/Hamilton.js | 2 +- client/src/variants/Konane.js | 4 +- client/src/variants/Madhouse.js | 2 +- client/src/variants/Pocketknight.js | 2 +- client/src/variants/Teleport.js | 2 +- client/src/variants/Titan.js | 2 +- client/src/variants/Yote.js | 4 +- server/db/populate.sql | 1 + 34 files changed, 682 insertions(+), 22 deletions(-) create mode 120000 client/public/images/pieces/Gomoku/bp.svg create mode 120000 client/public/images/pieces/Gomoku/wp.svg create mode 100644 client/src/translations/rules/Atarigo/en.pug create mode 100644 client/src/translations/rules/Atarigo/es.pug create mode 100644 client/src/translations/rules/Atarigo/fr.pug create mode 100644 client/src/variants/Atarigo.js diff --git a/client/public/images/pieces/Gomoku/bp.svg b/client/public/images/pieces/Gomoku/bp.svg new file mode 120000 index 00000000..0d6535fd --- /dev/null +++ b/client/public/images/pieces/Gomoku/bp.svg @@ -0,0 +1 @@ +../Yote/bp.svg \ No newline at end of file diff --git a/client/public/images/pieces/Gomoku/wp.svg b/client/public/images/pieces/Gomoku/wp.svg new file mode 120000 index 00000000..dc5717d3 --- /dev/null +++ b/client/public/images/pieces/Gomoku/wp.svg @@ -0,0 +1 @@ +../Yote/wp.svg \ No newline at end of file diff --git a/client/src/base_rules.js b/client/src/base_rules.js index e8a82f93..aff47109 100644 --- a/client/src/base_rules.js +++ b/client/src/base_rules.js @@ -442,8 +442,10 @@ export const ChessRules = class ChessRules { // if more than 9 consecutive free spaces, break the integer, // otherwise FEN parsing will fail. if (count <= 9) return count; - // Currently only boards of size up to 11 or 12: - return "9" + (count - 9); + // Most boards of size < 18: + if (count <= 18) return "9" + (count - 9); + // Except Gomoku: + return "99" + (count - 18); }; let position = ""; for (let i = 0; i < V.size.x; i++) { diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index b4528082..9e23fc56 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -194,7 +194,9 @@ export default { showCheck && lightSquare && incheckSq[ci][cj], "incheck-dark": showCheck && !lightSquare && incheckSq[ci][cj], - "hover-highlight": this.vr.hoverHighlight(ci, cj) + "hover-highlight": + this.vr.hoverHighlight( + [ci, cj], !this.analyze ? this.userColor : null) }, attrs: { id: getSquareId({ x: ci, y: cj }) @@ -575,11 +577,12 @@ export default { return path; }, re_setDrawings: function() { + // Add some drawing on board (for some variants + arrows and circles) + const boardElt = document.getElementById("gamePosition"); + if (!boardElt) return; // Remove current canvas, if any const curCanvas = document.getElementById("arrowCanvas"); if (!!curCanvas) curCanvas.parentNode.removeChild(curCanvas); - // Add some drawing on board (for some variants + arrows and circles) - const boardElt = document.getElementById("gamePosition"); const squareWidth = boardElt.offsetWidth / V.size.y; const bPos = boardElt.getBoundingClientRect(); let svgArrows = []; diff --git a/client/src/styles/_board_squares_img.sass b/client/src/styles/_board_squares_img.sass index e39290d7..3dc3a127 100644 --- a/client/src/styles/_board_squares_img.sass +++ b/client/src/styles/_board_squares_img.sass @@ -43,6 +43,10 @@ div.board12 width: 8.33% padding-bottom: 8.33% +div.board19 + width: 5.26% + padding-bottom: 5.26% + img.piece width: 100% z-index: 10 diff --git a/client/src/translations/about/en.pug b/client/src/translations/about/en.pug index 68da4992..50c38957 100644 --- a/client/src/translations/about/en.pug +++ b/client/src/translations/about/en.pug @@ -47,6 +47,7 @@ h3 Related links a(href="http://pychess-variants.herokuapp.com/") pychess-variants.com a(href="https://glukkazan.github.io/") Dagaz demo + server a(href="https://www.jocly.com/#/games") jocly.com + a(href="http://www.iggamecenter.com/") iggamecenter.com a(href="https://musketeerchess.net/home/index.html") musketeerchess.net a(href="https://schemingmind.com/") schemingmind.com a(href="https://echekk.fr/spip.php?page=rubrique&id_rubrique=1") echekk.fr diff --git a/client/src/translations/about/es.pug b/client/src/translations/about/es.pug index d596b005..2d236106 100644 --- a/client/src/translations/about/es.pug +++ b/client/src/translations/about/es.pug @@ -46,6 +46,7 @@ h3 Enlaces relacionados a(href="http://pychess-variants.herokuapp.com/") pychess-variants.com a(href="https://glukkazan.github.io/") Dagaz demo + servidor a(href="https://www.jocly.com/#/games") jocly.com + a(href="http://www.iggamecenter.com/") iggamecenter.com a(href="https://musketeerchess.net/home/index.html") musketeerchess.net a(href="https://schemingmind.com/") schemingmind.com a(href="https://echekk.fr/spip.php?page=rubrique&id_rubrique=1") echekk.fr diff --git a/client/src/translations/about/fr.pug b/client/src/translations/about/fr.pug index f682df84..fe74c767 100644 --- a/client/src/translations/about/fr.pug +++ b/client/src/translations/about/fr.pug @@ -47,6 +47,7 @@ h3 Liens connexes a(href="http://pychess-variants.herokuapp.com/") pychess-variants.com a(href="https://glukkazan.github.io/") Dagaz demo + serveur a(href="https://www.jocly.com/#/games") jocly.com + a(href="http://www.iggamecenter.com/") iggamecenter.com a(href="https://musketeerchess.net/home/index.html") musketeerchess.net a(href="https://schemingmind.com/") schemingmind.com a(href="https://echekk.fr/spip.php?page=rubrique&id_rubrique=1") echekk.fr diff --git a/client/src/translations/en.js b/client/src/translations/en.js index 06fadc9c..a52d34ec 100644 --- a/client/src/translations/en.js +++ b/client/src/translations/en.js @@ -214,6 +214,7 @@ export const translations = { "Explosive captures (v2)": "Explosive captures (v2)", "Extra bishops and knights": "Extra bishops and knights", "Faster development": "Faster development", + "First capture wins": "First capture wins", "Four new pieces": "Four new pieces", "Free initial setup": "Free initial setup", "Friendly pieces": "Friendly pieces", diff --git a/client/src/translations/es.js b/client/src/translations/es.js index cab69cf2..ce055713 100644 --- a/client/src/translations/es.js +++ b/client/src/translations/es.js @@ -214,6 +214,7 @@ export const translations = { "Explosive captures (v2)": "Capturas explosivas (v2)", "Extra bishops and knights": "Alfiles y caballos adicionales", "Faster development": "Desarrollo acelerado", + "First capture wins": "La primera captura gana", "Four new pieces": "Quatro nuevas piezas", "Free initial setup": "Posición inicial libre", "Friendly pieces": "Piezas amistosas", diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js index ba812a46..e94e1b29 100644 --- a/client/src/translations/fr.js +++ b/client/src/translations/fr.js @@ -214,6 +214,7 @@ export const translations = { "Explosive captures (v2)": "Captures explosives (v2)", "Extra bishops and knights": "Fous et cavaliers supplémentaires", "Faster development": "Développement accéléré", + "First capture wins": "La première capture gagne", "Four new pieces": "Quatre nouvelles pièces", "Free initial setup": "Position initiale libre", "Friendly pieces": "Pièces amies", diff --git a/client/src/translations/rules/Atarigo/en.pug b/client/src/translations/rules/Atarigo/en.pug new file mode 100644 index 00000000..20e560f0 --- /dev/null +++ b/client/src/translations/rules/Atarigo/en.pug @@ -0,0 +1,52 @@ +p.boxed + | The first player to capture something wins. + +p + | This game follows the rules of + a(href="https://en.wikipedia.org/wiki/Go_(game)") Weiqi + | , or Go in japanese. However, the first player to achieve a capture + | wins, and the board size is arbitrarily reduced to 12 x 12. + +h3 Rules summary + +p. + No diagonals are considered on the board. + Therefore, "adjacent" will mean "orthogonally adjacent". + +ul + li Players alternate turns, starting with Black. + li. + A move consists in putting a stone on an intersection of the board. + This stone will never move. However, it can be captured. + li. + A move adjacent to a connected group of enemy stones "kills" the group + if it has exactly one liberty left: its stones are removed from the board. + The capturing player thus wins. + +p. + Considering stones as vertices on a graph, linked by an edge if they + are adjacent, a connected group is a connected sub-graph. + On the diagram below, removing a stone at the marked location + breaks the connection. + +figure.diagram-container + .diagram.diag12 + | fen:93/93/93/2PPP2p4/4P2p4/3PP2p4/3P2ppp3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + .diagram.diag22 + | fen:93/93/93/2PPP2p4/4P2p4/4P2p4/3P2p1p3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + figcaption. + Left: both groups connected. + Right: "both disconnected" (2 groups for black, 3 for white). + +p. + The liberties of a group are all the free intersections adjacent to a stone + of the group, as illustrated. + +figure.diagram-container + .diagram + | fen:5pp5/93/93/93/3P8/3PP5pp/91p1/93/93/6P5/93/93 e12,f11,g11,h12,c7,c8,d9,e8,f7,e6,d6,g2,g4,f3,h3,k5,l6,j6,j7,k8,l8: + figcaption Surrounding marks indicate groups' liberties. + +p. + The initial configuration is formed by two pairs of groups of one stone + each, with only two liberties per group. Everything is disconnected. diff --git a/client/src/translations/rules/Atarigo/es.pug b/client/src/translations/rules/Atarigo/es.pug new file mode 100644 index 00000000..60a4e57e --- /dev/null +++ b/client/src/translations/rules/Atarigo/es.pug @@ -0,0 +1,52 @@ +p.boxed + | El primer jugador en capturar algo gana. + +p + | Este juego sigue las reglas de + a(href="https://en.wikipedia.org/wiki/Go_(game)") Weiqi + | o Go en japonés. Sin embargo, el primer jugador en capturar + | gana, y el tablero se reduce arbitrariamente a 12 x 12. + +h3 Resumen de reglas + +p. + Las diagonales no se consideran en el tablero. + Por tanto, "adyacente" significará "ortogonalmente adyacente". + +ul + li Los jugadores alternan turnos, comenzando con las negras. + li. + Un movimiento consiste en colocar una piedra en una intersección del + tablero. Esta piedra nunca se moverá. Sin embargo, se puede capturar. + li. + Un golpe adyacente a un grupo de piedras enemigas conectadas "mata" a este + grupo si sólo le queda una libertad: sus piedras se retiran del juego. + El jugador capturador gana. + +p. + Considerando piedras como vértices de un gráfico, conectados por un borde + si son adyacentes, un grupo conectado es un subgrafo conectado. + En el diagrama a continuación, retire una piedra en la ubicación marcada + romper la conexión. + +figure.diagram-container + .diagram.diag12 + | fen:93/93/93/2PPP2p4/4P2p4/3PP2p4/3P2ppp3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + .diagram.diag22 + | fen:93/93/93/2PPP2p4/4P2p4/4P2p4/3P2p1p3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + figcaption. + Izquierda: los dos grupos conectados. + Derecha: "ambos desconectados" (2 grupos para negro, 3 para blanco). + +p. + Las libertades de un grupo son todas las intersecciones libres adyacentes a + una piedra de grupo, como se muestra. + +figure.diagram-container + .diagram + | fen:5pp5/93/93/93/3P8/3PP5pp/91p1/93/93/6P5/93/93 e12,f11,g11,h12,c7,c8,d9,e8,f7,e6,d6,g2,g4,f3,h3,k5,l6,j6,j7,k8,l8: + figcaption Marcas circundantes que indican libertades grupales. + +p. + La configuración inicial está formada por dos pares de grupos de una piedra, + con solo dos libertades por grupo. Todo está desconectado. diff --git a/client/src/translations/rules/Atarigo/fr.pug b/client/src/translations/rules/Atarigo/fr.pug new file mode 100644 index 00000000..ff66d90f --- /dev/null +++ b/client/src/translations/rules/Atarigo/fr.pug @@ -0,0 +1,52 @@ +p.boxed + | Le premier joueur qui capture quelque chose gagne. + +p + | Ce jeu suit les règles du + a(href="https://en.wikipedia.org/wiki/Go_(game)") Weiqi + | , ou Go en japonais. Cependant, le premier joueur à effectuer une capture + | gagne, et le plateau est arbitrairement réduit à 12 x 12. + +h3 Résumé des règles + +p. + Les diagonales ne sont pas considérées sur le plateau. + Ainsi, "adjacent" signifiera "orthogonalement adjacent". + +ul + li Les joueurs alternent les tours, démarrant par les noirs. + li. + Un coup consiste à poser une pierre sur une intersection du plateau. + Cette pierre ne bougera jamais. Elle peut cependant être capturée. + li. + Un coup adjacent à un groupe de pierres ennemies connectées "tue" ce + groupe s'il ne lui reste plus qu'une seule liberté : ses pierres sont + retirées du jeu. Le joueur capturant gagne alors. + +p. + Considérant les pierres comme des sommets d'un graphe, reliées par une arête + si elles sont adjacentes, un groupe connecté est un sous-graphe connecté. + Sur le diagramme ci-dessous, enlever une pierre à l'emplacement marqué + casse la connection. + +figure.diagram-container + .diagram.diag12 + | fen:93/93/93/2PPP2p4/4P2p4/3PP2p4/3P2ppp3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + .diagram.diag22 + | fen:93/93/93/2PPP2p4/4P2p4/4P2p4/3P2p1p3/2PP2p1p3/2P5pp2/93/93/93 d7,h6: + figcaption. + Gauche : les deux groupes connectés. + Droite : "les deux déconnectés" (2 groupes pour noir, 3 pour blanc). + +p. + Les libertés d'un groupe sont toutes les intersections libres adjacentes à + une pierre du groupe, comme illustré. + +figure.diagram-container + .diagram + | fen:5pp5/93/93/93/3P8/3PP5pp/91p1/93/93/6P5/93/93 e12,f11,g11,h12,c7,c8,d9,e8,f7,e6,d6,g2,g4,f3,h3,k5,l6,j6,j7,k8,l8: + figcaption Les marques entourantes indiquant les libertés des groupes. + +p. + La configuration initiale est formée de deux paires de groupes à une pierre, + avec seulement deux libertés par groupe. Tout est déconnecté. diff --git a/client/src/translations/rules/Gomoku/en.pug b/client/src/translations/rules/Gomoku/en.pug index 3a33838b..903b3567 100644 --- a/client/src/translations/rules/Gomoku/en.pug +++ b/client/src/translations/rules/Gomoku/en.pug @@ -1,2 +1,16 @@ p.boxed - | TODO + | Align five stones in any direction. + +p. + At each turn, put a stone on the board, on any empty intersection. + Once on the board, it will not move. The first player to align + five stones (or more) in any direction wins the game. + However, if White (playing in second) can achieve such an alignment right + after Black, the game is a draw. + +figure.diagram-container + .diagram + | fen:991/991/991/991/991/94P5/93p6/7ppp1p7/9Pp8/9pP8/7PPP1P7/93P6/94p5/991/991/991/991/991/991 k9,k12: + figcaption. + If black plays at the lower marked point, + White replies at the other mark: draw. diff --git a/client/src/translations/rules/Gomoku/es.pug b/client/src/translations/rules/Gomoku/es.pug index 3a33838b..18ffcadb 100644 --- a/client/src/translations/rules/Gomoku/es.pug +++ b/client/src/translations/rules/Gomoku/es.pug @@ -1,2 +1,16 @@ p.boxed - | TODO + | Alinee cinco piedras en cualquier dirección. + +p. + En cada turno, coloque una piedra en el tablero, en una intersección vacía. + Una vez que la piedra esté en su lugar, no se moverá. El primer jugador en + alinear cinco (o más) piedras en cualquier dirección gana. + Sin embargo, si las blancas (jugando segundas) pueden alcanzar + este gol justo después de las negras, el juego es un empate. + +figure.diagram-container + .diagram + | fen:991/991/991/991/991/94P5/93p6/7ppp1p7/9Pp8/9pP8/7PPP1P7/93P6/94p5/991/991/991/991/991/991 k9,k12: + figcaption. + Si las negras juegan en el punto anotado más bajo, + el blanco responde a la otra marca: empate. diff --git a/client/src/translations/rules/Gomoku/fr.pug b/client/src/translations/rules/Gomoku/fr.pug index 3a33838b..14c32ee5 100644 --- a/client/src/translations/rules/Gomoku/fr.pug +++ b/client/src/translations/rules/Gomoku/fr.pug @@ -1,2 +1,16 @@ p.boxed - | TODO + | Alignez cinq pierres dans n'importe quelle direction. + +p. + À chaque tour, placez une pierre sur le plateau, sur une intersection vide. + Une fois la pierre en place elle ne bougera plus. Le premier joueur à + aligner cinq pierres (ou plus) dans n'importe quelle direction remporte + la victoire. Cependant, si les blancs (jouant en second) peuvent attindre + ce but juste après les noirs, la partie est nulle. + +figure.diagram-container + .diagram + | fen:991/991/991/991/991/94P5/93p6/7ppp1p7/9Pp8/9pP8/7PPP1P7/93P6/94p5/991/991/991/991/991/991 k9,k12: + figcaption. + Si les noirs jouent au point marqué inférieur, + les blancs répondent à l'autre marque : nulle. diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug index 552652ff..eca4de1a 100644 --- a/client/src/translations/variants/en.pug +++ b/client/src/translations/variants/en.pug @@ -438,6 +438,7 @@ h3 Non-chess p Some games not chess related. - var varlist = [ + "Atarigo", "Emergo", "Fanorona", "Gomoku", diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug index 57ba50e8..9da45160 100644 --- a/client/src/translations/variants/es.pug +++ b/client/src/translations/variants/es.pug @@ -448,6 +448,7 @@ h3 Aparte del Ajedrez p Algunos juegos no están relacionados con el ajedrez. - var varlist = [ + "Atarigo", "Emergo", "Fanorona", "Gomoku", diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug index cebc6221..1f75160c 100644 --- a/client/src/translations/variants/fr.pug +++ b/client/src/translations/variants/fr.pug @@ -446,6 +446,7 @@ h3 Hors Échecs p Quelques jeux non connectés aux échecs. - var varlist = [ + "Atarigo", "Emergo", "Fanorona", "Gomoku", diff --git a/client/src/variants/Atarigo.js b/client/src/variants/Atarigo.js new file mode 100644 index 00000000..2e50922d --- /dev/null +++ b/client/src/variants/Atarigo.js @@ -0,0 +1,225 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; +import { ArrayFun } from "@/utils/array"; + +export class AtarigoRules extends ChessRules { + + static get Monochrome() { + return true; + } + + static get Notoodark() { + return true; + } + + static get Lines() { + let lines = []; + // Draw all inter-squares lines, shifted: + for (let i = 0; i < V.size.x; i++) + lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]); + for (let j = 0; j < V.size.y; j++) + lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]); + return lines; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get ReverseColors() { + return true; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (row[i].toLowerCase() == V.PAWN) sumElts++; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 3) Check capture "flag" + if (!fenParsed.capture || !fenParsed.capture.match(/^[01]$/)) + return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + // Capture field allows to compute the score cleanly. + { capture: fenParts[3] } + ); + } + + static get size() { + return { x: 12, y: 12 }; + } + + static GenRandInitFen() { + return "93/93/93/93/93/5Pp5/5pP5/93/93/93/93/93 w 0 0"; + } + + getFen() { + return super.getFen() + " " + (this.capture ? 1 : 0); + } + + setOtherVariables(fen) { + this.capture = parseInt(V.ParseFen(fen).capture, 10); + } + + getPiece() { + return V.PAWN; + } + + getPpath(b) { + return "Gomoku/" + b; + } + + onlyClick() { + return true; + } + + canIplay(side, [x, y]) { + return (side == this.turn && this.board[x][y] == V.EMPTY); + } + + hoverHighlight([x, y], side) { + if (!!side && side != this.turn) return false; + return (this.board[x][y] == V.EMPTY); + } + + searchForEmptySpace([x, y], color, explored) { + if (explored[x][y]) return false; //didn't find empty space + explored[x][y] = true; + let res = false; + for (let s of V.steps[V.ROOK]) { + const [i, j] = [x + s[0], y + s[1]]; + if (V.OnBoard(i, j)) { + if (this.board[i][j] == V.EMPTY) res = true; + else if (this.getColor(i, j) == color) + res = this.searchForEmptySpace([i, j], color, explored) || res; + } + } + return res; + } + + doClick([x, y]) { + const color = this.turn; + const oppCol = V.GetOppCol(color); + let move = new Move({ + appear: [ + new PiPo({ x: x, y: y, c: color, p: V.PAWN }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + V.PlayOnBoard(this.board, move); //put the stone + let noSuicide = false; + let captures = []; + for (let s of V.steps[V.ROOK]) { + const [i, j] = [x + s[0], y + s[1]]; + if (V.OnBoard(i, j)) { + if (this.board[i][j] == V.EMPTY) noSuicide = true; //clearly + else if (this.getColor(i, j) == color) { + // Free space for us = not a suicide + if (!noSuicide) { + let explored = ArrayFun.init(V.size.x, V.size.y, false); + noSuicide = this.searchForEmptySpace([i, j], color, explored); + } + } + else { + // Free space for opponent = not a capture + let explored = ArrayFun.init(V.size.x, V.size.y, false); + const captureSomething = + !this.searchForEmptySpace([i, j], oppCol, explored); + if (captureSomething) { + for (let ii = 0; ii < V.size.x; ii++) { + for (let jj = 0; jj < V.size.y; jj++) { + if (explored[ii][jj]) { + captures.push( + new PiPo({ x: ii, y: jj, c: oppCol, p: V.PAWN }) + ); + } + } + } + } + } + } + } + V.UndoOnBoard(this.board, move); //remove the stone + if (!noSuicide && captures.length == 0) return null; + Array.prototype.push.apply(move.vanish, captures); + return move; + } + + getPotentialMovesFrom([x, y]) { + const move = this.doClick([x, y]); + return (!move ? [] : [move]); + } + + getAllPotentialMoves() { + let moves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if (this.board[i][j] == V.EMPTY) { + const mv = this.doClick(i, j); + if (!!mv) moves.push(mv); + } + } + } + return moves; + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + postPlay(move) { + if (move.vanish.length >= 1) this.capture = true; + } + + postUndo() { + this.capture = false; + } + + getCurrentScore() { + if (this.capture) return (this.turn == 'w' ? "0-1" : "1-0"); + return "*"; + } + + getComputerMove() { + const moves = super.getAllValidMoves(); + if (moves.length == 0) return null; + // Just random mover for now... writing a good bot is far out of scope + return moves[randInt(moves.length)]; + } + + getNotation(move) { + return V.CoordsToSquare(move.end); + } + +}; diff --git a/client/src/variants/Atomic2.js b/client/src/variants/Atomic2.js index f97be01c..54ffb17a 100644 --- a/client/src/variants/Atomic2.js +++ b/client/src/variants/Atomic2.js @@ -23,7 +23,7 @@ export class Atomic2Rules extends Atomic1Rules { return super.getPotentialMovesFrom([x, y]); } - hoverHighlight(x, y) { + hoverHighlight([x, y]) { return this.movesCount == 0 && [1, 6].includes(x); } diff --git a/client/src/variants/Bario.js b/client/src/variants/Bario.js index 21965e16..fcdb44d9 100644 --- a/client/src/variants/Bario.js +++ b/client/src/variants/Bario.js @@ -35,7 +35,7 @@ export class BarioRules extends ChessRules { ); } - hoverHighlight(x, y) { + hoverHighlight([x, y]) { const c = this.turn; return ( this.movesCount <= 1 && diff --git a/client/src/variants/Chakart.js b/client/src/variants/Chakart.js index e5473d7c..f170f707 100644 --- a/client/src/variants/Chakart.js +++ b/client/src/variants/Chakart.js @@ -30,7 +30,7 @@ export class ChakartRules extends ChessRules { return true; } - hoverHighlight(x, y) { + hoverHighlight([x, y]) { if (this.subTurn == 1) return false; const L = this.firstMove.length; const fm = this.firstMove[L-1]; diff --git a/client/src/variants/Emergo.js b/client/src/variants/Emergo.js index 0d81759c..264186b3 100644 --- a/client/src/variants/Emergo.js +++ b/client/src/variants/Emergo.js @@ -3,5 +3,11 @@ import { ChessRules } from "@/base_rules"; export class YoteRules extends ChessRules { // TODO + //If (as white) a pile W1/B1 jumps over another pile W2/B2, it lets on the intermediate square exactly W2 men, to end as W1/(B1+B2). + //In the first case in the video, W1=1, B1=0, W2=0, B2=1 ==> 1/1 and finally 1/2 with nothing on intermediate squares since W2 is always 0. + //In the second case, W1=1, B1=0, W2=1, B2=1 ==> 1 man left on intermediate square, end as 1/1. + //...I think it's that (?). Not very well explained either on Wikipedia or mindsports.nl :/ + //Found this link: http://www.iggamecenter.com/info/en/emergo.html - so it's all clear now ! I'll add the game soon. + //Btw, I'm not a big fan of this naming "men" for pieces, but, won't contradict the author on that  }; diff --git a/client/src/variants/Gomoku.js b/client/src/variants/Gomoku.js index 5bf971a8..f0032945 100644 --- a/client/src/variants/Gomoku.js +++ b/client/src/variants/Gomoku.js @@ -1,7 +1,217 @@ -import { ChessRules } from "@/base_rules"; +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; export class GomokuRules extends ChessRules { - // TODO + static get Monochrome() { + return true; + } + + static get Notoodark() { + return true; + } + + static get Lines() { + let lines = []; + // Draw all inter-squares lines, shifted: + for (let i = 0; i < V.size.x; i++) + lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]); + for (let j = 0; j < V.size.y; j++) + lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]); + return lines; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get ReverseColors() { + return true; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (row[i].toLowerCase() == V.PAWN) sumElts++; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + static get size() { + return { x: 19, y: 19 }; + } + + static GenRandInitFen() { + return [...Array(19)].map(e => "991").join('/') + " w 0"; + } + + setOtherVariables() {} + + getPiece() { + return V.PAWN; + } + + getPpath(b) { + return "Gomoku/" + b; + } + + onlyClick() { + return true; + } + + canIplay(side, [x, y]) { + return (side == this.turn && this.board[x][y] == V.EMPTY); + } + + hoverHighlight([x, y], side) { + if (!!side && side != this.turn) return false; + return (this.board[x][y] == V.EMPTY); + } + + doClick([x, y]) { + return ( + new Move({ + appear: [ + new PiPo({ x: x, y: y, c: this.turn, p: V.PAWN }) + ], + vanish: [], + start: { x: -1, y: -1 }, + }) + ); + } + + getPotentialMovesFrom([x, y]) { + return [this.doClick([x, y])]; + } + + getAllPotentialMoves() { + let moves = []; + for (let i = 0; i < 19; i++) { + for (let j=0; j < 19; j++) { + if (this.board[i][j] == V.EMPTY) moves.push(this.doClick(i, j)); + } + } + return moves; + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + postPlay() {} + postUndo() {} + + countAlignedStones([x, y], color) { + let maxInLine = 0; + for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + // Skip half of steps, since we explore both directions + if (s[0] == -1 || (s[0] == 0 && s[1] == -1)) continue; + let countInLine = 1; + for (let dir of [-1, 1]) { + let [i, j] = [x + dir * s[0], y + dir * s[1]]; + while ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + countInLine++; + i += dir * s[0]; + j += dir * s[1]; + } + } + if (countInLine > maxInLine) maxInLine = countInLine; + } + return maxInLine; + } + + getCurrentScore() { + let fiveAlign = { w: false, b: false, wNextTurn: false }; + for (let i=0; i<19; i++) { + for (let j=0; j<19; j++) { + if (this.board[i][j] == V.EMPTY) { + if ( + !fiveAlign.wNextTurn && + this.countAlignedStones([i, j], 'b') >= 5 + ) { + fiveAlign.wNextTurn = true; + } + } + else { + const c = this.getColor(i, j); + if (!fiveAlign[c] && this.countAlignedStones([i, j], c) >= 5) + fiveAlign[c] = true; + } + } + } + if (fiveAlign['w']) { + if (fiveAlign['b']) return "1/2"; + if (this.turn == 'b' && fiveAlign.wNextTurn) return "*"; + return "1-0"; + } + if (fiveAlign['b']) return "0-1"; + return "*"; + } + + getComputerMove() { + const color = this.turn; + let candidates = []; + let curMax = 0; + for (let i=0; i<19; i++) { + for (let j=0; j<19; j++) { + if (this.board[i][j] == V.EMPTY) { + const nbAligned = this.countAlignedStones([i, j], color); + if (nbAligned >= curMax) { + const move = new Move({ + appear: [ + new PiPo({ x: i, y: j, c: color, p: V.PAWN }) + ], + vanish: [], + start: { x: -1, y: -1 } + }); + if (nbAligned > curMax) { + curMax = nbAligned; + candidates = [move]; + } + else candidates.push(move); + } + } + } + } + // Among a priori equivalent moves, select the most central ones. + // Of course this is not good, but can help this ultra-basic bot. + let bestCentrality = 0; + candidates.forEach(c => { + const deltaX = Math.min(c.end.x, 18 - c.end.x); + const deltaY = Math.min(c.end.y, 18 - c.end.y); + c.centrality = deltaX * deltaX + deltaY * deltaY; + if (c.centrality > bestCentrality) bestCentrality = c.centrality; + }); + const threshold = Math.min(32, bestCentrality); + const finalCandidates = candidates.filter(c => c.centrality >= threshold); + return finalCandidates[randInt(finalCandidates.length)]; + } + + getNotation(move) { + return V.CoordsToSquare(move.end); + } }; diff --git a/client/src/variants/Hamilton.js b/client/src/variants/Hamilton.js index 88f67139..47af232a 100644 --- a/client/src/variants/Hamilton.js +++ b/client/src/variants/Hamilton.js @@ -19,7 +19,7 @@ export class HamiltonRules extends ChessRules { return "xx"; } - hoverHighlight(x, y) { + hoverHighlight() { return this.movesCount == 0; } diff --git a/client/src/variants/Konane.js b/client/src/variants/Konane.js index 357bae75..1daad7af 100644 --- a/client/src/variants/Konane.js +++ b/client/src/variants/Konane.js @@ -53,9 +53,9 @@ export class KonaneRules extends ChessRules { this.captures = []; //reinit for each move } - hoverHighlight(x, y) { - if (this.movesCount >= 2) return false; + hoverHighlight([x, y], side) { const c = this.turn; + if (this.movesCount >= 2 || (!!side && side != c)) return false; if (c == 'w') return (x == y && [0, 3, 4, 7].includes(x)); // "Black": search for empty square and allow nearby for (let i of [0, 3, 4, 7]) { diff --git a/client/src/variants/Madhouse.js b/client/src/variants/Madhouse.js index 5f90904e..72c41cd8 100644 --- a/client/src/variants/Madhouse.js +++ b/client/src/variants/Madhouse.js @@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea"; export class MadhouseRules extends ChessRules { - hoverHighlight(x, y) { + hoverHighlight([x, y]) { // Testing move validity results in an infinite update loop. // TODO: find a way to test validity anyway. return (this.subTurn == 2 && this.board[x][y] == V.EMPTY); diff --git a/client/src/variants/Pocketknight.js b/client/src/variants/Pocketknight.js index f40265e6..41dbb725 100644 --- a/client/src/variants/Pocketknight.js +++ b/client/src/variants/Pocketknight.js @@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea"; export class PocketknightRules extends ChessRules { - hoverHighlight(x, y) { + hoverHighlight([x, y]) { // Testing move validity results in an infinite update loop. // TODO: find a way to test validity anyway. return (this.subTurn == 2 && this.board[x][y] == V.EMPTY); diff --git a/client/src/variants/Teleport.js b/client/src/variants/Teleport.js index 82528c18..cbf6a786 100644 --- a/client/src/variants/Teleport.js +++ b/client/src/variants/Teleport.js @@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea"; export class TeleportRules extends ChessRules { - hoverHighlight(x, y) { + hoverHighlight([x, y]) { // Testing move validity results in an infinite update loop. // TODO: find a way to test validity anyway. return (this.subTurn == 2 && this.board[x][y] == V.EMPTY); diff --git a/client/src/variants/Titan.js b/client/src/variants/Titan.js index 9e651b0d..f8d626eb 100644 --- a/client/src/variants/Titan.js +++ b/client/src/variants/Titan.js @@ -185,7 +185,7 @@ export class TitanRules extends ChessRules { return moves; } - hoverHighlight(x, y) { + hoverHighlight([x, y]) { const c = this.turn; return ( this.movesCount <= 3 && diff --git a/client/src/variants/Yote.js b/client/src/variants/Yote.js index 76d7c2e9..d836bbd6 100644 --- a/client/src/variants/Yote.js +++ b/client/src/variants/Yote.js @@ -167,8 +167,8 @@ export class YoteRules extends ChessRules { return (x < V.size.x && this.getColor(x, y) != side); } - // TODO: hoverHighlight() would well take an arg "side"... - hoverHighlight(x, y) { + hoverHighlight([x, y], side) { + if (!!side && side != this.turn) return false; const L = this.captures.length; if (!this.captures[L-1]) return false; const oppCol = V.GetOppCol(this.turn); diff --git a/server/db/populate.sql b/server/db/populate.sql index e419e902..cf0f8c52 100644 --- a/server/db/populate.sql +++ b/server/db/populate.sql @@ -21,6 +21,7 @@ insert or ignore into Variants (name, description) values ('Antiking2', 'Keep antiking in check (v2)'), ('Antimatter', 'Dangerous collisions'), ('Arena', 'Middle battle'), + ('Atarigo', 'First capture wins'), ('Atomic1', 'Explosive captures (v1)'), ('Atomic2', 'Explosive captures (v2)'), ('Avalanche', 'Pawnfalls'), -- 2.44.0