Reorganize folders (untested Baroque). Draft Go
authorBenjamin Auder <benjamin.auder@somewhere>
Sun, 24 Jul 2022 21:28:23 +0000 (23:28 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Sun, 24 Jul 2022 21:28:23 +0000 (23:28 +0200)
29 files changed:
pieces/Avalam/black_stack.svg [new file with mode: 0644]
pieces/Avalam/black_stack2.svg [new file with mode: 0644]
pieces/Avalam/black_stack3.svg [new file with mode: 0644]
pieces/Avalam/black_stack4.svg [new file with mode: 0644]
pieces/Avalam/black_stack5.svg [new file with mode: 0644]
pieces/Avalam/white_stack.svg [new file with mode: 0644]
pieces/Avalam/white_stack2.svg [new file with mode: 0644]
pieces/Avalam/white_stack3.svg [new file with mode: 0644]
pieces/Avalam/white_stack4.svg [new file with mode: 0644]
pieces/Avalam/white_stack5.svg [new file with mode: 0644]
variants/Atarigo/class.js
variants/Atarigo/style.css
variants/Baroque/class.js
variants/Baroque/style.css
variants/Berolina/class.js
variants/Berolina/rules.html [new file with mode: 0644]
variants/Berolina/style.css [new file with mode: 0644]
variants/Go/class.js [new file with mode: 0644]
variants/Go/rules.html [new file with mode: 0644]
variants/Go/style.css [new file with mode: 0644]
variants/_Berolina/class.js [new file with mode: 0644]
variants/_Berolina/pieces/CREDITS [moved from variants/Berolina/pieces/CREDITS with 100% similarity]
variants/_Berolina/pieces/black_pawn.svg [moved from variants/Berolina/pieces/black_pawn.svg with 100% similarity]
variants/_Berolina/pieces/white_pawn.svg [moved from variants/Berolina/pieces/white_pawn.svg with 100% similarity]
variants/_SpecialCaptures/class.js [new file with mode: 0644]
variants/_SpecialCaptures/pieces/CREDITS [new file with mode: 0644]
variants/_SpecialCaptures/pieces/capture_pull.svg [new file with mode: 0644]
variants/_SpecialCaptures/pieces/capture_push.svg [new file with mode: 0644]
variants/_SpecialCaptures/style.css [new file with mode: 0644]

diff --git a/pieces/Avalam/black_stack.svg b/pieces/Avalam/black_stack.svg
new file mode 100644 (file)
index 0000000..1ee0e2b
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/black_stack2.svg b/pieces/Avalam/black_stack2.svg
new file mode 100644 (file)
index 0000000..de29024
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>
+<path d="M100,85 h30 v30 h-30 v30 h30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/black_stack3.svg b/pieces/Avalam/black_stack3.svg
new file mode 100644 (file)
index 0000000..3518349
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>
+<path d="M100,85 h30 v30 h-30 M130,115 v30 h-30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/black_stack4.svg b/pieces/Avalam/black_stack4.svg
new file mode 100644 (file)
index 0000000..6d92f7c
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>
+<path d="M100,85 v30 h30 v30 M130,85 v30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/black_stack5.svg b/pieces/Avalam/black_stack5.svg
new file mode 100644 (file)
index 0000000..9ea4b0a
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="crimson" stroke="darkslategray"/>
+<path d="M130,85 h-30 v30 h30 v30 h-30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/white_stack.svg b/pieces/Avalam/white_stack.svg
new file mode 100644 (file)
index 0000000..d997ee1
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/white_stack2.svg b/pieces/Avalam/white_stack2.svg
new file mode 100644 (file)
index 0000000..b238b2d
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>
+<path d="M100,85 h30 v30 h-30 v30 h30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/white_stack3.svg b/pieces/Avalam/white_stack3.svg
new file mode 100644 (file)
index 0000000..3a3df31
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>
+<path d="M100,85 h30 v30 h-30 M130,115 v30 h-30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/white_stack4.svg b/pieces/Avalam/white_stack4.svg
new file mode 100644 (file)
index 0000000..91a4df3
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>
+<path d="M100,85 v30 h30 v30 M130,85 v30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
diff --git a/pieces/Avalam/white_stack5.svg b/pieces/Avalam/white_stack5.svg
new file mode 100644 (file)
index 0000000..7e8cf11
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="230" height="230">
+<circle cx="115" cy="115" r="100" fill="gold" stroke="darkslategray"/>
+<path d="M130,85 h-30 v30 h30 v30 h-30" fill="none" stroke-width="5" stroke="black"/>
+</svg>
\ No newline at end of file
index 5337f71..2cf9048 100644 (file)
-import ChessRules from "/base_rules.js";
+import GoRules from "/variants/Go/class.js";
 import Move from "/utils/Move.js";
 import PiPo from "/utils/PiPo.js";
 import {ArrayFun} from "/utils/array.js";
 
-export default class AtarigoRules extends ChessRules {
+export default class AtarigoRules extends GoRules {
 
   static get Options() {
-    return {
-      input: [
-        {
-          label: "Board size",
-          variable: "bsize",
-          type: "number",
-          defaut: 11
-        }
-      ]
-    };
-  }
-
-  get hasFlags() {
-    return false;
-  }
-  get hasEnpassant() {
-    return false;
-  }
-  get clickOnly() {
-    return true;
-  }
-
-  getSvgChessboard() {
-    const flipped = (this.playerColor == 'b');
-    let board = `
-      <svg
-        viewBox="0 0 ${10*(this.size.y)} ${10*(this.size.x)}"
-        class="chessboard_SVG">`;
-    for (let i=0; i < this.size.x; i++) {
-      for (let j=0; j < this.size.y; j++) {
-        const ii = (flipped ? this.size.x - 1 - i : i);
-        const jj = (flipped ? this.size.y - 1 - j : j);
-        board += `
-          <rect
-            id="${this.coordsToId({x: ii, y: jj})}"
-            width="10"
-            height="10"
-            x="${10*j}"
-            y="${10*i}"
-            fill="transparent"
-          />`;
-      }
-    }
-    // Add lines to delimitate "squares"
-    for (let i = 0; i < this.size.x; i++) {
-      const y = i * 10 + 5, maxX = this.size.y * 10 - 5;
-      board += `
-        <line x1="5" y1="${y}" x2="${maxX}" y2="${y}"
-              stroke="black" stroke-width="0.2"/>`;
-    }
-    for (let i = 0; i < this.size.x; i++) {
-      const x = i * 10 + 5, maxY = this.size.x * 10 - 5;
-      board += `
-        <line x1="${x}" y1="5" x2="${x}" y2="${maxY}"
-              stroke="black" stroke-width="0.2"/>`;
-    }
-    board += "</svg>";
-    return board;
-  }
-
-  get size() {
-    return {
-      x: this.options["bsize"],
-      y: this.options["bsize"],
-    };
-  }
-
-  genRandInitBaseFen() {
-    const fenLine = C.FenEmptySquares(this.size.y);
-    return {
-      fen: (fenLine + '/').repeat(this.size.x - 1) + fenLine + " w 0",
-      o: {}
-    };
-  }
-
-  pieces(color, x, y) {
-    return {
-      's': {
-        "class": "stone",
-        moves: []
-      }
-    };
-  }
-
-  doClick(coords) {
-    const [x, y] = [coords.x, coords.y];
-    if (this.board[x][y] != "")
-      return null;
-    const color = this.turn;
-    const oppCol = C.GetOppCol(color);
-    let move = new Move({
-      appear: [ new PiPo({ x: x, y: y, c: color, p: 's' }) ],
-      vanish: [],
-      start: {x: x, y: y}
-    });
-    this.playOnBoard(move); //put the stone
-    let noSuicide = false;
-    let captures = [];
-    for (let s of [[0, 1], [1, 0], [0, -1], [-1, 0]]) {
-      const [i, j] = [x + s[0], y + s[1]];
-      if (this.onBoard(i, j)) {
-        if (this.board[i][j] == "")
-          noSuicide = true; //clearly
-        else if (this.getColor(i, j) == color) {
-          // Free space for us = not a suicide
-          if (!noSuicide) {
-            let explored = ArrayFun.init(this.size.x, this.size.y, false);
-            noSuicide = this.searchForEmptySpace([i, j], color, explored);
-          }
-        }
-        else {
-          // Free space for opponent = not a capture
-          let explored = ArrayFun.init(this.size.x, this.size.y, false);
-          const captureSomething =
-            !this.searchForEmptySpace([i, j], oppCol, explored);
-          if (captureSomething) {
-            for (let ii = 0; ii < this.size.x; ii++) {
-              for (let jj = 0; jj < this.size.y; jj++) {
-                if (explored[ii][jj])
-                  captures.push(new PiPo({ x: ii, y: jj, c: oppCol, p: 's' }));
-              }
-            }
-          }
-        }
-      }
-    }
-    this.undoOnBoard(move); //remove the stone
-    if (!noSuicide && captures.length == 0)
-      return null;
-    Array.prototype.push.apply(move.vanish, captures);
-    return move;
-  }
-
-  searchForEmptySpace([x, y], color, explored) {
-    if (explored[x][y])
-      return false; //didn't find empty space
-    explored[x][y] = true;
-    let res = false;
-    for (let s of [[1, 0], [0, 1], [-1, 0], [0, -1]]) {
-      const [i, j] = [x + s[0], y + s[1]];
-      if (this.onBoard(i, j)) {
-        if (this.board[i][j] == "")
-          res = true;
-        else if (this.getColor(i, j) == color)
-          res = this.searchForEmptySpace([i, j], color, explored) || res;
-      }
-    }
-    return res;
-  }
-
-  filterValid(moves) {
-    // Suicide check not here, because side-computation of captures
-    return moves;
+    let input = GoRules.Options.input;
+    input[0].defaut = 11;
+    return {input: input};
   }
 
   getCurrentScore(move_s) {
index d38ff8e..a2579d2 100644 (file)
@@ -1,11 +1 @@
-.chessboard_SVG {
-  background-color: #BA8C63;
-}
-
-piece.white.stone {
-  background-image: url('/variants/Go/pieces/black_stone.svg');
-}
-
-piece.black.stone {
-  background-image: url('/variants/Go/pieces/white_stone.svg');
-}
+@import url("/variants/Go/style.css");
index 98ef1c7..edf1dd5 100644 (file)
@@ -1,10 +1,11 @@
 import ChessRules from "/base_rules.js";
 import GiveawayRules from "/variants/Giveaway/class.js";
+import AbstractSpecialCaptureRules from "/variants/_SpecialCaptures.js";
 import {Random} from "/utils/alea.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
-export default class BaroqueRules extends ChessRules {
+export default class BaroqueRules extends AbstractSpecialCaptureRules {
 
   static get Options() {
     return {
@@ -33,9 +34,6 @@ export default class BaroqueRules extends ChessRules {
   get hasFlags() {
     return false;
   }
-  get hasEnpassant() {
-    return false;
-  }
 
   genRandInitBaseFen() {
     if (this.options["randomness"] == 0)
@@ -61,25 +59,18 @@ export default class BaroqueRules extends ChessRules {
     return res;
   }
 
-  // Although other pieces keep their names here for coding simplicity,
-  // keep in mind that:
-  //  - a "rook" is a coordinator, capturing by coordinating with the king
-  //  - a "knight" is a long-leaper, capturing as in draughts
-  //  - a "bishop" is a chameleon, capturing as its prey
-  //  - a "queen" is a withdrawer, capturing by moving away from pieces
-
   pieces() {
     return Object.assign({},
       super.pieces(),
       {
         'p': {
-          "class": "pawn",
+          "class": "pawn", //pincer
           moves: [
             {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
           ]
         },
         'r': {
-          "class": "rook",
+          "class": "rook", //coordinator
           moves: [
             {
               steps: [
@@ -90,20 +81,20 @@ export default class BaroqueRules extends ChessRules {
           ]
         },
         'n': {
-          "class": "knight",
+          "class": "knight", //long-leaper
           moveas: 'r'
         },
         'b': {
-          "class": "bishop",
+          "class": "bishop", //chameleon
           moveas: 'r'
         },
         'q': {
-          "class": "queen",
+          "class": "queen", //withdrawer
           moveas: 'r'
         },
         'i': {
           "class": "immobilizer",
-          moveas: 'q'
+          moveas: 'r'
         }
       }
     );
@@ -162,236 +153,23 @@ export default class BaroqueRules extends ChessRules {
       return [];
     switch (moves[0].vanish[0].p) {
       case 'p':
-        this.addPawnCaptures(moves);
+        this.addPincerCaptures(moves);
         break;
       case 'r':
-        this.addRookCaptures(moves);
+        this.addCoordinatorCaptures(moves);
         break;
       case 'n':
         const [x, y] = [moves[0].start.x, moves[0].start.y];
-        moves = moves.concat(this.getKnightCaptures([x, y]));
+        moves = moves.concat(this.getLeaperCaptures([x, y]));
         break;
       case 'b':
-        moves = this.getBishopCaptures(moves);
+        moves = this.getChameleonCaptures(moves, "pull");
         break;
       case 'q':
-        this.addQueenCaptures(moves);
+        this.addPushmePullyouCaptures(moves, false, "pull");
         break;
     }
     return moves;
   }
 
-  // Modify capturing moves among listed pawn moves
-  addPawnCaptures(moves, byChameleon) {
-    const steps = this.pieces()['p'].moves[0].steps;
-    const color = this.turn;
-    const oppCol = C.GetOppCol(color);
-    moves.forEach(m => {
-      if (byChameleon && m.start.x != m.end.x && m.start.y != m.end.y)
-        // Chameleon not moving as pawn
-        return;
-      // Try capturing in every direction
-      for (let step of steps) {
-        const sq2 = [m.end.x + 2 * step[0], this.getY(m.end.y + 2 * step[1])];
-        if (
-          this.onBoard(sq2[0], sq2[1]) &&
-          this.board[sq2[0]][sq2[1]] != "" &&
-          this.getColor(sq2[0], sq2[1]) == color
-        ) {
-          // Potential capture
-          const sq1 = [m.end.x + step[0], this.getY(m.end.y + step[1])];
-          if (
-            this.board[sq1[0]][sq1[1]] != "" &&
-            this.getColor(sq1[0], sq1[1]) == oppCol
-          ) {
-            const piece1 = this.getPiece(sq1[0], sq1[1]);
-            if (!byChameleon || piece1 == 'p') {
-              m.vanish.push(
-                new PiPo({
-                  x: sq1[0],
-                  y: sq1[1],
-                  c: oppCol,
-                  p: piece1
-                })
-              );
-            }
-          }
-        }
-      }
-    });
-  }
-
-  addRookCaptures(moves, byChameleon) {
-    const color = this.turn;
-    const oppCol = V.GetOppCol(color);
-    const kp = this.searchKingPos(color)[0];
-    moves.forEach(m => {
-      // Check piece-king rectangle (if any) corners for enemy pieces
-      if (m.end.x == kp[0] || m.end.y == kp[1])
-        return; //"flat rectangle"
-      const corner1 = [m.end.x, kp[1]];
-      const corner2 = [kp[0], m.end.y];
-      for (let [i, j] of [corner1, corner2]) {
-        if (this.board[i][j] != "" && this.getColor(i, j) == oppCol) {
-          const piece = this.getPiece(i, j);
-          if (!byChameleon || piece == 'r') {
-            m.vanish.push(
-              new PiPo({
-                x: i,
-                y: j,
-                p: piece,
-                c: oppCol
-              })
-            );
-          }
-        }
-      }
-    });
-  }
-
-  getKnightCaptures(startSquare, byChameleon) {
-    // Look in every direction for captures
-    const steps = this.pieces()['r'].moves[0].steps;
-    const color = this.turn;
-    const oppCol = C.GetOppCol(color);
-    let moves = [];
-    const [x, y] = [startSquare[0], startSquare[1]];
-    const piece = this.getPiece(x, y); //might be a chameleon!
-    outerLoop: for (let step of steps) {
-      let [i, j] = [x + step[0], this.getY(y + step[1])];
-      while (this.onBoard(i, j) && this.board[i][j] == "")
-        [i, j] = [i + step[0], this.getY(j + step[1])];
-      if (
-        !this.onBoard(i, j) ||
-        this.getColor(i, j) == color ||
-        (byChameleon && this.getPiece(i, j) != 'n')
-      ) {
-        continue;
-      }
-      // last(thing), cur(thing) : stop if "cur" is our color,
-      // or beyond board limits, or if "last" isn't empty and cur neither.
-      // Otherwise, if cur is empty then add move until cur square;
-      // if cur is occupied then stop if !!byChameleon and the square not
-      // occupied by a leaper.
-      let last = [i, j];
-      let cur = [i + step[0], this.getY(j + step[1])];
-      let vanished = [new PiPo({x: x, y: y, c: color, p: piece})];
-      while (this.onBoard(cur[0], cur[1])) {
-        if (this.board[last[0]][last[1]] != "") {
-          const oppPiece = this.getPiece(last[0], last[1]);
-          if (!!byChameleon && oppPiece != 'n')
-            continue outerLoop;
-          // Something to eat:
-          vanished.push(
-            new PiPo({x: last[0], y: last[1], c: oppCol, p: oppPiece})
-          );
-        }
-        if (this.board[cur[0]][cur[1]] != "") {
-          if (
-            this.getColor(cur[0], cur[1]) == color ||
-            this.board[last[0]][last[1]] != ""
-          ) {
-            //TODO: redundant test
-            continue outerLoop;
-          }
-        }
-        else {
-          moves.push(
-            new Move({
-              appear: [new PiPo({x: cur[0], y: cur[1], c: color, p: piece})],
-              vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
-              start: {x: x, y: y},
-              end: {x: cur[0], y: cur[1]}
-            })
-          );
-        }
-        last = [last[0] + step[0], this.getY(last[1] + step[1])];
-        cur = [cur[0] + step[0], this.getY(cur[1] + step[1])];
-      }
-    }
-    return moves;
-  }
-
-  // Chameleon
-  getBishopCaptures(moves) {
-    const [x, y] = [moves[0].start.x, moves[0].start.y];
-    moves = moves.concat(this.getKnightCaptures([x, y], "asChameleon"));
-    // No "king capture" because king cannot remain under check
-    this.addPawnCaptures(moves, "asChameleon");
-    this.addRookCaptures(moves, "asChameleon");
-    this.addQueenCaptures(moves, "asChameleon");
-    // Post-processing: merge similar moves, concatenating vanish arrays
-    let mergedMoves = {};
-    moves.forEach(m => {
-      const key = m.end.x + this.size.x * m.end.y;
-      if (!mergedMoves[key])
-        mergedMoves[key] = m;
-      else {
-        for (let i = 1; i < m.vanish.length; i++)
-          mergedMoves[key].vanish.push(m.vanish[i]);
-      }
-    });
-    return Object.values(mergedMoves);
-  }
-
-  addQueenCaptures(moves, byChameleon) {
-    if (moves.length == 0)
-      return;
-    const [x, y] = [moves[0].start.x, moves[0].start.y];
-    const adjacentSteps = this.pieces()['r'].moves[0].steps;
-    let capturingDirections = {};
-    const color = this.turn;
-    const oppCol = C.GetOppCol(color);
-    adjacentSteps.forEach(step => {
-      const [i, j] = [x - step[0], this.getY(y - step[1])];
-      if (
-        this.onBoard(i, j) &&
-        this.board[i][j] != "" &&
-        this.getColor(i, j) == oppCol &&
-        (!byChameleon || this.getPiece(i, j) == 'q')
-      ) {
-        capturingDirections[step[0] + "." + step[1]] = true;
-      }
-    });
-    moves.forEach(m => {
-      const step = [
-        m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
-        m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
-      ];
-      if (capturingDirections[step[0] + "." + step[1]]) {
-        const [i, j] = [x - step[0], this.getY(y - step[1])];
-        m.vanish.push(
-          new PiPo({
-            x: i,
-            y: j,
-            p: this.getPiece(i, j),
-            c: oppCol
-          })
-        );
-      }
-    });
-  }
-
-  underAttack([x, y], oppCol) {
-    // Generate all potential opponent moves, check if king captured.
-    // TODO: do it more efficiently.
-    const color = this.getColor(x, y);
-    for (let i = 0; i < this.size.x; i++) {
-      for (let j = 0; j < this.size.y; j++) {
-        if (
-          this.board[i][j] != "" && this.getColor(i, j) == oppCol &&
-          this.getPotentialMovesFrom([i, j]).some(m => {
-            return (
-              m.vanish.length >= 2 &&
-              [1, m.vanish.length - 1].some(k => m.vanish[k].p == 'k')
-            );
-          })
-        ) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
 };
index 1fd6566..12550dc 100644 (file)
@@ -1,4 +1,5 @@
 @import url("/base_pieces.css");
+@import url("/variants/_SpecialCaptures/style.css");
 
 piece.white.immobilizer {
   background-image: url('/variants/Baroque/pieces/white_immobilizer.svg');
index fdca8fb..beabdc6 100644 (file)
@@ -1,25 +1,3 @@
-import ChessRules from "/base_rules.js";
+import AbstractBerolinaRules from "/variants/_Berolina/class.js";
 
-export default class BerolinaRules extends ChessRules {
-
-//TODO: Berolina pawns in Utils, also captures for Baroque+Fugue+...
-
-  pieces(color, x, y) {
-      const pawnShift = (color == "w" ? -1 : 1);
-      let res = super.pieces(color, x, y);
-      res['p'].moves = [
-        {
-          steps: [[pawnShift, 1], [pawnShift, -1]],
-          range: 1
-        }
-      ];
-      res['p'].attack = [
-        {
-          steps: [[pawnShift, 0]],
-          range: 1
-        }
-      ];
-      return res;
-    }
-
-};
+export default class BerolinaRules extends AbstractBerolinaRules {};
diff --git a/variants/Berolina/rules.html b/variants/Berolina/rules.html
new file mode 100644 (file)
index 0000000..c65158e
--- /dev/null
@@ -0,0 +1 @@
+<p>TODO</p>
diff --git a/variants/Berolina/style.css b/variants/Berolina/style.css
new file mode 100644 (file)
index 0000000..a3550bc
--- /dev/null
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
diff --git a/variants/Go/class.js b/variants/Go/class.js
new file mode 100644 (file)
index 0000000..b8b7780
--- /dev/null
@@ -0,0 +1,174 @@
+//TODO:
+// - pass btn on top + message if opponent just passed
+// - do not count points: rely on players' ability to do that
+// - implement Ko rule (need something in fen: lastMove)
+
+import ChessRules from "/base_rules.js";
+import Move from "/utils/Move.js";
+import PiPo from "/utils/PiPo.js";
+import {ArrayFun} from "/utils/array.js";
+
+export default class GoRules extends ChessRules {
+
+  // TODO: option oneColor (just alter pieces class of white stones)
+  static get Options() {
+    return {
+      input: [
+        {
+          label: "Board size",
+          variable: "bsize",
+          type: "number",
+          defaut: 9
+        }
+      ]
+    };
+  }
+
+  get hasFlags() {
+    return false;
+  }
+  get hasEnpassant() {
+    return false;
+  }
+  get clickOnly() {
+    return true;
+  }
+
+  getSvgChessboard() {
+    const flipped = (this.playerColor == 'b');
+    let board = `
+      <svg
+        viewBox="0 0 ${10*(this.size.y)} ${10*(this.size.x)}"
+        class="chessboard_SVG">`;
+    for (let i=0; i < this.size.x; i++) {
+      for (let j=0; j < this.size.y; j++) {
+        const ii = (flipped ? this.size.x - 1 - i : i);
+        const jj = (flipped ? this.size.y - 1 - j : j);
+        board += `
+          <rect
+            id="${this.coordsToId({x: ii, y: jj})}"
+            width="10"
+            height="10"
+            x="${10*j}"
+            y="${10*i}"
+            fill="transparent"
+          />`;
+      }
+    }
+    // Add lines to delimitate "squares"
+    for (let i = 0; i < this.size.x; i++) {
+      const y = i * 10 + 5, maxX = this.size.y * 10 - 5;
+      board += `
+        <line x1="5" y1="${y}" x2="${maxX}" y2="${y}"
+              stroke="black" stroke-width="0.2"/>`;
+    }
+    for (let i = 0; i < this.size.x; i++) {
+      const x = i * 10 + 5, maxY = this.size.x * 10 - 5;
+      board += `
+        <line x1="${x}" y1="5" x2="${x}" y2="${maxY}"
+              stroke="black" stroke-width="0.2"/>`;
+    }
+    board += "</svg>";
+    return board;
+  }
+
+  get size() {
+    return {
+      x: this.options["bsize"],
+      y: this.options["bsize"],
+    };
+  }
+
+  genRandInitBaseFen() {
+    const fenLine = C.FenEmptySquares(this.size.y);
+    return {
+      fen: (fenLine + '/').repeat(this.size.x - 1) + fenLine + " w 0",
+      o: {}
+    };
+  }
+
+  pieces(color, x, y) {
+    return {
+      's': {
+        "class": "stone",
+        moves: []
+      }
+    };
+  }
+
+  doClick(coords) {
+    const [x, y] = [coords.x, coords.y];
+    if (this.board[x][y] != "")
+      return null;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    let move = new Move({
+      appear: [ new PiPo({ x: x, y: y, c: color, p: 's' }) ],
+      vanish: [],
+      start: {x: x, y: y}
+    });
+    this.playOnBoard(move); //put the stone
+    let noSuicide = false;
+    let captures = [];
+    for (let s of [[0, 1], [1, 0], [0, -1], [-1, 0]]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (this.onBoard(i, j)) {
+        if (this.board[i][j] == "")
+          noSuicide = true; //clearly
+        else if (this.getColor(i, j) == color) {
+          // Free space for us = not a suicide
+          if (!noSuicide) {
+            let explored = ArrayFun.init(this.size.x, this.size.y, false);
+            noSuicide = this.searchForEmptySpace([i, j], color, explored);
+          }
+        }
+        else {
+          // Free space for opponent = not a capture
+          let explored = ArrayFun.init(this.size.x, this.size.y, false);
+          const captureSomething =
+            !this.searchForEmptySpace([i, j], oppCol, explored);
+          if (captureSomething) {
+            for (let ii = 0; ii < this.size.x; ii++) {
+              for (let jj = 0; jj < this.size.y; jj++) {
+                if (explored[ii][jj])
+                  captures.push(new PiPo({ x: ii, y: jj, c: oppCol, p: 's' }));
+              }
+            }
+          }
+        }
+      }
+    }
+    this.undoOnBoard(move); //remove the stone
+    if (!noSuicide && captures.length == 0)
+      return null;
+    Array.prototype.push.apply(move.vanish, captures);
+    return move;
+  }
+
+  searchForEmptySpace([x, y], color, explored) {
+    if (explored[x][y])
+      return false; //didn't find empty space
+    explored[x][y] = true;
+    let res = false;
+    for (let s of [[1, 0], [0, 1], [-1, 0], [0, -1]]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (this.onBoard(i, j)) {
+        if (this.board[i][j] == "")
+          res = true;
+        else if (this.getColor(i, j) == color)
+          res = this.searchForEmptySpace([i, j], color, explored) || res;
+      }
+    }
+    return res;
+  }
+
+  filterValid(moves) {
+    // Suicide check not here, because side-computation of captures
+    return moves;
+  }
+
+  getCurrentScore() {
+    return "*"; //Go game is a little special...
+  }
+
+};
diff --git a/variants/Go/rules.html b/variants/Go/rules.html
new file mode 100644 (file)
index 0000000..c65158e
--- /dev/null
@@ -0,0 +1 @@
+<p>TODO</p>
diff --git a/variants/Go/style.css b/variants/Go/style.css
new file mode 100644 (file)
index 0000000..233a3a5
--- /dev/null
@@ -0,0 +1,10 @@
+.chessboard_SVG {
+  background-color: #BA8C63;
+}
+
+piece.white.stone {
+  background-image: url('/variants/Go/pieces/black_stone.svg');
+}
+piece.black.stone {
+  background-image: url('/variants/Go/pieces/white_stone.svg');
+}
diff --git a/variants/_Berolina/class.js b/variants/_Berolina/class.js
new file mode 100644 (file)
index 0000000..6066e51
--- /dev/null
@@ -0,0 +1,23 @@
+import ChessRules from "/base_rules.js";
+
+export default class BerolinaRules extends ChessRules {
+
+  pieces(color, x, y) {
+    const pawnShift = (color == "w" ? -1 : 1);
+    let res = super.pieces(color, x, y);
+    res['p'].moves = [
+      {
+        steps: [[pawnShift, 1], [pawnShift, -1]],
+        range: 1
+      }
+    ];
+    res['p'].attack = [
+      {
+        steps: [[pawnShift, 0]],
+        range: 1
+      }
+    ];
+    return res;
+  }
+
+};
diff --git a/variants/_SpecialCaptures/class.js b/variants/_SpecialCaptures/class.js
new file mode 100644 (file)
index 0000000..9b48266
--- /dev/null
@@ -0,0 +1,241 @@
+import ChessRules from "/base_rules.js";
+import Move from "/utils/Move.js";
+import PiPo from "/utils/PiPo.js";
+
+export default class AbstractSpecialCaptureRules extends ChessRules {
+
+  // Wouldn't make sense:
+  get hasEnpassant() {
+    return false;
+  }
+
+  pieces() {
+    return Object.assign({},
+      super.pieces(),
+      {
+        '+': {"class": "push-action"},
+        '-': {"class": "pull-action"}
+      }
+    );
+  }
+
+  // Modify capturing moves among listed pincer moves
+  addPincerCaptures(moves, byChameleon) {
+    const steps = this.pieces()['p'].moves[0].steps;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    moves.forEach(m => {
+      if (byChameleon && m.start.x != m.end.x && m.start.y != m.end.y)
+        // Chameleon not moving as pawn
+        return;
+      // Try capturing in every direction
+      for (let step of steps) {
+        const sq2 = [m.end.x + 2 * step[0], this.getY(m.end.y + 2 * step[1])];
+        if (
+          this.onBoard(sq2[0], sq2[1]) &&
+          this.board[sq2[0]][sq2[1]] != "" &&
+          this.getColor(sq2[0], sq2[1]) == color
+        ) {
+          // Potential capture
+          const sq1 = [m.end.x + step[0], this.getY(m.end.y + step[1])];
+          if (
+            this.board[sq1[0]][sq1[1]] != "" &&
+            this.getColor(sq1[0], sq1[1]) == oppCol
+          ) {
+            const piece1 = this.getPiece(sq1[0], sq1[1]);
+            if (!byChameleon || piece1 == 'p') {
+              m.vanish.push(
+                new PiPo({
+                  x: sq1[0],
+                  y: sq1[1],
+                  c: oppCol,
+                  p: piece1
+                })
+              );
+            }
+          }
+        }
+      }
+    });
+  }
+
+  addCoordinatorCaptures(moves, byChameleon) {
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const kp = this.searchKingPos(color)[0];
+    moves.forEach(m => {
+      // Check piece-king rectangle (if any) corners for enemy pieces
+      if (m.end.x == kp[0] || m.end.y == kp[1])
+        return; //"flat rectangle"
+      const corner1 = [m.end.x, kp[1]];
+      const corner2 = [kp[0], m.end.y];
+      for (let [i, j] of [corner1, corner2]) {
+        if (this.board[i][j] != "" && this.getColor(i, j) == oppCol) {
+          const piece = this.getPiece(i, j);
+          if (!byChameleon || piece == 'r') {
+            m.vanish.push(
+              new PiPo({
+                x: i,
+                y: j,
+                p: piece,
+                c: oppCol
+              })
+            );
+          }
+        }
+      }
+    });
+  }
+
+  getLeaperCaptures([x, y], byChameleon, onlyOne) {
+    // Look in every direction for captures
+    const steps = this.pieces()['r'].moves[0].steps;
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      let [i, j] = [x + step[0], this.getY(y + step[1])];
+      while (this.onBoard(i, j) && this.board[i][j] == "")
+        [i, j] = [i + step[0], this.getY(j + step[1])];
+      if (
+        !this.onBoard(i, j) ||
+        this.getColor(i, j) == color ||
+        (byChameleon && this.getPiece(i, j) != 'n')
+      ) {
+        continue; //nothing to eat
+      }
+      let vanished = [];
+      while (true) {
+        // Found something (more) to eat:
+        vanished.push(
+          new PiPo({x: i, y: j, c: oppCol, p: this.getPiece(i, j)}));
+        [i, j] = [i + step[0], this.getY(j + step[1])];
+        while (this.onBoard(i, j) && this.board[i][j] == "") {
+          let mv = this.getBasicMove([x, y], [i, j]);
+          Array.prorotype.push.apply(mv.vanish, vanished);
+          moves.push(mv);
+          [i, j] = [i + step[0], this.getY(j + step[1])];
+        }
+        if (
+          onlyOne ||
+          !this.onBoard(i, j) ||
+          this.getColor(i, j) == color ||
+          (byChameleon && this.getPiece(i, j) != 'n')
+        ) {
+          continue outerLoop;
+        }
+      }
+    }
+    return moves;
+  }
+
+  // Chameleon
+  getChameleonCaptures(moves, pushPullType, onlyOneJump) {
+    const [x, y] = [moves[0].start.x, moves[0].start.y];
+    moves = moves.concat(
+      this.getKnightCaptures([x, y], "asChameleon", onlyOneJump));
+    // No "king capture" because king cannot remain under check
+    this.addPincerCaptures(moves, "asChameleon");
+    this.addCoordinatorCaptures(moves, "asChameleon");
+    this.addPushmePullyouCaptures(moves, "asChameleon", pushPullType);
+    // Post-processing: merge similar moves, concatenating vanish arrays
+    let mergedMoves = {};
+    moves.forEach(m => {
+      const key = m.end.x + this.size.x * m.end.y;
+      if (!mergedMoves[key])
+        mergedMoves[key] = m;
+      else {
+        for (let i = 1; i < m.vanish.length; i++)
+          mergedMoves[key].vanish.push(m.vanish[i]);
+      }
+    });
+    return Object.values(mergedMoves);
+  }
+
+  // type: nothing (freely, capture all), or pull or push, or "exclusive"
+  addPushmePullyouCaptures(moves, byChameleon, type) {
+    if (moves.length == 0)
+      return;
+    const [sx, sy] = [moves[0].start.x, moves[0].start.y];
+    const adjacentSteps = this.pieces()['r'].moves[0].steps;
+    let capturingPullDir = {};
+    const color = this.turn;
+    const oppCol = C.GetOppCol(color);
+    if (type != "push") {
+      adjacentSteps.forEach(step => {
+        const [bi, bj] = [sx - step[0], this.getY(sy - step[1])];
+        if (
+          this.onBoard(bi, bj) &&
+          this.board[bi][bj] != "" &&
+          this.getColor(bi, bj) == oppCol &&
+          (!byChameleon || this.getPiece(bi, bj) == 'q')
+        ) {
+          capturingPullDir[step[0] + "." + step[1]] = true;
+        }
+      });
+    }
+    moves.forEach(m => {
+      const [ex, ey] = [m.end.x, m.end.y];
+      const step = [
+        ex != x ? (ex - x) / Math.abs(ex - x) : 0,
+        ey != y ? (ey - y) / Math.abs(ey - y) : 0
+      ];
+      let vanishPull, vanishPush;
+      if (type != "pull") {
+        const [fi, fj] = [ex + step[0], this.getY(ey + step[1])];
+        if (
+          this.onBoard(fi, fj) &&
+          this.board[fi][fj] != "" &&
+          this.getColor(bi, bj) == oppCol &&
+          (!byChameleon || this.getPiece(fi, fj) == 'q')
+        ) {
+          vanishPush =
+            new PiPo({x: fi, y: fj, p: this.getPiece(fi, fj), c: oppCol});
+        }
+      }
+      if (capturingPullDir[step[0] + "." + step[1]]) {
+        const [bi, bj] = [x - step[0], this.getY(y - step[1])];
+        vanishPull =
+          new PiPo({x: bi, y: bj, p: this.getPiece(bi, bj), c: oppCol});
+      }
+      if (vanishPull && vanishPush && type == "exclusive") {
+        // Create a new move for push action (cannot play both)
+        let newMove = JSON.parse(JSON.stringify(m));
+        newMove.vanish.push(vanishPush);
+        newMove.choice = '+';
+        moves.push(newMove);
+        m.vanish.push(vanishPull);
+        m.choice = '-';
+      }
+      else {
+        if (vanishPull)
+          m.vanish.push(vanishPull);
+        if (vanishPush)
+          m.vanish.push(vanishPush);
+      }
+    });
+  }
+
+  underAttack([x, y], oppCol) {
+    // Generate all potential opponent moves, check if king captured.
+    // TODO: do it more efficiently.
+    const color = this.getColor(x, y);
+    for (let i = 0; i < this.size.x; i++) {
+      for (let j = 0; j < this.size.y; j++) {
+        if (
+          this.board[i][j] != "" && this.getColor(i, j) == oppCol &&
+          this.getPotentialMovesFrom([i, j]).some(m => {
+            return (
+              m.vanish.length >= 2 &&
+              [1, m.vanish.length - 1].some(k => m.vanish[k].p == 'k')
+            );
+          })
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+};
diff --git a/variants/_SpecialCaptures/pieces/CREDITS b/variants/_SpecialCaptures/pieces/CREDITS
new file mode 100644 (file)
index 0000000..efa0731
--- /dev/null
@@ -0,0 +1,2 @@
+https://commons.wikimedia.org/wiki/File:Icon-push-object-3194184.svg
+https://www.svgrepo.com/svg/323068/pull
diff --git a/variants/_SpecialCaptures/pieces/capture_pull.svg b/variants/_SpecialCaptures/pieces/capture_pull.svg
new file mode 100644 (file)
index 0000000..5c56398
--- /dev/null
@@ -0,0 +1 @@
+<svg width="512px" height="512px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#000" d="M93.773 44.664L68.55 57.39l37.313 81.938-12.09-94.664zm90.24 22.76L143.274 150.3l65.317-63.21-24.58-19.666zM18.16 125.832l10.63 26.8 45.698 5.903-56.328-32.703zm91.897 27.463c-3.665.025-7.122.8-10.256 2.295-17.278 8.244-21.157 36.154-8.663 62.34 6.016 12.59 15.09 23.08 25.218 29.158-10.305 83.743 29.287 137.784 91.366 163.535-6.917 35.032-33.276 60.587-61.855 84.023l93.987 2.895-9.897-9.165-42.893-7.88c33.39-22.314 45.968-38.168 56.854-71.397-5.27-10.354-18.877-24.948-25.432-35.895 19.945 2.308 49.183 5.725 53.745 10.135 3.78 9.84 21.27 31.79 27.754 59.832l6.336 20.523 49.205-46.476-2.654-10.328-39.57 26.59c.868-28.203-11.48-65.273-22.79-77.613 0 0-28.852-17.656-78.207-24.197-23.798-16.76-36.016-42.392-45.87-60.483l51.965 3.803 80.844-9.424s2.82 2.165 6.457 4.72c5.99 9.605 16.65 16.048 28.718 16.048 15.646 0 28.932-10.82 32.732-25.334H486v-18H366.857c-4.145-13.994-17.165-24.31-32.44-24.31-10.23 0-19.447 4.632-25.667 11.894-1.853-.17-3.7-.344-5.45-.605l-9.023 13.026-75.072 6.48-63.6-9c7.833-12.96 7.088-33.54-1.896-52.412-9.92-20.788-27.617-34.888-43.653-34.78zm224.36 83.394c8.846 0 15.825 6.976 15.825 15.822 0 8.845-6.98 15.822-15.824 15.822-2.576 0-4.986-.606-7.12-1.664 2.146-10.544-.162-23.4-1.073-27.73a15.89 15.89 0 0 1 8.193-2.25zM384 384l-32 112h128V384h-96z"/></svg>
\ No newline at end of file
diff --git a/variants/_SpecialCaptures/pieces/capture_push.svg b/variants/_SpecialCaptures/pieces/capture_push.svg
new file mode 100644 (file)
index 0000000..6e8d0fd
--- /dev/null
@@ -0,0 +1 @@
+<svg height='300px' width='300px'  fill="#000000" xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><switch><foreignObject requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/" x="0" y="0" width="1" height="1"></foreignObject><g i:extraneous="self"><g><path d="M22.5,65.4L4.7,87.9c-2,2.6-1.6,6.3,1,8.3c1.1,0.9,2.4,1.3,3.7,1.3c1.7,0,3.5-0.8,4.6-2.2l17.2-21.6l-8.3-7.8     C22.7,65.7,22.6,65.6,22.5,65.4z"></path><path d="M56.2,38l-8-6.2c-1.5-1.2-3.5-1.7-5.4-1.3c-1.9,0.4-3.5,1.5-4.5,3.2L25.5,54.4c-1.6,2.4-1.2,5.6,0.9,7.6l12.5,11.7     l-7,15.4c-1.4,3-0.1,6.5,2.9,7.9c0.8,0.4,1.6,0.5,2.5,0.5c2.2,0,4.4-1.3,5.4-3.5l8.8-19.3c1.1-2.3,0.5-5.1-1.3-6.8l-8-7.4     l10.4-13.3l12.6,3.6c0.6,0.1,1.1,0.2,1.7,0.2l14.4-1v-9.9l-14.2,1L56.2,38z"></path><ellipse transform="matrix(0.43 -0.9028 0.9028 0.43 12.8873 68.1666)" cx="60.4" cy="23.9" rx="10.4" ry="10.4"></ellipse><path d="M94.5,2.5h-5.6c-1.2,0-2.1,0.9-2.1,2.1v90.8c0,1.2,0.9,2.1,2.1,2.1h5.6c1.2,0,2.1-0.9,2.1-2.1V4.6     C96.6,3.4,95.6,2.5,94.5,2.5z"></path></g></g></switch></svg>
\ No newline at end of file
diff --git a/variants/_SpecialCaptures/style.css b/variants/_SpecialCaptures/style.css
new file mode 100644 (file)
index 0000000..3360267
--- /dev/null
@@ -0,0 +1,6 @@
+.piece.white.push-action {
+  background-image: url('/variants/_SpecialCaptures/pieces/capture_push.svg');
+}
+.piece.white.pull-action {
+  background-image: url('/variants/_SpecialCaptures/pieces/capture_pull.svg');
+}