From 7c05a5f2297bea540c700ebceb0cc8b03a7f6775 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
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