From 4258b58c6aff86ce69ebfbcd40d704836df27ac9 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 6 Jan 2021 11:41:02 +0100
Subject: [PATCH] Some fixes + draft Avalanche

---
 client/src/base_rules.js                      |   1 +
 client/src/translations/en.js                 |   1 +
 client/src/translations/es.js                 |   1 +
 client/src/translations/fr.js                 |   1 +
 .../src/translations/rules/Avalanche/en.pug   |  34 ++
 .../src/translations/rules/Avalanche/es.pug   |  37 ++
 .../src/translations/rules/Avalanche/fr.pug   |  36 ++
 client/src/translations/variants/en.pug       |   1 +
 client/src/translations/variants/es.pug       |   1 +
 client/src/translations/variants/fr.pug       |   1 +
 client/src/variants/Avalanche.js              | 382 ++++++++++++++++++
 client/src/variants/Isardam.js                |   4 +
 client/src/variants/Pacosako.js               |  76 ++--
 client/src/views/Hall.vue                     |   4 +-
 server/db/populate.sql                        |   1 +
 15 files changed, 538 insertions(+), 43 deletions(-)
 create mode 100644 client/src/translations/rules/Avalanche/en.pug
 create mode 100644 client/src/translations/rules/Avalanche/es.pug
 create mode 100644 client/src/translations/rules/Avalanche/fr.pug
 create mode 100644 client/src/variants/Avalanche.js

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 5f47904f..0f18dbec 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -1173,6 +1173,7 @@ export const ChessRules = class ChessRules {
   }
 
   updateCastleFlags(move, piece, color) {
+    // TODO: check flags. If already off, no need to always re-evaluate
     const c = color || V.GetOppCol(this.turn);
     const firstRank = (c == "w" ? V.size.x - 1 : 0);
     // Update castling flags if rooks are moved
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index dad1e8c0..59801a78 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -246,6 +246,7 @@ export const translations = {
   "Non-conformism and utopia": "Non-conformism and utopia",
   "Occupy the enemy palace": "Occupy the enemy palace",
   "Paralyzed pieces": "Paralyzed pieces",
+  "Pawnfalls": "Pawnfalls",
   "Pawns capture backward": "Pawns capture backward",
   "Pawns move diagonally": "Pawns move diagonally",
   "Pieces upside down": "Pieces upside down",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index c1658bc0..5e96bf4e 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -246,6 +246,7 @@ export const translations = {
   "Non-conformism and utopia": "No-conformismo y utopía",
   "Occupy the enemy palace": "Ocupar el palacio enemigo",
   "Paralyzed pieces": "Piezas paralizadas",
+  "Pawnfalls": "Peones cayendo",
   "Pawns capture backward": "Los peones capturan hacia atrás",
   "Pawns move diagonally": "Los peones se mueven en diagonal",
   "Pieces upside down": "Piezas al revés",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 630437d3..f5e9e6a1 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -246,6 +246,7 @@ export const translations = {
   "Non-conformism and utopia": "Non-conformisme et utopie",
   "Occupy the enemy palace": "Occuper le palais ennemi",
   "Paralyzed pieces": "Pièces paralysées",
+  "Pawnfalls": "Chutes de pions",
   "Pawns capture backward": "Les pions capturent en arrière",
   "Pawns move diagonally": "Les pions vont en diagonale",
   "Pieces upside down": "Pièces à l'envers",
diff --git a/client/src/translations/rules/Avalanche/en.pug b/client/src/translations/rules/Avalanche/en.pug
new file mode 100644
index 00000000..8469d057
--- /dev/null
+++ b/client/src/translations/rules/Avalanche/en.pug
@@ -0,0 +1,34 @@
+p.boxed
+  | A normal move is always followed by an opponent's pawn push.
+
+p.
+  Each turn consists of two parts. The first part is a move
+  legal with the orthodox chess rules. The second part of the move,
+  called "push", consists of moving an opponent's pawn a single space forward
+  (toward the player in turn).
+  White has no pawn push on the first move ("balanced" Avalanche).
+
+p.
+  If no pawn can advance at the second part, then it's
+  opponent's turn. If a pushed pawn reaches its last rank,
+  the opponent decides next in which piece it promotes.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3Pp2/8/1PPPKPPP/RNBQ1BNR:
+  .diagram.diag22
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3P3/2N2p2/1PPPKPPP/R1BQ1BNR:
+  figcaption.
+    White plays Nc3,f3 (bad idea). Then Black can capture the king.
+
+p In some cases, kings can be captured. This counts as a win.
+
+p There are no en-passant captures.
+
+h3 More information
+
+p
+  | See 
+  a(href="https://www.chessvariants.com/mvopponent.dir/avalanche.html")
+    | Avalanche Chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Avalanche/es.pug b/client/src/translations/rules/Avalanche/es.pug
new file mode 100644
index 00000000..00ccc05a
--- /dev/null
+++ b/client/src/translations/rules/Avalanche/es.pug
@@ -0,0 +1,37 @@
+p.boxed
+  | Un movimiento normal siempre va seguido de un empujón de un peón contrario.
+
+p.
+  Cada ronda consta de dos partes. El primero es un movimiento legal según
+  reglas ortodoxas. La segunda parte, denominada "empujar", consta de
+  mueve un peón enemigo hacia adelante una casilla (hacia el jugador al turno).
+  Las blancas no tienen un empujón en la primera jugada
+  (Avalancha "Equilibrada").
+
+p.
+  Si ningún peón puede avanzar en el segundo parte, entonces el turno
+  pasa al oponente. Si un peón empujado llega a su última fila,
+  luego su dueño decide en qué pieza lo asciende.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3Pp2/8/1PPPKPPP/RNBQ1BNR:
+  .diagram.diag22
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3P3/2N2p2/1PPPKPPP/R1BQ1BNR:
+  figcaption.
+    Las blancas juegan Nc3,f3 (mala idea).
+    Las negras pueden entonces llevarse al rey.
+
+p.
+  En algunos casos, se pueden capturar reyes.
+  Cuenta como una victoria.
+
+p No hay capturas en-passant.
+
+h3 Más información
+
+p
+  | Ver 
+  a(href="https://www.chessvariants.com/mvopponent.dir/avalanche.html")
+    | Avalanche Chess
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Avalanche/fr.pug b/client/src/translations/rules/Avalanche/fr.pug
new file mode 100644
index 00000000..652df0fe
--- /dev/null
+++ b/client/src/translations/rules/Avalanche/fr.pug
@@ -0,0 +1,36 @@
+p.boxed
+  | Un coup normal est toujours suivi d'une poussée d'un pion adverse.
+
+p.
+  Chaque tour consiste en deux parties. La première est un coup légal selon
+  les règles orthodoxes. La seconde partie, appelée "poussée", consiste à
+  déplacer un pion ennemi d'une case vers l'avant (vers le joueur au trait).
+  Les blancs n'ont pas de poussée au premier coup (Avalanche "équilibrée").
+
+p.
+  Si aucun pion ne peut avancer lors de la seconde partie, alors le tour
+  passe à l'adversaire. Si un pion poussé atteint sa dernière rangée,
+  alors son propriétaire décide en quelle pièce il est promu.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3Pp2/8/1PPPKPPP/RNBQ1BNR:
+  .diagram.diag22
+    | fen:rnbqkbnr/1pppp1pp/8/p7/P3P3/2N2p2/1PPPKPPP/R1BQ1BNR:
+  figcaption.
+    Les blancs jouent Nc3,f3 (mauvaise idée).
+    Les noirs peuvent alors prendre le roi.
+
+p.
+  Dans certains cas les rois peuvent être capturés.
+  Cela compte comme une victoire.
+
+p Il n'y a pas de captures en passant.
+
+h3 Plus d'information
+
+p
+  | Voir 
+  a(href="https://www.chessvariants.com/mvopponent.dir/avalanche.html")
+    | Avalanche Chess
+  | &nbsp;sur chessvariants.com.
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index fad590cf..6850dc93 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -391,6 +391,7 @@ p.
   var varlist = [
     "Alice",
     "Ambiguous",
+    "Avalanche",
     "Bicolour",
     "Castle",
     "Doublearmy",
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index 2b30fc32..d63fe46a 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -402,6 +402,7 @@ p.
   var varlist = [
     "Alice",
     "Ambiguous",
+    "Avalanche",
     "Bicolour",
     "Castle",
     "Doublearmy",
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 876fa496..c29a5acb 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -401,6 +401,7 @@ p.
   var varlist = [
     "Alice",
     "Ambiguous",
+    "Avalanche",
     "Bicolour",
     "Castle",
     "Doublearmy",
diff --git a/client/src/variants/Avalanche.js b/client/src/variants/Avalanche.js
new file mode 100644
index 00000000..edeb5648
--- /dev/null
+++ b/client/src/variants/Avalanche.js
@@ -0,0 +1,382 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class AvalancheRules extends ChessRules {
+
+  static get PawnSpecs() {
+    return (
+      Object.assign(
+        { promotions: [V.PAWN] },
+        ChessRules.PawnSpecs
+      )
+    );
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParts = fen.split(" ");
+    if (fenParts.length != 5) return false;
+    if (!fenParts[4].match(/^[0-8]$/)) return false;
+    return true;
+  }
+
+  canIplay(side, [x, y]) {
+    if (this.subTurn == 0) return (x >= V.size.x);
+    const c = this.getColor(x, y);
+    return (
+      (this.subTurn == 1 && c == side) ||
+      (this.subTurn == 2 && c != side && this.getPiece(x, y) == V.PAWN)
+    );
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { promoteFile: fenParts[4] }
+    );
+  }
+
+  getPromoteFen() {
+    const L = this.promoteFile.length;
+    return (this.promoteFile[L-1] + 1);
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getPromoteFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getPromoteFen();
+  }
+
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness).slice(0, -1) + "0";
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
+  static get RESERVE_PIECES() {
+    // Promotion pieces
+    return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const fenPromoteFile = V.ParseFen(fen).promoteFile;
+    this.promoteFile = [parseInt(fenPromoteFile, 10) - 1];
+    this.reserve = { 'w': null, 'b': null };
+    if (this.promoteFile[0] >= 0) {
+      this.reserve = {
+        [this.turn]: {
+          [V.ROOK]: 1,
+          [V.KNIGHT]: 1,
+          [V.BISHOP]: 1,
+          [V.QUEEN]: 1
+        }
+      };
+      this.subTurn = 0;
+    }
+    else this.subTurn = 1;
+  }
+
+  getReservePpath(index, color) {
+    return color + V.RESERVE_PIECES[index];
+  }
+
+  getReserveMove(y) {
+    // Send a new piece piece to our first rank
+    const color = this.turn;
+    const L = this.promoteFile.length;
+    const [rank, file] = [color == 'w' ? 0 : 7, this.promoteFile[L-1]];
+    return new Move({
+      appear: [
+        new PiPo({ x: rank, y: file, c: color, p: V.RESERVE_PIECES[y] })
+      ],
+      vanish: [
+        new PiPo({ x: rank, y: file, c: color, p: V.PAWN })
+      ],
+      start: { x: 8, y: y },
+      end: { x: rank, y: file }
+    });
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (this.subTurn == 0)
+      // Reserves, outside of board: x == sizeX(+1)
+      return (x >= 8 ? [this.getReserveMove(y)] : []);
+    if (this.subTurn == 1)
+      // Usual case:
+      return super.getPotentialMovesFrom([x, y]);
+    // subTurn == 2: only allowed to push an opponent's pawn (if possible)
+    const oppPawnShift = (this.turn == 'w' ? 1 : -1);
+    if (
+      V.OnBoard(x + oppPawnShift, y) &&
+      this.board[x + oppPawnShift][y] == V.EMPTY
+    ) {
+      return [this.getBasicMove([x, y], [x + oppPawnShift, y])];
+    }
+    return [];
+  }
+
+  getAllValidMoves() {
+    if (this.subTurn == 0) {
+      let moves = [];
+      for (let y = 0; y < V.RESERVE_PIECES.length; y++)
+        moves.push(this.getReserveMove(y));
+      return moves;
+    }
+    if (this.subTurn == 1)
+      return this.filterValid(super.getAllPotentialMoves());
+    // subTurn == 2: move opponent's pawn only
+    let moves = [];
+    const oppCol = V.GetOppCol(this.turn);
+    for (let i = 0; i < 8; i++) {
+      for (let j = 0; j < 8; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == oppCol &&
+          this.getPiece(i, j) == V.PAWN
+        ) {
+          Array.prototype.push.apply(
+            moves, this.getPotentialMovesFrom([i, j]));
+        }
+      }
+    }
+    return moves;
+  }
+
+  filterValid(moves) {
+    if (this.subTurn != 1) return moves; //self-checks by pawns are allowed
+    return super.filterValid(moves);
+  }
+
+  atLeastOneMove() {
+    if (this.subTurn == 0) return true; //TODO: never called in this situation
+    if (this.subTurn == 1) {
+      // Cannot use super method: infinite recursive calls
+      const color = this.turn;
+      for (let i = 0; i < 8; i++) {
+        for (let j = 0; j < 8; j++) {
+          if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+            const moves = this.getPotentialMovesFrom([i, j]);
+            if (moves.length > 0) {
+              for (let k = 0; k < moves.length; k++) {
+                const piece = moves[k].vanish[0].p;
+                if (piece == V.KING) {
+                  this.kingPos[color] =
+                    [moves[k].appear[0].x, moves[k].appear[0].y];
+                }
+                V.PlayOnBoard(this.board, moves[k]);
+                const res = !this.underCheck(color);
+                V.UndoOnBoard(this.board, moves[k]);
+                if (piece == V.KING) {
+                  this.kingPos[color] =
+                    [moves[k].vanish[0].x, moves[k].vanish[0].y];
+                }
+                if (res) return true;
+              }
+            }
+          }
+        }
+      }
+      return false;
+    }
+    // subTurn == 2: need to find an enemy pawn which can advance
+    const oppCol = V.GetOppCol(this.turn);
+    const oppPawnShift = (oppCol == 'w' ? -1 : 1);
+    for (let i = 0; i < 8; i++) {
+      for (let j = 0; j < 8; j++) {
+        if (
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == oppCol &&
+          this.getPiece(i, j) == V.PAWN &&
+          V.OnBoard(i + oppPawnShift, j) &&
+          this.board[i + oppPawnShift][j] == V.EMPTY
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  getCheckSquares() {
+    if (this.kingPos[this.turn][0] < 0) return [];
+    return super.getCheckSquares();
+  }
+
+  getCurrentScore() {
+    // If my king disappeared: I lost!
+    const c = this.turn;
+    if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
+    return super.getCurrentScore();
+  }
+
+  prePlay(move) {
+    if (this.subTurn != 1) return;
+    const c = move.vanish[0].c;
+    const piece = move.vanish[0].p;
+    const firstRank = c == "w" ? V.size.x - 1 : 0;
+    if (piece == V.KING) {
+      this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
+      this.castleFlags[c] = [V.size.y, V.size.y];
+      return;
+    }
+    const oppCol = V.GetOppCol(c);
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING) {
+      // Opponent's king is captured, game over
+      this.kingPos[oppCol] = [-1, -1];
+      move.captureKing = true;
+    }
+    const oppFirstRank = V.size.x - 1 - firstRank;
+    if (
+      move.start.x == firstRank && //our rook moves?
+      this.castleFlags[c].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
+      this.castleFlags[c][flagIdx] = V.size.y;
+    }
+    if (
+      move.end.x == oppFirstRank && //we took opponent rook?
+      this.castleFlags[oppCol].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
+      this.castleFlags[oppCol][flagIdx] = V.size.y;
+    }
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    this.prePlay(move);
+    V.PlayOnBoard(this.board, move);
+    const c = this.turn;
+    move.turn = [c, this.subTurn];
+    const oppCol = V.GetOppCol(c);
+    const oppLastRank = (c == 'w' ? 7 : 0);
+    if (this.subTurn <= 1) this.reserve[oppCol] = null;
+    if (this.subTurn == 0) {
+      this.subTurn++;
+      this.reserve[c] = null;
+    }
+    else if (this.subTurn == 1) {
+      this.subTurn++;
+      if (
+        this.movesCount == 0 ||
+        !!move.captureKing ||
+        !this.atLeastOneMove()
+      ) {
+        this.turn = oppCol;
+        this.movesCount++;
+        this.subTurn = 1;
+        this.promoteFile.push(-1);
+        move.pushPromote = true;
+      }
+    }
+    else {
+      // subTurn == 2
+      this.turn = oppCol;
+      if (move.end.x == oppLastRank) {
+        this.promoteFile.push(move.end.y);
+        this.reserve[oppCol] = {
+          [V.ROOK]: 1,
+          [V.KNIGHT]: 1,
+          [V.BISHOP]: 1,
+          [V.QUEEN]: 1
+        };
+        this.subTurn = 0;
+      }
+      else {
+        this.subTurn = 1;
+        this.promoteFile.push(-1);
+      }
+      move.pushPromote = true;
+      this.movesCount++;
+    }
+  }
+
+  undo(move) {
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    const changeTurn = (this.turn != move.turn[0]);
+    this.turn = move.turn[0];
+    this.subTurn = move.turn[1];
+    if (!!move.pushPromote) {
+      const promoteFile = this.promoteFile.pop();
+      if (promoteFile >= 0) this.reserve[V.GetOppCol(this.turn)] = null;
+    }
+    else if (this.subTurn == 0) {
+      this.reserve[this.turn] = {
+        [V.ROOK]: 1,
+        [V.KNIGHT]: 1,
+        [V.BISHOP]: 1,
+        [V.QUEEN]: 1
+      };
+    }
+    if (changeTurn) this.movesCount--;
+    this.postUndo(move);
+  }
+
+  postUndo(move) {
+    if (this.subTurn != 1) return;
+    if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
+      // Opponent's king was captured
+      this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
+    super.postUndo(move);
+  }
+
+  getComputerMove() {
+    // Just try to capture as much material as possible (1-half move)
+    const moves = this.getAllValidMoves();
+    if (this.subTurn == 0) {
+      this.play(moves[3]); //HACK... 3 = queen index
+      const res = this.getComputerMove();
+      this.undo(moves[3]);
+      return [moves[3], res];
+    }
+    // subTurn == 1 (necessarily)
+    let candidates = [];
+    let maxValue = -V.INFINITY;
+    for (let m of moves) {
+      let value = 0;
+      if (m.vanish.length == 2) {
+        // Compute delta value, to not give all material on pawns... (TODO)
+        // 0.5 to favor captures (if same type of piece).
+        value = 0.5 +
+          ChessRules.VALUES[m.vanish[1].p] - ChessRules.VALUES[m.vanish[0].p];
+      }
+      if (value > maxValue) {
+        candidates = [m];
+        maxValue = value;
+      }
+      else if (value == maxValue) candidates.push(m);
+    }
+    const m1 = candidates[randInt(candidates.length)];
+    this.play(m1);
+    let m2 = null;
+    if (this.subTurn == 2) {
+      // Just pick a pawn at random
+      const moves2 = this.getAllValidMoves();
+      m2 = moves2[randInt(moves2.length)];
+    }
+    this.undo(m1);
+    if (!m2) return m1;
+    return [m1, m2];
+  }
+
+  getNotation(move) {
+    if (this.subTurn == 0)
+      return move.appear[0].p.toUpperCase() + "@" + V.CoordsToSquare(move.end);
+    if (this.subTurn == 1) return super.getNotation(move);
+    // subTurn == 2: just indicate final square
+    return V.CoordsToSquare(move.end);
+  }
+
+};
diff --git a/client/src/variants/Isardam.js b/client/src/variants/Isardam.js
index 4ca9a856..d745f150 100644
--- a/client/src/variants/Isardam.js
+++ b/client/src/variants/Isardam.js
@@ -73,4 +73,8 @@ export class IsardamRules extends ChessRules {
     );
   }
 
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
 };
diff --git a/client/src/variants/Pacosako.js b/client/src/variants/Pacosako.js
index 8cec688f..c6f466f1 100644
--- a/client/src/variants/Pacosako.js
+++ b/client/src/variants/Pacosako.js
@@ -671,27 +671,9 @@ export class PacosakoRules extends ChessRules {
     return moves.filter(m => !this.oppositeMoves(this.umoves[L - 1], m));
   }
 
-  play(move) {
-    move.flags = JSON.stringify(this.aggregateFlags());
-    this.epSquares.push(this.getEpSquare(move));
-    // Check if the move is the last of the turn: all cases except releases
-    if (!move.released) {
-      // No more union releases available
-      this.turn = V.GetOppCol(this.turn);
-      this.movesCount++;
-      this.lastMoveEnd.push(null);
-    }
-    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);
-  }
-
   updateCastleFlags(move, piece) {
-    const c = V.GetOppCol(this.turn);
+    const c = this.turn;
     const firstRank = (c == "w" ? 7 : 0);
-    const oppCol = this.turn;
-    const oppFirstRank = 7 - firstRank;
     if (piece == V.KING && move.appear.length > 0)
       this.castleFlags[c] = [V.size.y, V.size.y];
     else if (
@@ -701,34 +683,46 @@ export class PacosakoRules extends ChessRules {
       const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
       this.castleFlags[c][flagIdx] = V.size.y;
     }
-    // No more checking: a rook in union can take part in castling.
+    else if (
+      move.end.x == firstRank &&
+      this.castleFlags[c].includes(move.end.y)
+    ) {
+      // Move to our rook: necessary normal piece, to union, releasing
+      // (or the rook was moved before!)
+      const flagIdx = (move.end.y == this.castleFlags[c][0] ? 0 : 1);
+      this.castleFlags[c][flagIdx] = V.size.y;
+    }
   }
 
-  postPlay(move) {
-    if (move.vanish.length == 0)
-      // A released piece just moved. Cannot be the king.
-      return;
-    const c = move.vanish[0].c;
-    const piece = move.vanish[0].p;
+  prePlay(move) {
+    // Easier before move is played in this case (flags are saved)
+    const c = this.turn;
+    const L = this.lastMoveEnd.length;
+    const lm = this.lastMoveEnd[L-1];
+    const piece = (!!lm ? lm.p : move.vanish[0].p);
     if (piece == V.KING)
       this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
     this.updateCastleFlags(move, piece);
-    if (
-      [1, 6].includes(move.start.x) &&
-      move.vanish.length >= 1 &&
-      move.appear.length == 1
-    ) {
-      // Does this move turn off a 2-squares pawn flag?
-      if (
-        move.vanish[0].p == V.PAWN ||
-        (
-          !(ChessRules.PIECES.includes(move.vanish[0].p)) &&
-          this.getUnionPieces(move.vanish[0].c, move.vanish[0].p)[c] == V.PAWN
-        )
-      ) {
-        this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
-      }
+    const pawnFirstRank = (c == 'w' ? 6 : 1);
+    if (move.start.x == pawnFirstRank)
+      // This move (potentially) turns off a 2-squares pawn flag
+      this.pawnFlags[c][move.start.y] = false;
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    this.prePlay(move);
+    this.epSquares.push(this.getEpSquare(move));
+    // Check if the move is the last of the turn: all cases except releases
+    if (!move.released) {
+      // No more union releases available
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount++;
+      this.lastMoveEnd.push(null);
     }
+    else this.lastMoveEnd.push(Object.assign({ p: move.released }, move.end));
+    V.PlayOnBoard(this.board, move);
+    this.umoves.push(this.getUmove(move));
   }
 
   undo(move) {
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index ebb46753..7d7f234f 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -982,13 +982,13 @@ export default {
             .getElementById("btnC" + newChall.type)
             .classList.add("somethingnew");
         }
-        if (!!chall.to) {
+        if (chall.to == this.st.user.name) {
           notify(
             "New challenge",
             // fromValues.name should exist since the player is online, but
             // let's consider there is some chance that the challenge arrives
             // right after we connected and before receiving the poll result:
-            { body: "from " + (fromValues.name || "unknown yet...") }
+            { body: "from " + (fromValues.name || "@nonymous") }
           );
         }
       }
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 216dd66f..17fe0d0f 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -21,6 +21,7 @@ insert or ignore into Variants (name, description) values
   ('Arena', 'Middle battle'),
   ('Atomic1', 'Explosive captures (v1)'),
   ('Atomic2', 'Explosive captures (v2)'),
+  ('Avalanche', 'Pawnfalls'),
   ('Ball', 'Score a goal'),
   ('Balaklava', 'Meet the Mammoth'),
   ('Baroque', 'Exotic captures'),
-- 
2.44.0