From 4a2093139089632727de4f510127ef186cab528e Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 4 Jan 2021 17:44:48 +0100
Subject: [PATCH] Finish Pacosako + add GameStat table to know how many live
 games are played

---
 .gitignore                                    |   2 +
 client/public/variants/Pacosako/manual.pdf    |   1 +
 client/src/base_rules.js                      |   5 +-
 client/src/styles/_rules.sass                 |   5 +
 client/src/translations/rules/Pacosako/en.pug | 107 ++++-
 client/src/translations/rules/Pacosako/es.pug | 111 +++++-
 client/src/translations/rules/Pacosako/fr.pug | 110 ++++-
 client/src/variants/Pacosako.js               | 376 +++++++++++++++---
 client/src/views/Game.vue                     |  46 ++-
 client/src/views/Hall.vue                     |  30 +-
 server/db/create.sql                          |   6 +
 server/db/dbconnect.py.dist                   |  20 +
 server/db/sync_gamestat.py                    |  25 ++
 server/models/Game.js                         |  10 +
 server/routes/games.js                        |   8 +
 15 files changed, 746 insertions(+), 116 deletions(-)
 create mode 100644 client/public/variants/Pacosako/manual.pdf
 create mode 100644 server/db/dbconnect.py.dist
 create mode 100755 server/db/sync_gamestat.py

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