Add Gomoku + Atarigo
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 19 Jan 2021 02:29:08 +0000 (03:29 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 19 Jan 2021 02:29:08 +0000 (03:29 +0100)
34 files changed:
client/public/images/pieces/Gomoku/bp.svg [new symlink]
client/public/images/pieces/Gomoku/wp.svg [new symlink]
client/src/base_rules.js
client/src/components/Board.vue
client/src/styles/_board_squares_img.sass
client/src/translations/about/en.pug
client/src/translations/about/es.pug
client/src/translations/about/fr.pug
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/translations/rules/Atarigo/en.pug [new file with mode: 0644]
client/src/translations/rules/Atarigo/es.pug [new file with mode: 0644]
client/src/translations/rules/Atarigo/fr.pug [new file with mode: 0644]
client/src/translations/rules/Gomoku/en.pug
client/src/translations/rules/Gomoku/es.pug
client/src/translations/rules/Gomoku/fr.pug
client/src/translations/variants/en.pug
client/src/translations/variants/es.pug
client/src/translations/variants/fr.pug
client/src/variants/Atarigo.js [new file with mode: 0644]
client/src/variants/Atomic2.js
client/src/variants/Bario.js
client/src/variants/Chakart.js
client/src/variants/Emergo.js
client/src/variants/Gomoku.js
client/src/variants/Hamilton.js
client/src/variants/Konane.js
client/src/variants/Madhouse.js
client/src/variants/Pocketknight.js
client/src/variants/Teleport.js
client/src/variants/Titan.js
client/src/variants/Yote.js
server/db/populate.sql

diff --git a/client/public/images/pieces/Gomoku/bp.svg b/client/public/images/pieces/Gomoku/bp.svg
new file mode 120000 (symlink)
index 0000000..0d6535f
--- /dev/null
@@ -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 (symlink)
index 0000000..dc5717d
--- /dev/null
@@ -0,0 +1 @@
+../Yote/wp.svg
\ No newline at end of file
index e8a82f9..aff4710 100644 (file)
@@ -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;
       // 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++) {
     };
     let position = "";
     for (let i = 0; i < V.size.x; i++) {
index b452808..9e23fc5 100644 (file)
@@ -194,7 +194,9 @@ export default {
                     showCheck && lightSquare && incheckSq[ci][cj],
                   "incheck-dark":
                     showCheck && !lightSquare && incheckSq[ci][cj],
                     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 })
                 },
                 attrs: {
                   id: getSquareId({ x: ci, y: cj })
@@ -575,11 +577,12 @@ export default {
       return path;
     },
     re_setDrawings: function() {
       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);
       // 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 = [];
       const squareWidth = boardElt.offsetWidth / V.size.y;
       const bPos = boardElt.getBoundingClientRect();
       let svgArrows = [];
index e39290d..3dc3a12 100644 (file)
@@ -43,6 +43,10 @@ div.board12
   width: 8.33%
   padding-bottom: 8.33%
 
   width: 8.33%
   padding-bottom: 8.33%
 
+div.board19
+  width: 5.26%
+  padding-bottom: 5.26%
+
 img.piece
   width: 100%
   z-index: 10
 img.piece
   width: 100%
   z-index: 10
index 68da499..50c3895 100644 (file)
@@ -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://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
   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
index d596b00..2d23610 100644 (file)
@@ -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://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
   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
index f682df8..fe74c76 100644 (file)
@@ -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://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
   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
index 06fadc9..a52d34e 100644 (file)
@@ -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",
   "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",
   "Four new pieces": "Four new pieces",
   "Free initial setup": "Free initial setup",
   "Friendly pieces": "Friendly pieces",
index cab69cf..ce05571 100644 (file)
@@ -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",
   "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",
   "Four new pieces": "Quatro nuevas piezas",
   "Free initial setup": "Posición inicial libre",
   "Friendly pieces": "Piezas amistosas",
index ba812a4..e94e1b2 100644 (file)
@@ -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é",
   "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",
   "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 (file)
index 0000000..20e560f
--- /dev/null
@@ -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 (file)
index 0000000..60a4e57
--- /dev/null
@@ -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 (file)
index 0000000..ff66d90
--- /dev/null
@@ -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é.
index 3a33838..903b356 100644 (file)
@@ -1,2 +1,16 @@
 p.boxed
 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.
index 3a33838..18ffcad 100644 (file)
@@ -1,2 +1,16 @@
 p.boxed
 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.
index 3a33838..14c32ee 100644 (file)
@@ -1,2 +1,16 @@
 p.boxed
 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.
index 552652f..eca4de1 100644 (file)
@@ -438,6 +438,7 @@ h3 Non-chess
 p Some games not chess related.
 -
   var varlist = [
 p Some games not chess related.
 -
   var varlist = [
+    "Atarigo",
     "Emergo",
     "Fanorona",
     "Gomoku",
     "Emergo",
     "Fanorona",
     "Gomoku",
index 57ba50e..9da4516 100644 (file)
@@ -448,6 +448,7 @@ h3 Aparte del Ajedrez
 p Algunos juegos no están relacionados con el ajedrez.
 -
   var varlist = [
 p Algunos juegos no están relacionados con el ajedrez.
 -
   var varlist = [
+    "Atarigo",
     "Emergo",
     "Fanorona",
     "Gomoku",
     "Emergo",
     "Fanorona",
     "Gomoku",
index cebc622..1f75160 100644 (file)
@@ -446,6 +446,7 @@ h3 Hors Échecs
 p Quelques jeux non connectés aux échecs.
 -
   var varlist = [
 p Quelques jeux non connectés aux échecs.
 -
   var varlist = [
+    "Atarigo",
     "Emergo",
     "Fanorona",
     "Gomoku",
     "Emergo",
     "Fanorona",
     "Gomoku",
diff --git a/client/src/variants/Atarigo.js b/client/src/variants/Atarigo.js
new file mode 100644 (file)
index 0000000..2e50922
--- /dev/null
@@ -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);
+  }
+
+};
index f97be01..54ffb17 100644 (file)
@@ -23,7 +23,7 @@ export class Atomic2Rules extends Atomic1Rules {
     return super.getPotentialMovesFrom([x, y]);
   }
 
     return super.getPotentialMovesFrom([x, y]);
   }
 
-  hoverHighlight(x, y) {
+  hoverHighlight([x, y]) {
     return this.movesCount == 0 && [1, 6].includes(x);
   }
 
     return this.movesCount == 0 && [1, 6].includes(x);
   }
 
index 21965e1..fcdb44d 100644 (file)
@@ -35,7 +35,7 @@ export class BarioRules extends ChessRules {
     );
   }
 
     );
   }
 
-  hoverHighlight(x, y) {
+  hoverHighlight([x, y]) {
     const c = this.turn;
     return (
       this.movesCount <= 1 &&
     const c = this.turn;
     return (
       this.movesCount <= 1 &&
index e5473d7..f170f70 100644 (file)
@@ -30,7 +30,7 @@ export class ChakartRules extends ChessRules {
     return true;
   }
 
     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];
     if (this.subTurn == 1) return false;
     const L = this.firstMove.length;
     const fm = this.firstMove[L-1];
index 0d81759..264186b 100644 (file)
@@ -3,5 +3,11 @@ import { ChessRules } from "@/base_rules";
 export class YoteRules extends ChessRules {
 
   // TODO
 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 
 
 };
 
 };
index 5bf971a..f003294 100644 (file)
@@ -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 {
 
 
 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);
+  }
 
 };
 
 };
index 88f6713..47af232 100644 (file)
@@ -19,7 +19,7 @@ export class HamiltonRules extends ChessRules {
     return "xx";
   }
 
     return "xx";
   }
 
-  hoverHighlight(x, y) {
+  hoverHighlight() {
     return this.movesCount == 0;
   }
 
     return this.movesCount == 0;
   }
 
index 357bae7..1daad7a 100644 (file)
@@ -53,9 +53,9 @@ export class KonaneRules extends ChessRules {
     this.captures = []; //reinit for each move
   }
 
     this.captures = []; //reinit for each move
   }
 
-  hoverHighlight(x, y) {
-    if (this.movesCount >= 2) return false;
+  hoverHighlight([x, y], side) {
     const c = this.turn;
     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]) {
     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]) {
index 5f90904..72c41cd 100644 (file)
@@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea";
 
 export class MadhouseRules extends ChessRules {
 
 
 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);
     // 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);
index f40265e..41dbb72 100644 (file)
@@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea";
 
 export class PocketknightRules extends ChessRules {
 
 
 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);
     // 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);
index 82528c1..cbf6a78 100644 (file)
@@ -3,7 +3,7 @@ import { randInt } from "@/utils/alea";
 
 export class TeleportRules extends ChessRules {
 
 
 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);
     // 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);
index 9e651b0..f8d626e 100644 (file)
@@ -185,7 +185,7 @@ export class TitanRules extends ChessRules {
     return moves;
   }
 
     return moves;
   }
 
-  hoverHighlight(x, y) {
+  hoverHighlight([x, y]) {
     const c = this.turn;
     return (
       this.movesCount <= 3 &&
     const c = this.turn;
     return (
       this.movesCount <= 3 &&
index 76d7c2e..d836bbd 100644 (file)
@@ -167,8 +167,8 @@ export class YoteRules extends ChessRules {
     return (x < V.size.x && this.getColor(x, y) != side);
   }
 
     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);
     const L = this.captures.length;
     if (!this.captures[L-1]) return false;
     const oppCol = V.GetOppCol(this.turn);
index e419e90..cf0f8c5 100644 (file)
@@ -21,6 +21,7 @@ insert or ignore into Variants (name, description) values
   ('Antiking2', 'Keep antiking in check (v2)'),
   ('Antimatter', 'Dangerous collisions'),
   ('Arena', 'Middle battle'),
   ('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'),
   ('Atomic1', 'Explosive captures (v1)'),
   ('Atomic2', 'Explosive captures (v2)'),
   ('Avalanche', 'Pawnfalls'),