Add Yote
authorBenjamin Auder <benjamin.auder@somewhere>
Sun, 17 Jan 2021 23:02:44 +0000 (00:02 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Sun, 17 Jan 2021 23:02:44 +0000 (00:02 +0100)
16 files changed:
client/public/images/pieces/Konane/bp.svg [changed from file to symlink]
client/public/images/pieces/Konane/wp.svg [changed from file to symlink]
client/public/images/pieces/Yote/bp.svg [new file with mode: 0644]
client/public/images/pieces/Yote/wp.svg [new file with mode: 0644]
client/src/components/BaseGame.vue
client/src/components/Board.vue
client/src/translations/rules/Konane/en.pug
client/src/translations/rules/Konane/es.pug
client/src/translations/rules/Konane/fr.pug
client/src/translations/rules/Yote/en.pug
client/src/translations/rules/Yote/es.pug
client/src/translations/rules/Yote/fr.pug
client/src/utils/scoring.js
client/src/variants/Crazyhouse.js
client/src/variants/Konane.js
client/src/variants/Yote.js

deleted file mode 100644 (file)
index 75e907e0b0808bc7620a25de90565a1071d85a25..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
-<defs><radialGradient id="rg" cx=".47" cy=".49" r=".48">
-<stop offset=".7" stop-color="#FFF"/>
-<stop offset=".9" stop-color="#DDD"/>
-<stop offset="1" stop-color="#777"/>
-</radialGradient></defs>
-<circle cx="250" cy="250" r="235" fill="url(#rg)"/>
-</svg>
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..dc5717d38b398b0a2ea1a4f60ab0752f77cbd1ea
--- /dev/null
@@ -0,0 +1 @@
+../Yote/wp.svg
\ No newline at end of file
deleted file mode 100644 (file)
index 357079eb5a4a8d0c8ef366c2ceb2ec2e07f85ac5..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
-<defs><radialGradient id="rg" cx=".3" cy=".3" r=".8">
-<stop offset="0" stop-color="#777"/>
-<stop offset=".3" stop-color="#222"/>
-<stop offset="1" stop-color="#000"/>
-</radialGradient></defs>
-<circle cx="250" cy="250" r="235" fill="url(#rg)"/>
-</svg>
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..0d6535fd4fe8a68a1ae3380ae7940ba93a72a4ac
--- /dev/null
@@ -0,0 +1 @@
+../Yote/bp.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Yote/bp.svg b/client/public/images/pieces/Yote/bp.svg
new file mode 100644 (file)
index 0000000..75e907e
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+<defs><radialGradient id="rg" cx=".47" cy=".49" r=".48">
+<stop offset=".7" stop-color="#FFF"/>
+<stop offset=".9" stop-color="#DDD"/>
+<stop offset="1" stop-color="#777"/>
+</radialGradient></defs>
+<circle cx="250" cy="250" r="235" fill="url(#rg)"/>
+</svg>
diff --git a/client/public/images/pieces/Yote/wp.svg b/client/public/images/pieces/Yote/wp.svg
new file mode 100644 (file)
index 0000000..357079e
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+<defs><radialGradient id="rg" cx=".3" cy=".3" r=".8">
+<stop offset="0" stop-color="#777"/>
+<stop offset=".3" stop-color="#222"/>
+<stop offset="1" stop-color="#000"/>
+</radialGradient></defs>
+<circle cx="250" cy="250" r="235" fill="url(#rg)"/>
+</svg>
index f101702..7627f2d 100644 (file)
@@ -569,7 +569,7 @@ export default {
           }
         }
         if (score != "*" && ["analyze", "versus"].includes(this.mode)) {
-          const message = getScoreMessage(score);
+          const message = getScoreMessage(score, V.ReverseColors);
           // Show score on screen
           this.showEndgameMsg(score + " . " + this.st.tr[message]);
         }
index cc3f6fd..b176bd7 100644 (file)
@@ -663,27 +663,29 @@ export default {
             this.$emit("click-square", sq);
             if (withPiece && !this.vr.onlyClick(sq)) {
               this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
-              // For potential drag'n drop, remember start coordinates
-              // (to center the piece on mouse cursor)
-              let parent = e.target.parentNode; //surrounding square
-              const rect = parent.getBoundingClientRect();
-              this.start = {
-                x: rect.x + rect.width / 2,
-                y: rect.y + rect.width / 2,
-                id: parent.id
-              };
-              // Add the moving piece to the board, just after current image
-              this.selectedPiece = e.target.cloneNode();
-              Object.assign(
-                this.selectedPiece.style,
-                {
-                  position: "absolute",
-                  top: 0,
-                  display: "inline-block",
-                  zIndex: 3000
-                }
-              );
-              parent.insertBefore(this.selectedPiece, e.target.nextSibling);
+              if (this.possibleMoves.length > 0) {
+                // For potential drag'n drop, remember start coordinates
+                // (to center the piece on mouse cursor)
+                let parent = e.target.parentNode; //surrounding square
+                const rect = parent.getBoundingClientRect();
+                this.start = {
+                  x: rect.x + rect.width / 2,
+                  y: rect.y + rect.width / 2,
+                  id: parent.id
+                };
+                // Add the moving piece to the board, just after current image
+                this.selectedPiece = e.target.cloneNode();
+                Object.assign(
+                  this.selectedPiece.style,
+                  {
+                    position: "absolute",
+                    top: 0,
+                    display: "inline-block",
+                    zIndex: 3000
+                  }
+                );
+                parent.insertBefore(this.selectedPiece, e.target.nextSibling);
+              }
             }
           }
         }
index 9a15f67..930f8c7 100644 (file)
@@ -2,6 +2,9 @@ p.boxed
   | Capture orthogonally at each turn, "as in Draughts".
   | If you cannot capture, you lose.
 
+p
+  a(href="https://en.wikipedia.org/wiki/Konane") Hawaiian Checkers
+
 p.
   To initiate the game, the first player (black) must remove one of his stones
   either in the upper left or lower right corner, or in the center,
index 7cb7f73..ca17726 100644 (file)
@@ -2,6 +2,9 @@ p.boxed
   | Captura ortogonalmente en cada turno, "como a las Damas".
   | Si no es posible la captura, ha perdido.
 
+p
+  a(href="https://en.wikipedia.org/wiki/Konane") Damas hawaianas
+
 p.
   Para iniciar el juego, el primer jugador (negras) debe eliminar uno de
   sus piedras en la esquina superior izquierda o inferior derecha,
index 2329737..7cdc286 100644 (file)
@@ -2,6 +2,9 @@ p.boxed
   | Capturez orthogonalement à chaque tour, "comme aux Dames".
   | Si aucune capture n'est possible, vous avez perdu.
 
+p
+  a(href="https://en.wikipedia.org/wiki/Konane") Dames hawaïennes
+
 p.
   Pour initialiser la partie, le premier joueur (noirs) doit retirer une de
   ses pierres soit dans le coin supérieur gauche ou inférieur droit,
index 3a33838..9689de8 100644 (file)
@@ -1,2 +1,51 @@
 p.boxed
-  | TODO
+  | Move a stone or capture by jumping orthogonally.
+
+p Traditional game in some Western Africa countries.
+
+p
+  | The rules described here correspond to 
+  a(href="https://www.youtube.com/watch?v=yuqHy8GOZ_Q") this video
+  | , with the alternative voctory condition.
+  | See also 
+  a(href="http://www.cyningstan.com/game/342/yot") this introduction
+  | .
+
+p.
+  It seems that the color of pieces starting the game isn't really determined,
+  so I decided to start with black stones, as in Go game or Othello.
+  The following is mostly copy-paste from Wikipedia:
+
+p.
+  The game is played on a 5×6 board, which is empty at the beginning of the
+  game. Each player has twelve pieces in hand. Players alternate turns,
+  with Black moving first. In a move, a player may either:
+ul
+  li Place a piece in hand on any empty cell of the board.
+  li.
+    Move one of their pieces already on the board orthogonally to an empty
+    adjacent cell.
+  li.
+    Capture an opponent's piece if it is orthogonally adjacent to a player's
+    piece, by jumping to the empty cell immediately beyond it.
+    The captured piece is removed from the board, and the capturing player
+    removes another of the opponent's pieces of his choosing from the board.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:6/1p2P1/3p2/1p1P2/5P d2,d4:
+  .diagram.diag22
+    | fen:6/3PP1/6/1p4/5P:
+  figcaption Before and after the marked capture + removal of b4.
+
+p.
+  The game ends when a player has no stones on the board (after move 1),
+  or if he cannot play a move. He loses in both cases.
+  If only a low number of pieces remain on the board, captures might be
+  impossible and after a sequences of attempts players may agree on a draw.
+
+h4 Notes
+
+p Two consecutive normal moves cannot cancel each other.
+
+p To remove a stone after a capture, simply click on it.
index 3a33838..20170ba 100644 (file)
@@ -1,2 +1,55 @@
 p.boxed
-  | TODO
+  | Mueve una piedra o captura mediante un salto ortogonal.
+
+p Juego tradicional en algunos países de África Occidental.
+
+p
+  | Las reglas descritas aquí corresponden a 
+  a(href="https://www.youtube.com/watch?v=yuqHy8GOZ_Q") este video
+  | , con la condición de victoria alternativa.
+  | Ver también 
+  a(href="http://www.cyningstan.com/game/342/yot") esta introducción
+  | .
+
+p.
+  Parece que el color de las piezas que comienzan el juego no es realmente
+  decidido, así que decidí comenzar con las piedras negras, como en
+  juego de Go u Othello. Lo siguiente se ha tomado principalmente de Wikipedia:
+
+p.
+  El juego tiene lugar en un tablero inicialmente vacío de 5x6.
+  Cada jugador tiene doce piezas en la mano. Los jugadores se alternan vueltas,
+  comenzando por las negras. Durante un movimiento, podemos:
+ul
+  li Coloque una pieza de la reserva en un espacio vacío.
+  li.
+    Mueve una piedra que ya esté en el tablero,
+    ortogonalmente a un cuadrado vacío.
+  li.
+    Capturar una pieza del oponente si está ortogonalmente adyacente a esa
+    del jugador, saltando al cuadrado vacío inmediatamente después.
+    La piedra capturada se elimina del juego y el jugador también elimina
+    otra pieza opuesta de su elección en el tablero.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:6/1p2P1/3p2/1p1P2/5P d2,d4:
+  .diagram.diag22
+    | fen:6/3PP1/6/1p4/5P:
+  figcaption Antes y después de la captura + eliminación marcada en b4.
+
+p.
+  El juego termina cuando un jugador no tiene más piedras en el tablero
+  (después del primer movimiento), o si no puede ejecutar una jugada.
+  Pierde en ambos casos.
+  Si solo quedan en juego un pequeño número de piezas,
+  las capturas pueden no ser posibles, y en este caso los jugadores pueden
+  después de algunos intentos, decida empatar.
+
+h4 Notas
+
+p Dos jugadas normales consecutivos no pueden anularse entre sí.
+
+p.
+  Para eliminar una piedra después de la captura,
+  simplemente haga clic en ella.
index 3a33838..da87c56 100644 (file)
@@ -1,2 +1,52 @@
 p.boxed
-  | TODO
+  | Déplacez une pierre ou capturez via un saut orthogonal.
+
+p Jeu traditionnel dans certains pays d'Afrique de l'Ouest.
+
+p
+  | Les règles décrites ici correspondent à 
+  a(href="https://www.youtube.com/watch?v=yuqHy8GOZ_Q") cette vidéo
+  | , avec la condition de victoire alternative.
+  | Voir aussi 
+  a(href="http://www.cyningstan.com/game/342/yot") cette introduction
+  | .
+
+p.
+  Il semble que la couleur des pièces démarrant la partie ne soit pas vraiment
+  déterminée, donc j'ai décidé de commencer avec les pierres noires, comme au
+  jeu de Go ou à Othello. La suite est essentiellement reprise de Wikipedia :
+
+p.
+  Le jeu se déroule sur un plateau de taille 5x6 initialement vide.
+  Chaque joueur dispose de douze pièces en main. Les joueurs alternent les
+  tours, en commençant par les noirs. Lors d'un coup, on peut :
+ul
+  li Placer une pièce de la réserve sur une case vide.
+  li.
+    Déplacer une pierre déjà sur le plateau, orthogonalement vers une case vide.
+  li.
+    Capturer une pièce adverse si elle est orthogonalement adjacente à celle
+    du joueur, en sautant vers la case vide située immédiatement après.
+    La pierre capturée est retirée du jeu, et le joueur enlève aussi une autre
+    pièce adverse de son choix sur le plateau.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:6/1p2P1/3p2/1p1P2/5P d2,d4:
+  .diagram.diag22
+    | fen:6/3PP1/6/1p4/5P:
+  figcaption Avant et après la capture marquée + suppression en b4.
+
+p.
+  La partie se termine quand un joueur n'a plus de pierres sur le plateau
+  (après le premier coup), ou s'il est dans l'incapacité de jouer un coup.
+  Il perd dans les deux cas.
+  Si seulement un petit nombre de pièces restent en jeu, de nouvelles
+  captures pourraient être impossibles, et dans ce cas les joueurs peuvent
+  après quelques essais décider d'un match nul.
+
+h4 Notes
+
+p Deux coups normaux consécutifs ne peuvent s'annuler l'un l'autre.
+
+p Pour supprimer une pierre après capture, cliquez simplement dessus.
index c659b7d..3d54128 100644 (file)
@@ -1,12 +1,12 @@
 // Default score message if none provided
-export function getScoreMessage(score) {
+export function getScoreMessage(score, reverseColors) {
   let eogMessage = "Undefined"; //not translated: unused
   switch (score) {
     case "1-0":
-      eogMessage = "White win";
+      eogMessage = (!reverseColors ? "White win" : "Black win");
       break;
     case "0-1":
-      eogMessage = "Black win";
+      eogMessage = (!reverseColors ? "Black win" : "White win");
       break;
     case "1/2":
       eogMessage = "Draw";
index db9cb57..64ce48b 100644 (file)
@@ -188,7 +188,7 @@ export class CrazyhouseRules extends ChessRules {
     if (!super.atLeastOneMove()) {
       // Search one reserve move
       for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
-        let moves = this.filterValid(
+        const moves = this.filterValid(
           this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
         );
         if (moves.length > 0) return true;
index 8f285ef..1ff77ee 100644 (file)
@@ -1,7 +1,5 @@
 import { ChessRules, Move, PiPo } from "@/base_rules";
 
-// TODO: Maybe more flexible end of game messages (V.ColorsReversed ?!)
-
 export class KonaneRules extends ChessRules {
 
   static get HasFlags() {
@@ -12,10 +10,18 @@ export class KonaneRules extends ChessRules {
     return false;
   }
 
+  static get ReverseColors() {
+    return true;
+  }
+
   static get PIECES() {
     return V.PAWN;
   }
 
+  getPiece() {
+    return V.PAWN;
+  }
+
   getPpath(b) {
     return "Konane/" + b;
   }
index 0d81759..dc1ef68 100644 (file)
@@ -1,7 +1,398 @@
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
 
 export class YoteRules extends ChessRules {
 
-  // TODO
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get Monochrome() {
+    return true;
+  }
+
+  static get Notoodark() {
+    return true;
+  }
+
+  static get ReverseColors() {
+    return true;
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 3) Check reserves
+    if (
+      !fenParsed.reserve ||
+      !fenParsed.reserve.match(/^([0-9]{1,2},){2,2}$/)
+    ) {
+      return false;
+    }
+    // 4) Check lastMove
+    if (!fenParsed.lastMove) return false;
+    const lmParts = fenParsed.lastMove.split(",");
+    for (lp of lmParts) {
+      if (lp != "-" && !lp.match(/^([a-f][1-5]){2,2}$/)) return false;
+    }
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      {
+        reserve: fenParts[3],
+        lastMove: fenParts[4]
+      }
+    );
+  }
+
+  static GenRandInitFen(randomness) {
+    return "6/6/6/6/6 w 0 12,12 -,-";
+  }
+
+  getFen() {
+    return (
+      super.getFen() + " " +
+      this.getReserveFen() + " " +
+      this.getLastmoveFen()
+    );
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getReserveFen();
+  }
+
+  getReserveFen() {
+    return (
+      (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
+      (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
+    );
+  }
+
+  getLastmoveFen() {
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    return (
+      (
+        !lm['w']
+          ? '-'
+          : V.CoordsToSquare(lm['w'].start) + V.CoordsToSquare(lm['w'].end)
+      )
+      + "," +
+      (
+        !lm['b']
+          ? '-'
+          : V.CoordsToSquare(lm['b'].start) + V.CoordsToSquare(lm['b'].end)
+      )
+    );
+  }
+
+  setOtherVariables(fen) {
+    const fenParsed = V.ParseFen(fen);
+    const reserve = fenParsed.reserve.split(",").map(x => parseInt(x, 10));
+    this.reserve = {
+      w: { [V.PAWN]: reserve[0] },
+      b: { [V.PAWN]: reserve[1] }
+    };
+    // And last moves (to avoid undoing your last move)
+    const lmParts = fenParsed.lastMove.split(",");
+    this.lastMove = [{ w: null, b: null }];
+    ['w', 'b'].forEach((c, i) => {
+      if (lmParts[i] != '-') {
+        this.lastMove[0][c] = {
+          start: V.SquareToCoords(lmParts[i].substr(0, 2)),
+          end: V.SquareToCoords(lmParts[i].substr(2))
+        };
+      }
+    });
+    // Local stack to know if (current) last move captured something
+    this.captures = [false];
+  }
+
+  static get size() {
+    return { x: 5, y: 6 };
+  }
+
+  static get PIECES() {
+    return [V.PAWN];
+  }
+
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece() {
+    return V.PAWN;
+  }
+
+  getPpath(b) {
+    return "Yote/" + b;
+  }
+
+  getReservePpath(index, color) {
+    return "Yote/" + color + V.PAWN;
+  }
+
+  static get RESERVE_PIECES() {
+    return [V.PAWN];
+  }
+
+  canIplay(side, [x, y]) {
+    if (this.turn != side) return false;
+    const L = this.captures.length;
+    if (!this.captures[L-1]) return this.getColor(x, y) == side;
+    return (x < V.size.x && this.getColor(x, y) != side);
+  }
+
+  hoverHighlight(x, y) {
+    const L = this.captures.length;
+    if (!this.captures[L-1]) return false;
+    const oppCol = V.GetOppCol(this.turn);
+    return (this.board[x][y] != V.EMPTY && this.getColor(x, y) == oppCol);
+  }
+
+  // TODO: onlyClick() doesn't fulfill exactly its role.
+  // Seems that there is some lag... TOFIX
+  onlyClick([x, y]) {
+    const L = this.captures.length;
+    return (this.captures[L-1] && this.getColor(x, y) != this.turn);
+  }
+
+  // PATCH related to above TO-DO:
+  getPossibleMovesFrom([x, y]) {
+    if (x < V.size.x && this.board[x][y] == V.EMPTY) return [];
+    return super.getPossibleMovesFrom([x, y]);
+  }
+
+  doClick([x, y]) {
+    const L = this.captures.length;
+    if (!this.captures[L-1]) return null;
+    const oppCol = V.GetOppCol(this.turn);
+    if (this.board[x][y] == V.EMPTY || this.getColor(x, y) != oppCol)
+      return null;
+    return new Move({
+      appear: [],
+      vanish: [ new PiPo({ x: x, y: y, c: oppCol, p: V.PAWN }) ],
+      end: { x: x, y: y }
+    });
+  }
+
+  getReserveMoves(x) {
+    const color = this.turn;
+    if (this.reserve[color][V.PAWN] == 0) return [];
+    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) {
+          let mv = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: color,
+                p: V.PAWN
+              })
+            ],
+            vanish: [],
+            start: { x: x, y: 0 }, //a bit artificial...
+            end: { x: i, y: j }
+          });
+          moves.push(mv);
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const L = this.captures.length;
+    if (this.captures[L-1]) {
+      if (x >= V.size.x) return [];
+      const mv = this.doClick([x, y]);
+      return (!!mv ? [mv] : []);
+    }
+    if (x >= V.size.x)
+      return this.getReserveMoves([x, y]);
+    return this.getPotentialPawnMoves([x, y]);
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    let moves = [];
+    const color = this.turn;
+    const L = this.lastMove.length;
+    const lm = this.lastMove[L-1];
+    let forbiddenStep = null;
+    if (!!lm[color]) {
+      forbiddenStep = [
+        lm[color].start.x - lm[color].end.x,
+        lm[color].start.y - lm[color].end.y
+      ];
+    }
+    const oppCol = V.GetOppCol(color);
+    for (let s of V.steps[V.ROOK]) {
+      if (
+        !!forbiddenStep &&
+        s[0] == forbiddenStep[0] && s[1] == forbiddenStep[1]
+      ) {
+        continue;
+      }
+      const [i1, j1] = [x + s[0], y + s[1]];
+      if (V.OnBoard(i1, j1)) {
+        if (this.board[i1][j1] == V.EMPTY)
+          moves.push(super.getBasicMove([x, y], [i1, j1]));
+        else if (this.getColor(i1, j1) == oppCol) {
+          const [i2, j2] = [i1 + s[0], j1 + s[1]];
+          if (V.OnBoard(i2, j2) && this.board[i2][j2] == V.EMPTY) {
+            let mv = new Move({
+              appear: [
+                new PiPo({ x: i2, y: j2, c: color, p: V.PAWN })
+              ],
+              vanish: [
+                new PiPo({ x: x, y: y, c: color, p: V.PAWN }),
+                new PiPo({ x: i1, y: j1, c: oppCol, p: V.PAWN })
+              ]
+            });
+            moves.push(mv);
+          }
+        }
+      }
+    }
+    return moves;
+  }
+
+  getAllPotentialMoves() {
+    let moves = super.getAllPotentialMoves();
+    const color = this.turn;
+    moves = moves.concat(
+      this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1)));
+    return moves;
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  atLeastOneMove() {
+    if (!super.atLeastOneMove()) {
+      // Search one reserve move
+      const moves =
+        this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
+      if (moves.length > 0) return true;
+      return false;
+    }
+    return true;
+  }
+
+  play(move) {
+    const color = this.turn;
+    move.turn = color; //for undo
+    const L = this.lastMove.length;
+    if (color == 'w')
+      this.lastMove.push({ w: null, b: this.lastMove[L-1]['b'] });
+    if (move.appear.length == move.vanish.length) { //== 1
+      // Normal move (non-capturing, non-dropping, non-removal)
+      let lm = this.lastMove[L - (color == 'w' ? 0 : 1)];
+      if (!lm[color]) lm[color] = {};
+      lm[color].start = move.start;
+      lm[color].end = move.end;
+    }
+    const oppCol = V.GetOppCol(color);
+    V.PlayOnBoard(this.board, move);
+    const captureNotEnding = (
+      move.vanish.length == 2 &&
+      this.board.some(b => b.some(cell => cell != "" && cell[0] == oppCol))
+    );
+    this.captures.push(captureNotEnding);
+    // Change turn unless I just captured something,
+    // and an opponent stone can be removed from board.
+    if (!captureNotEnding) {
+      this.turn = oppCol;
+      this.movesCount++;
+    }
+    this.postPlay(move);
+  }
+
+  undo(move) {
+    V.UndoOnBoard(this.board, move);
+    if (this.turn == 'b') this.lastMove.pop();
+    else this.lastMove['b'] = null;
+    this.captures.pop();
+    if (move.turn != this.turn) {
+      this.turn = move.turn;
+      this.movesCount--;
+    }
+    this.postUndo(move);
+  }
+
+  postPlay(move) {
+    if (move.vanish.length == 0) {
+      const color = move.appear[0].c;
+      this.reserve[color][V.PAWN]--;
+      if (this.reserve[color][V.PAWN] == 0) delete this.reserve[color];
+    }
+  }
+
+  postUndo(move) {
+    if (move.vanish.length == 0) {
+      const color = move.appear[0].c;
+      if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 };
+      this.reserve[color][V.PAWN]++;
+    }
+  }
+
+  getCurrentScore() {
+    if (this.movesCount <= 2) return "*";
+    const color = this.turn;
+    // If no stones on board, or no move available, I lose
+    if (
+      this.board.every(b => {
+        return b.every(cell => {
+          return (cell == "" || cell[0] != color);
+        });
+      })
+      ||
+      !this.atLeastOneMove()
+    ) {
+      return (color == 'w' ? "0-1" : "1-0");
+    }
+    return "*";
+  }
+
+  static get SEARCH_DEPTH() {
+    return 4;
+  }
+
+  evalPosition() {
+    let evaluation = super.evalPosition();
+    // Add reserves:
+    evaluation += this.reserve["w"][V.PAWN];
+    evaluation -= this.reserve["b"][V.PAWN];
+    return evaluation;
+  }
+
+  getNotation(move) {
+    if (move.vanish.length == 0)
+      // Placement:
+      return "@" + V.CoordsToSquare(move.end);
+    if (move.appear.length == 0)
+      // Removal after capture:
+      return V.CoordsToSquare(move.start) + "X";
+    return (
+      V.CoordsToSquare(move.start) +
+      (move.vanish.length == 2 ? "x" : "") +
+      V.CoordsToSquare(move.end)
+    );
+  }
 
 };