From 04d93b7bb3b64ecdf3fb7219eee42879f0200b88 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 2 Jan 2024 13:08:07 +0100
Subject: [PATCH 01/16] update

---
 variants/Dice/class.js  | 36 +++++++++++++++++++++++++++++++-----
 variants/Dice/style.css | 13 ++++++++++++-
 2 files changed, 43 insertions(+), 6 deletions(-)

diff --git a/variants/Dice/class.js b/variants/Dice/class.js
index 02e4f6d..1bb6f1f 100644
--- a/variants/Dice/class.js
+++ b/variants/Dice/class.js
@@ -54,20 +54,47 @@ export default class DiceRules extends ChessRules {
     this.afterPlay = (move_s, newTurn, ops) => {
       // Movestack contains only one move:
       move_s[0].toplay = this.getRandomPiece(this.turn);
-      super.displayMessage(
-        this.message, "To play: " + move_s[0].toplay.toUpperCase());
+      this.toplay = move_s[0].toplay;
+      this.displayMessage(move_s[0].toplay,
+                          C.GetOppTurn(move_s[0].appear[0].c));
       o.afterPlay(move_s, newTurn, ops);
     };
   }
 
+  static get PieceToUnicode() {
+    return {
+      'K': "&#9812;",
+      'Q': "&#9813;",
+      'R': "&#9814;",
+      'B': "&#9815;",
+      'N': "&#9816;",
+      'P': "&#9817;",
+      'k': "&#9818;",
+      'q': "&#9819;",
+      'r': "&#9820;",
+      'b': "&#9821;",
+      'n': "&#9822;",
+      'p': "&#9823;"
+    };
+  }
+
+  displayMessage(piece, color) {
+    if (color == 'w')
+      piece = piece.toUpperCase();
+    super.displayMessage(this.message,
+      '<span>to play:</span> ' +
+      '<span class="symb">' + V.PieceToUnicode[piece] + '</span>'
+    );
+  }
+
   setOtherVariables(fenParsed) {
     super.setOtherVariables(fenParsed);
     this.toplay = fenParsed.toplay;
     this.message = document.createElement("div");
     C.AddClass_es(this.message, "piece-text");
-    this.message.innerHTML = "To play: " + this.toplay.toUpperCase();
     let container = document.getElementById(this.containerId);
     container.appendChild(this.message);
+    this.displayMessage(this.toplay, fenParsed.turn);
   }
 
   getRandomPiece(color) {
@@ -102,8 +129,7 @@ export default class DiceRules extends ChessRules {
 
   playReceivedMove(moves, callback) {
     this.toplay = moves[0].toplay; //only one move
-    super.displayMessage(
-      this.message, "To play: " + this.toplay.toUpperCase());
+    this.displayMessage(this.toplay, C.GetOppTurn(moves[0].appear[0].c));
     super.playReceivedMove(moves, callback);
   }
 
diff --git a/variants/Dice/style.css b/variants/Dice/style.css
index 5881bc3..c9ed898 100644
--- a/variants/Dice/style.css
+++ b/variants/Dice/style.css
@@ -1,5 +1,11 @@
 @import url("/base_pieces.css");
 
+/* doesn't work:
+@font-face {
+  font-family: chess-font;
+  src: url(/assets/FreeSerifBold-rdMp.otf);
+} */
+
 div.piece-text {
   position: relative;
   margin-top: 15px;
@@ -8,5 +14,10 @@ div.piece-text {
   background-color: transparent;
   color: darkred;
   font-weight: bold;
-  font-size: 2em;
+  font-size: 1.7em;
 }
+
+/*
+div.piece-text > span.symb {
+  font-family: chess-font;
+} */
-- 
2.48.1


From 03883a0d1495bc6e6fadc3a11aa99286aba5c9e1 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 2 Jan 2024 16:03:28 +0100
Subject: [PATCH 02/16] Fix Dice display

---
 variants/Dice/class.js  | 32 ++++++++++++--------------------
 variants/Dice/style.css | 11 ++++++-----
 2 files changed, 18 insertions(+), 25 deletions(-)

diff --git a/variants/Dice/class.js b/variants/Dice/class.js
index 1bb6f1f..53836eb 100644
--- a/variants/Dice/class.js
+++ b/variants/Dice/class.js
@@ -61,29 +61,21 @@ export default class DiceRules extends ChessRules {
     };
   }
 
-  static get PieceToUnicode() {
-    return {
-      'K': "&#9812;",
-      'Q': "&#9813;",
-      'R': "&#9814;",
-      'B': "&#9815;",
-      'N': "&#9816;",
-      'P': "&#9817;",
-      'k': "&#9818;",
-      'q': "&#9819;",
-      'r': "&#9820;",
-      'b': "&#9821;",
-      'n': "&#9822;",
-      'p': "&#9823;"
-    };
-  }
-
   displayMessage(piece, color) {
-    if (color == 'w')
-      piece = piece.toUpperCase();
+    if (color == 'b') {
+      const blackPieceToCode = {
+        'k': 'l',
+        'p': 'o',
+        'n': 'm',
+        'b': 'v',
+        'q': 'w',
+        'r': 't'
+      };
+      piece = blackPieceToCode[piece];
+    }
     super.displayMessage(this.message,
       '<span>to play:</span> ' +
-      '<span class="symb">' + V.PieceToUnicode[piece] + '</span>'
+      '<span class="symb">' + piece + '</span>'
     );
   }
 
diff --git a/variants/Dice/style.css b/variants/Dice/style.css
index c9ed898..ad11010 100644
--- a/variants/Dice/style.css
+++ b/variants/Dice/style.css
@@ -1,10 +1,9 @@
 @import url("/base_pieces.css");
 
-/* doesn't work:
 @font-face {
   font-family: chess-font;
-  src: url(/assets/FreeSerifBold-rdMp.otf);
-} */
+  src: url(/assets/MERIFONT.TTF);
+}
 
 div.piece-text {
   position: relative;
@@ -17,7 +16,9 @@ div.piece-text {
   font-size: 1.7em;
 }
 
-/*
 div.piece-text > span.symb {
   font-family: chess-font;
-} */
+  display: inline-block;
+  position: relative;
+  top: 5px;
+}
-- 
2.48.1


From 130a166fd08355be5f2dfc923777c1c6d03f09ce Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 3 Jan 2024 01:40:00 +0100
Subject: [PATCH 03/16] Chakart: fixing attempt

---
 variants/Chakart/class.js | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/variants/Chakart/class.js b/variants/Chakart/class.js
index 2b502b6..3454d8f 100644
--- a/variants/Chakart/class.js
+++ b/variants/Chakart/class.js
@@ -130,6 +130,12 @@ export default class ChakartRules extends ChessRules {
     );
   }
 
+  isKing(x, y, p) {
+    if (!p)
+      p = this.getPiece(x, y);
+    return ['k', 'l'].includes(p);
+  }
+
   genRandInitBaseFen() {
     const s = FenUtil.setupPieces(
       ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
@@ -630,19 +636,25 @@ export default class ChakartRules extends ChessRules {
         });
         break;
       case "koopa":
-        // Reverse move
+        // Reverse move, if possible
         em = new Move({
-          appear: [
-            new PiPo({
-              x: move.start.x, y: move.start.y, c: color, p: move.appear[0].p
-            })
-          ],
+          appear: [],
           vanish: [
             new PiPo({
               x: move.end.x, y: move.end.y, c: color, p: move.appear[0].p
             })
-          ]
+          ],
+          end: {x: move.start.x, y: move.start.y} //may be irrelevant
         });
+        em.koopa = true; //avoid applying effect
+        if (move.vanish.length == 0)
+          // After toadette+drop, just erase piece
+          break;
+        em.appear.push(
+          new PiPo({
+            x: move.start.x, y: move.start.y, c: color, p: move.appear[0].p
+          })
+        );
         if (this.board[move.start.x][move.start.y] != "") {
           // Pawn or knight let something on init square
           em.vanish.push(new PiPo({
@@ -652,7 +664,6 @@ export default class ChakartRules extends ChessRules {
             p: this.getPiece(move.start.x, move.start.y)
           }));
         }
-        em.koopa = true; //avoid applying effect
         break;
       case "chomp":
         // Eat piece
-- 
2.48.1


From 3232aba3419f129c70d5edd9a4ded1fefc146ea0 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 3 Jan 2024 15:38:58 +0100
Subject: [PATCH 04/16] Add Discoduel, draft Dobutsu, some code cleaning

---
 base_rules.js                   |  20 +-
 pieces/Dobutsu/LICENSE.txt      | 395 ++++++++++++++++++++++++++++++++
 pieces/Dobutsu/README.md        |  13 ++
 pieces/Dobutsu/chick.svg        | 139 +++++++++++
 pieces/Dobutsu/elephant.svg     | 176 ++++++++++++++
 pieces/Dobutsu/giraffe.svg      | 186 +++++++++++++++
 pieces/Dobutsu/hen.svg          | 156 +++++++++++++
 pieces/Dobutsu/lion.svg         | 201 ++++++++++++++++
 pieces/Dobutsu/rev_chick.svg    | 139 +++++++++++
 pieces/Dobutsu/rev_elephant.svg | 176 ++++++++++++++
 pieces/Dobutsu/rev_giraffe.svg  | 186 +++++++++++++++
 pieces/Dobutsu/rev_hen.svg      | 156 +++++++++++++
 pieces/Dobutsu/rev_lion.svg     | 201 ++++++++++++++++
 utils/array.js                  |   8 +-
 variants.js                     |   4 +-
 variants/Avalanche/class.js     |   2 +-
 variants/Chaining/class.js      |   4 +-
 variants/Clorange/class.js      |  13 +-
 variants/Convert/class.js       |   4 +-
 variants/Coregal/class.js       |   4 +-
 variants/Discoduel/class.js     |  51 +++++
 variants/Discoduel/rules.html   |   5 +
 variants/Discoduel/style.css    |   1 +
 variants/Dobutsu/class.js       | 100 ++++++++
 variants/Dobutsu/rules.html     |  12 +
 variants/Dobutsu/style.css      |  34 +++
 26 files changed, 2355 insertions(+), 31 deletions(-)
 create mode 100644 pieces/Dobutsu/LICENSE.txt
 create mode 100644 pieces/Dobutsu/README.md
 create mode 100644 pieces/Dobutsu/chick.svg
 create mode 100644 pieces/Dobutsu/elephant.svg
 create mode 100644 pieces/Dobutsu/giraffe.svg
 create mode 100644 pieces/Dobutsu/hen.svg
 create mode 100644 pieces/Dobutsu/lion.svg
 create mode 100644 pieces/Dobutsu/rev_chick.svg
 create mode 100644 pieces/Dobutsu/rev_elephant.svg
 create mode 100644 pieces/Dobutsu/rev_giraffe.svg
 create mode 100644 pieces/Dobutsu/rev_hen.svg
 create mode 100644 pieces/Dobutsu/rev_lion.svg
 create mode 100644 variants/Discoduel/class.js
 create mode 100644 variants/Discoduel/rules.html
 create mode 100644 variants/Discoduel/style.css
 create mode 100644 variants/Dobutsu/class.js
 create mode 100644 variants/Dobutsu/rules.html
 create mode 100644 variants/Dobutsu/style.css

diff --git a/base_rules.js b/base_rules.js
index 0f89cc4..217d055 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -338,7 +338,7 @@ export default class ChessRules {
 
   getReserveFen(o) {
     if (o.init)
-      return "000000000000";
+      return Array(2 * V.ReserveArray.length).fill('0').join("");
     return (
       ['w', 'b'].map(c => Object.values(this.reserve[c]).join("")).join("")
     );
@@ -416,14 +416,14 @@ export default class ChessRules {
   }
 
   // Some additional variables from FEN (variant dependant)
-  setOtherVariables(fenParsed, pieceArray) {
+  setOtherVariables(fenParsed) {
     // Set flags and enpassant:
     if (this.hasFlags)
       this.setFlags(fenParsed.flags);
     if (this.hasEnpassant)
       this.epSquare = this.getEpSquare(fenParsed.enpassant);
     if (this.hasReserve && !this.isDiagram)
-      this.initReserves(fenParsed.reserve, pieceArray);
+      this.initReserves(fenParsed.reserve);
     if (this.options["crazyhouse"])
       this.initIspawn(fenParsed.ispawn);
     if (this.options["teleport"]) {
@@ -441,14 +441,16 @@ export default class ChessRules {
   }
 
   // ordering as in pieces() p,r,n,b,q,k
-  initReserves(reserveStr, pieceArray) {
-    if (!pieceArray)
-      pieceArray = ['p', 'r', 'n', 'b', 'q', 'k'];
+  static get ReserveArray() {
+    return ['p', 'r', 'n', 'b', 'q', 'k'];
+  }
+
+  initReserves(reserveStr) {
     const counts = reserveStr.split("").map(c => parseInt(c, 36));
-    const L = pieceArray.length;
+    const L = V.ReserveArray.length;
     this.reserve = {
-      w: ArrayFun.toObject(pieceArray, counts.slice(0, L)),
-      b: ArrayFun.toObject(pieceArray, counts.slice(L, 2 * L))
+      w: ArrayFun.toObject(V.ReserveArray, counts.slice(0, L)),
+      b: ArrayFun.toObject(V.ReserveArray, counts.slice(L, 2 * L))
     };
   }
 
diff --git a/pieces/Dobutsu/LICENSE.txt b/pieces/Dobutsu/LICENSE.txt
new file mode 100644
index 0000000..2f244ac
--- /dev/null
+++ b/pieces/Dobutsu/LICENSE.txt
@@ -0,0 +1,395 @@
+Attribution 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+	wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More_considerations
+     for the public:
+	wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution 4.0 International Public License ("Public License"). To the
+extent this Public License may be interpreted as a contract, You are
+granted the Licensed Rights in consideration of Your acceptance of
+these terms and conditions, and the Licensor grants You such rights in
+consideration of benefits the Licensor receives from making the
+Licensed Material available under these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  d. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  e. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  f. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  g. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  h. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  i. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  j. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  k. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part; and
+
+            b. produce, reproduce, and Share Adapted Material.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+       4. If You Share Adapted Material You produce, the Adapter's
+          License You apply must not prevent recipients of the Adapted
+          Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material; and
+
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/pieces/Dobutsu/README.md b/pieces/Dobutsu/README.md
new file mode 100644
index 0000000..73ed708
--- /dev/null
+++ b/pieces/Dobutsu/README.md
@@ -0,0 +1,13 @@
+[Couch Tomato - I presume]
+
+I recreated all of the original [Doubutsu](https://en.wikipedia.org/wiki/D%C5%8Dbutsu_sh%C5%8Dgi) pieces in Inkscape SVG format.
+
+Credits: the original Doubutsu pieces were created by [Madoka Kitato](https://en.wikipedia.org/wiki/Madoka_Kitao).
+
+(also: [screenshot of the gshogi version](https://raw.githubusercontent.com/Ka-hu/shogi-pieces/master/_screenshots/scrot_doubutsu_gshogi.png))
+
+![doubutsu screenshot](https://raw.githubusercontent.com/Ka-hu/shogi-pieces/master/_screenshots/scrot_doubutsu_xboard.png)
+
+## License
+
+Where it's not stated otherwise, my work is lincensed under [CC-BY-4.0](https://choosealicense.com/licenses/cc-by-4.0)
diff --git a/pieces/Dobutsu/chick.svg b/pieces/Dobutsu/chick.svg
new file mode 100644
index 0000000..69358c7
--- /dev/null
+++ b/pieces/Dobutsu/chick.svg
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wp.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="1"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="36.115918"
+     inkscape:cx="41.902375"
+     inkscape:zoom="6.9423186"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     transform="matrix(0.34437547,0,0,0.34437547,-1.9107969,29.84957)"
+     id="g5891">
+    <g
+       transform="matrix(3.7795276,0,0,3.7795276,208.50911,-1088.428)"
+       id="layer1"
+       inkscape:label="Layer 1">
+      <g
+         transform="translate(0.39418863,0.66145971)"
+         id="g6119">
+        <g
+           id="g5971"
+           transform="matrix(0.85211113,0,0,0.85211113,-9.1404396,-13.169231)">
+          <rect
+             ry="2.487885"
+             y="332.36246"
+             x="-46.119144"
+             height="40.824493"
+             width="40.824493"
+             id="rect5497-3"
+             style="opacity:1;fill:#edf0a1;fill-opacity:1;stroke:#000000;stroke-width:0.651029;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <g
+             transform="translate(-0.2672693,0.13363444)"
+             id="g5958">
+            <g
+               id="g5904"
+               transform="translate(-0.73499179,1.5368003)"
+               style="stroke:#000000">
+              <g
+                 id="g5898"
+                 style="stroke:#000000" />
+            </g>
+          </g>
+        </g>
+        <circle
+           style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           id="path5973-1"
+           cx="-31.264896"
+           cy="273.07776"
+           r="1.2745767" />
+      </g>
+      <g
+         transform="matrix(0.35009033,-0.05050649,0.05050649,0.35009033,-19.229149,187.55284)"
+         id="g6286">
+        <path
+           inkscape:connector-curvature="0"
+           id="path6261"
+           d="m -75.660215,305.19885 c 0,-2.48371 -0.0798,-2.56409 -1.87012,-1.88342 -2.41462,0.91804 -2.50427,0.90603 -2.50427,-0.33539 0,-0.57552 1.12506,-1.45323 2.50012,-1.95046 2.25596,-0.81576 3.29198,-2.28919 3.32245,-4.72518 0.005,-0.43776 -1.32276,-1.19522 -2.95163,-1.68324 -5.68459,-1.70315 -11.56197,-8.85642 -11.60371,-14.12269 -0.0165,-2.08889 -1.13247,-2.88354 -5.30173,-3.7754 -2.70122,-0.57782 -2.52265,-1.19262 0.91695,-3.15686 2.03265,-1.16077 2.91064,-2.15435 2.91064,-3.29382 0,-4.0488 5.92224,-9.26078 10.52278,-9.26078 5.63193,0 9.16198,3.90372 9.16198,10.1318 v 2.88799 l 3.82759,0.77982 c 4.43634,0.90384 15.964605,2.9357 16.76851,2.95545 2.09006,0.0514 -1.121794,6.10417 -5.931245,11.17759 -3.706562,3.90999 -6.560135,5.50339 -11.486195,6.41376 -2.34974,0.43425 -4.51329,1.16796 -4.80789,1.63046 -1.1685,1.83447 -0.49993,4.06374 1.6854,5.61984 1.60256,1.14112 2.01907,1.82171 1.45813,2.38265 -0.56094,0.56094 -1.03514,0.53533 -1.66011,-0.0896 -1.35319,-1.35319 -2.64506,-1.03783 -2.90201,0.70841 -0.40945,2.78264 -2.05564,2.45359 -2.05564,-0.41089 z m 12.81142,-13.42174 c 3.779908,-1.94721 10.518678,-9.37439 10.518678,-11.59322 0,-0.60586 -16.293918,-3.90531 -19.137968,-3.87535 -1.06346,0.0112 -1.27586,-0.66016 -1.27586,-4.03263 0,-4.30418 -1.37381,-7.87991 -3.31346,-8.62423 -3.26899,-1.25443 -6.32779,-0.46103 -9.16642,2.37761 -1.55677,1.55677 -2.83049,3.481 -2.83049,4.27607 0,0.79507 -0.65268,2.27533 -1.4504,3.28947 l -1.45041,1.84389 1.783,0.4475 c 1.3768,0.34555 1.88174,1.06902 2.21638,3.17562 1.02892,6.4771 5.05524,11.66104 10.74063,13.82872 3.2351,1.23345 9.88605,0.67941 13.36632,-1.11345 z m -21.49557,-20.11619 c -1.06128,-1.27877 0.10307,-4.08773 1.69442,-4.08773 1.69444,0 2.9949,2.15128 2.13039,3.52417 -1.02722,1.63128 -2.73041,1.88224 -3.82481,0.56356 z"
+           style="opacity:1;fill:#000000;stroke-width:0.729065" />
+        <path
+           style="opacity:1;fill:#fede00;fill-opacity:1;stroke:none;stroke-width:0.0992535;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           d="m -72.758456,293.6586 c -1.882216,-0.11998 -2.936236,-0.3775 -4.486955,-1.09624 -1.915997,-0.88803 -3.377483,-1.92599 -4.910434,-3.4874 -1.364201,-1.38958 -2.197702,-2.54467 -3.080824,-4.26959 -0.885609,-1.72981 -1.346113,-3.08689 -1.81202,-5.33995 -0.588442,-2.84554 -0.880885,-3.20656 -3.001908,-3.70565 -0.506169,-0.11911 -1.43245,-0.2452 -1.472363,-0.28019 -0.03995,-0.0349 0.800051,-0.55434 1.280511,-1.1541 1.096659,-1.36903 1.782525,-2.67779 1.979012,-3.77641 0.07611,-0.42501 0.245588,-0.98016 0.376832,-1.23368 0.44715,-0.86372 1.46694,-2.16712 2.485991,-3.17737 2.489186,-2.46767 5.059796,-3.41614 7.849016,-2.89602 1.608531,0.29995 2.318878,0.69675 3.049811,1.70367 0.571542,0.78732 1.060961,1.91892 1.361115,3.14695 0.352172,1.44093 0.433935,2.16945 0.529032,4.71386 0.04794,1.2832 0.138116,2.51597 0.200362,2.73947 0.166333,0.59726 0.552874,0.87923 1.20528,0.87923 2.473345,0 16.297805,2.64486 19.009187,3.63679 0.587231,0.21484 0.602082,0.27123 0.276396,1.04969 -1.083116,2.58884 -5.958295,7.83649 -9.434093,10.15484 -2.530966,1.68816 -7.172599,2.66179 -11.403948,2.3921 z m -9.351663,-21.1933 c 0.585216,-0.17084 1.391569,-0.88338 1.677789,-1.48258 0.349336,-0.73133 0.138006,-1.84008 -0.487701,-2.55865 -0.635926,-0.73033 -1.83698,-1.0713 -2.530947,-0.71852 -0.390337,0.19841 -0.964046,0.85791 -1.192525,1.37082 -0.248964,0.5589 -0.33732,1.58266 -0.177639,2.05828 0.210201,0.62608 0.820486,1.1739 1.551159,1.39239 0.377572,0.1129 0.602665,0.10091 1.159864,-0.0618 z"
+           id="path6272"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="sscsssscccssccccsccsssscccscs" />
+        <path
+           style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.107755;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           d="m -80.257078,270.14973 c 1.63e-4,1.36347 -0.943221,2.16412 -2.362351,2.44503 -0.924307,0.18296 -2.337613,-0.68871 -2.315104,-2.27966 9.35e-4,-1.36481 1.186961,-2.92759 2.244233,-2.75215 1.338711,0.22215 2.563523,1.22335 2.433222,2.58678 z"
+           id="path6280"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cscsc" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/elephant.svg b/pieces/Dobutsu/elephant.svg
new file mode 100644
index 0000000..c7eb6dc
--- /dev/null
+++ b/pieces/Dobutsu/elephant.svg
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="we.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="29.422532"
+     inkscape:cx="43.514924"
+     inkscape:zoom="2.3306093"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="1920"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(1.3015765,0,0,1.3015765,69.894622,-344.97829)"
+     style="display:inline">
+    <rect
+       style="opacity:1;fill:#d7b5d6;fill-opacity:1;stroke:#000000;stroke-width:0.554749;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="rect5497-7"
+       width="34.787006"
+       height="34.787006"
+       x="-48.044888"
+       y="270.70197"
+       ry="2.1199546" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="0.17839615"
+       inkscape:transform-center-y="0.22740032"
+       transform="rotate(-43.147535,-16.240471,257.24518)" />
+    <g
+       id="g11547"
+       transform="matrix(0.78674347,0,0,0.78674347,32.599138,66.939052)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path11355"
+         d="m -94.896099,289.4836 c -0.832835,-2.74859 -1.581643,-5.86315 -1.664014,-6.92125 -0.467012,-5.99924 4.023942,-12.41806 9.893478,-14.14054 7.15559,-2.09988 15.648661,0.46744 20.120448,6.08209 2.889226,3.62763 3.109811,8.68772 0.680167,15.60262 l -1.188679,3.38305 -1.845166,-0.31004 c -1.221219,-0.20521 -1.926269,-0.49853 -2.085001,-0.86749 -0.179337,-0.41686 -0.280337,-0.31153 -0.400409,0.41755 -0.19722,1.19774 -1.560314,1.61598 -4.791295,1.47017 -2.10144,-0.0949 -2.185115,-0.13072 -2.221905,-0.95248 -0.128739,-2.87575 -0.112166,-2.83031 -1.089881,-2.98827 -0.523142,-0.0842 -2.377098,-0.10864 -4.12,-0.0536 l -3.168914,0.10008 0.02709,1.27786 c 0.0149,0.70281 0.05363,1.62118 0.08607,2.0408 0.05432,0.70267 -0.19501,0.76884 -3.15584,0.83752 -1.768141,0.041 -3.292906,0.0622 -3.388356,0.047 -0.0954,-0.0152 -0.854944,-2.27645 -1.68779,-5.02505 z m 6.999492,1.84208 c 0.188116,-1.89539 0.240411,-1.98878 1.158991,-2.06995 2.213783,-0.19565 8.285389,-0.13053 8.80068,0.0944 0.382707,0.16704 0.545927,0.79617 0.520421,2.00592 l -0.03716,1.76255 1.657803,0.0199 c 3.33991,0.0401 3.125836,0.31765 3.760825,-4.87578 0.480579,-3.93053 0.635801,-4.57323 1.087192,-4.50142 0.753394,0.11985 0.989944,1.72234 0.69791,4.72798 -0.271477,2.79408 0.07535,3.61094 1.638749,3.85965 0.5682,0.0904 0.86954,-0.1254 1.122462,-0.80378 2.909844,-7.80472 2.958863,-13.06686 0.15323,-16.44895 -4.716874,-5.68602 -14.351731,-8.01527 -20.692941,-5.00258 -2.493104,1.18447 -4.712951,3.36971 -6.035189,5.94111 -1.960575,3.8128 -1.917938,6.53884 0.205994,13.17068 l 1.347994,4.20902 2.208014,-0.0519 2.20802,-0.0519 z m 4.058543,-9.04758 c -0.534212,-0.34694 -1.2957,-1.11772 -1.692197,-1.71284 -0.632674,-0.94961 -0.671074,-1.39528 -0.31376,-3.6414 0.435869,-2.73994 1.052415,-3.6787 3.133991,-4.77185 2.097335,-1.10142 6.211459,-0.0959 7.525233,1.83922 0.827018,1.21816 0.257296,1.9713 -0.62848,0.83082 -0.844153,-1.08689 -2.192374,-1.82945 -3.763758,-2.07298 -1.699947,-0.26334 -2.930766,0.16919 -4.126097,1.45036 -1.205636,1.29221 -1.780878,4.15358 -1.111364,5.52815 0.984525,2.02129 4.340136,3.04014 7.703987,2.33911 2.31126,-0.48166 2.135829,-0.49043 2.041585,0.102 -0.04197,0.26384 -0.903023,0.68947 -1.913452,0.94586 -2.119116,0.53771 -5.346033,0.14401 -6.855688,-0.83645 z m 11.554976,-4.06154 c -0.320041,-0.92338 0.121111,-1.70146 1.043501,-1.84046 1.367284,-0.20606 2.055279,1.59204 0.923372,2.41324 -0.726824,0.52732 -1.681992,0.24916 -1.966873,-0.57278 z"
+         style="fill:#000000;stroke-width:0.352778" />
+    </g>
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-0"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="-0.17839695"
+       inkscape:transform-center-y="0.22739943"
+       transform="matrix(-0.72959515,-0.68387931,-0.68387931,0.72959515,118.7706,58.453823)" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-0-6"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="-0.17839359"
+       inkscape:transform-center-y="-0.22740736"
+       transform="rotate(136.85247,-43.022022,282.2147)" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-3"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="0.17839801"
+       inkscape:transform-center-y="-0.22739811"
+       transform="matrix(0.72959515,0.68387931,0.68387931,-0.72959515,-180.21624,517.53904)" />
+    <path
+       style="opacity:1;fill:#ececec;fill-opacity:1;stroke:none;stroke-width:0.119309;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m 74.139644,41.577099 c -7.87925,0.0344 -15.910281,1.840605 -22.559814,6.199482 -6.309211,4.167165 -11.500302,10.062477 -14.697207,16.9265 -2.213273,4.568929 -3.674258,9.602623 -3.477825,14.717746 -0.07625,5.230598 1.176924,10.349521 2.482485,15.376736 1.790146,6.470337 3.99909,12.815717 5.997555,19.223117 0.164345,0.47836 0.222918,1.27816 0.931692,1.0867 4.214874,-0.0791 8.437214,-0.13905 12.645897,-0.34626 0.379412,-0.52598 0.19193,-1.32307 0.356599,-1.94493 0.373122,-2.83963 0.437493,-5.74822 1.090315,-8.53244 0.175564,-0.89763 1.04368,-1.34847 1.888439,-1.39378 4.151815,-0.47235 8.389359,-0.34111 12.584414,-0.42264 4.561604,0.033 9.14162,-0.0526 13.675284,0.47728 1.274717,0.10982 1.760504,1.62543 1.86344,2.7231 0.174097,2.83818 -0.02633,5.73095 0.127167,8.55924 1.157999,0.20468 2.467207,0.0243 3.681637,0.0845 2.660029,-0.0672 5.346743,0.15349 7.988739,-0.22064 1.564689,-0.27059 2.585689,-1.67616 2.947099,-3.13749 0.94131,-3.49225 1.14155,-7.13177 1.6611,-10.70037 0.57517,-4.113742 0.90181,-8.276925 1.89945,-12.311257 0.19403,-0.568009 0.45502,-1.535725 1.2066,-1.489997 1.3768,0.342131 1.69685,1.93116 1.98035,3.10543 0.51257,3.057311 0.2501,6.192576 0.10488,9.273317 -0.17813,3.215647 -0.79431,6.554127 0.13903,9.711677 0.46431,1.72784 2.05791,2.86873 3.74837,3.22011 1.26528,0.37979 2.95038,0.4484 3.7454,-0.82543 1.06382,-1.83482 1.58733,-3.93086 2.33274,-5.90606 2.97462,-8.920196 5.37535,-18.255223 4.94735,-27.732617 -0.26758,-6.116481 -2.20529,-12.28491 -6.21025,-16.998922 C 110.48227,52.280052 101.01466,46.886586 91.034372,43.980465 85.555277,42.383517 79.852082,41.511598 74.139644,41.577099 Z m 3.95084,9.000124 c 4.704084,0.128216 9.515821,1.45646 13.302645,4.33754 1.629986,1.331346 3.221186,3.08382 3.490155,5.251757 0.119765,0.634319 -0.532616,1.383296 -1.155657,0.88561 -1.587129,-1.11051 -2.576883,-2.876888 -4.173842,-3.991079 -3.464629,-2.712391 -7.965229,-4.04112 -12.345211,-3.83563 -5.020062,0.273894 -9.368527,3.986053 -11.243999,8.524559 -1.447065,3.507684 -2.18364,7.46856 -1.36663,11.221506 0.825487,3.156378 3.58606,5.377708 6.380729,6.795161 4.668377,2.271569 10.057335,2.587368 15.140881,1.957959 2.582029,-0.345524 5.111886,-1.067583 7.686496,-1.366872 0.457329,0.0249 0.440173,0.409267 0.328497,0.724073 -0.0909,0.92294 -1.169542,1.300315 -1.849152,1.731022 -2.933947,1.441111 -6.220674,2.085822 -9.506865,1.993296 -2.265629,0.06433 -4.539422,0.04373 -6.766801,-0.428497 -3.633383,-0.624177 -7.402018,-1.761493 -10.016357,-4.511249 -1.666534,-1.717506 -3.356136,-3.580704 -4.079167,-5.911375 -0.56297,-2.800071 0.125487,-5.662817 0.516667,-8.462987 0.628315,-3.647725 1.444049,-7.593098 4.247771,-10.215463 2.092951,-1.902154 4.583972,-3.449702 7.304545,-4.260639 1.342727,-0.32288 2.722961,-0.437975 4.105295,-0.438692 z m 28.106706,13.924125 c 2.09728,-0.198003 3.79837,1.910793 3.63936,3.912861 0.005,2.012253 -1.92467,3.776325 -3.91755,3.668215 -2.0441,0.09828 -3.62553,-1.911925 -3.49163,-3.86699 -0.23314,-1.524587 0.85037,-3.002766 2.28789,-3.497801 0.46352,-0.214569 0.98179,-0.216482 1.48193,-0.216285 z"
+       id="path12210"
+       inkscape:connector-curvature="0"
+       transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)" />
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/giraffe.svg b/pieces/Dobutsu/giraffe.svg
new file mode 100644
index 0000000..14a55a9
--- /dev/null
+++ b/pieces/Dobutsu/giraffe.svg
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="60"
+   height="60"
+   version="1.1"
+   id="svg4905"
+   sodipodi:docname="wg.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="960"
+     inkscape:window-height="1060"
+     id="namedview4907"
+     showgrid="false"
+     inkscape:zoom="6.1989695"
+     inkscape:cx="10.721179"
+     inkscape:cy="23.933131"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4905"
+     inkscape:document-rotation="0" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       in="SourceAlpha"
+       result="blur"
+       stdDeviation="2"
+       id="feGaussianBlur4880" />
+    <feOffset
+       result="offsetBlur"
+       dx="2"
+       dy="2"
+       id="feOffset4882" />
+    <feBlend
+       in="SourceGraphic"
+       in2="offsetBlur"
+       mode="normal"
+       id="feBlend4884" />
+  </filter>
+  <g
+     id="g6562"
+     transform="matrix(0.95833332,0,0,0.95833332,5.0076046,1.2500008)">
+    <g
+       transform="matrix(1.0779714,0,0,1.0779714,-2.6340321,-1.1975641)"
+       id="g5697">
+      <rect
+         style="display:inline;opacity:1;fill:#d7b5d6;fill-opacity:1;stroke:#000000;stroke-width:0.698944;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="rect5497"
+         width="43.82914"
+         height="43.82914"
+         x="4.7211394"
+         y="7.0264244"
+         ry="2.6709912" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.314982;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754"
+         sodipodi:sides="3"
+         sodipodi:cx="43.863743"
+         sodipodi:cy="28.832012"
+         sodipodi:r1="1.9764072"
+         sodipodi:r2="0.9882037"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m 42.875539,30.543631 0,-3.423238 2.964611,1.711619 z"
+         inkscape:transform-center-x="-0.13073194"
+         inkscape:transform-center-y="6.3154318e-06" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.314982;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-3"
+         sodipodi:sides="3"
+         sodipodi:cx="-8.4619169"
+         sodipodi:cy="28.832012"
+         sodipodi:r1="1.9764072"
+         sodipodi:r2="0.9882037"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -9.4501205,30.543631 0,-3.423238 2.9646107,1.711619 z"
+         inkscape:transform-center-x="0.13073078"
+         inkscape:transform-center-y="6.3154318e-06"
+         transform="scale(-1,1)" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-6"
+         sodipodi:sides="3"
+         sodipodi:cx="-28.324093"
+         sodipodi:cy="268.94327"
+         sodipodi:r1="1.5686662"
+         sodipodi:r2="0.78433317"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+         inkscape:transform-center-x="-2.0711132e-05"
+         inkscape:transform-center-y="-0.13072954"
+         transform="matrix(0,-1.2599285,1.2599285,0,-312.62562,-24.814417)" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-1"
+         sodipodi:sides="3"
+         sodipodi:cx="-31.130423"
+         sodipodi:cy="307.49689"
+         sodipodi:r1="1.5686662"
+         sodipodi:r2="0.78433317"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -31.914756,308.85539 0,-2.71701 2.353,1.35851 z"
+         inkscape:transform-center-x="1.5695237e-05"
+         inkscape:transform-center-y="0.13072936"
+         transform="matrix(0,1.2599285,-1.2599285,0,413.64771,86.363342)" />
+      <g
+         style="display:inline"
+         id="g2811"
+         transform="matrix(0.18596187,0,0,0.18596187,76.914042,-20.843006)">
+        <path
+           inkscape:connector-curvature="0"
+           id="path2787"
+           d="m -312.65013,373.40136 c -1.85093,-0.1579 -2.91447,-0.42442 -3.32156,-0.83239 -0.50864,-0.50975 -0.66598,-2.02398 -1.04469,-10.05417 -1.58267,-33.55938 -3.31088,-95.28498 -3.31521,-118.40819 -0.002,-12.81813 0.0951,-15.58274 0.74052,-20.99028 0.81944,-6.86597 0.82798,-15.76085 0.0166,-17.2521 -0.63664,-1.17003 -1.87618,-0.99627 -4.57154,0.64086 -3.90181,2.36992 -8.00372,4.22203 -9.82126,4.43454 -2.15904,0.25243 -3.50338,-0.65506 -3.87067,-2.61286 -0.9049,-4.82351 3.38328,-21.29494 7.14443,-27.44269 0.70513,-1.15255 1.80887,-2.96867 2.45276,-4.03582 1.63352,-2.70733 2.74987,-7.77018 2.47391,-11.21963 -0.19215,-2.40177 -0.14758,-2.63512 0.65688,-3.43958 1.09977,-1.09977 1.97452,-1.08757 4.07727,0.0569 1.59784,0.86965 1.74981,1.08361 2.78177,3.91643 0.60038,1.64809 1.0916,3.31836 1.0916,3.71171 0,0.68408 0.14191,0.70479 3.26319,0.47609 4.50202,-0.32987 5.55625,-0.54682 5.55625,-1.14341 0,-1.43638 4.16187,-7.05525 5.46873,-7.38325 2.08607,-0.52357 3.72085,2.1852 3.98227,6.59846 l 0.15912,2.68622 4.91114,0.17662 c 4.51602,0.16241 4.9809,0.24216 5.77827,0.99125 0.49302,0.46317 0.8638,1.20665 0.85939,1.72328 -0.02,2.33989 -3.02181,6.55139 -6.05387,8.49336 l -1.92782,1.23472 -0.0213,4.05695 c -0.0154,2.92974 -0.34062,5.87027 -1.17049,10.58333 -1.13393,6.43989 -1.14843,6.66199 -1.09446,16.75695 0.0668,12.49427 0.86399,33.04778 2.03752,52.53156 l 0.23178,3.84822 12.91084,-0.32044 c 16.92717,-0.42014 27.99848,-0.35267 32.13723,0.19583 10.71002,1.41938 17.71182,5.09252 21.86824,11.47205 0.82389,1.26454 1.57955,2.29917 1.67925,2.29917 0.0997,0 1.46272,-0.67469 3.02891,-1.49931 3.76743,-1.98359 7.13819,-5.21592 7.46828,-7.16156 0.50888,-2.99948 1.02895,-3.68635 2.79113,-3.68635 1.52437,0 2.32253,0.92752 2.32253,2.69896 0,4.67767 -3.81137,9.15295 -10.85336,12.74395 -3.23267,1.64847 -3.26567,1.67845 -2.99861,2.72514 0.36046,1.41281 0.17194,16.72296 -0.26064,21.16667 -0.18888,1.94028 -0.51211,6.22653 -0.71828,9.525 -0.94344,15.09368 -2.75484,36.44507 -3.73908,44.07347 -0.92172,7.14377 -1.37218,7.42945 -12.36259,7.84003 -6.80759,0.25431 -7.88037,0.0429 -8.60018,-1.69487 -0.5464,-1.31914 -0.24558,-15.84814 0.51981,-25.1057 0.28577,-3.45636 0.63065,-8.95472 0.76641,-12.21857 l 0.24684,-5.93427 -2.07864,-0.2311 c -5.6162,-0.6244 -13.66945,-0.95825 -27.68744,-1.1478 l -15.37826,-0.20794 -0.23619,2.78143 c -0.12991,1.52979 -0.13736,6.43268 -0.0165,10.89532 0.25003,9.23665 -0.36761,30.19407 -0.92029,31.22675 -0.20118,0.37591 -0.66492,0.84357 -1.03054,1.03924 -0.81597,0.43669 -12.58976,0.74084 -16.32929,0.42183 z m 74.48702,-4.98909 c 1.63495,-0.18796 3.08352,-0.52117 3.21906,-0.74047 0.57509,-0.93053 2.70657,-24.25893 3.84912,-42.12771 0.21091,-3.29847 0.4466,-6.49853 0.52374,-7.11125 0.13832,-1.09859 0.1248,-1.10959 -0.97648,-0.79375 -2.14933,0.61642 -5.57393,0.37673 -7.4714,-0.52294 -2.08205,-0.98718 -4.15786,-3.04462 -4.92166,-4.87809 -0.74078,-1.7782 -0.68618,-5.04937 0.11368,-6.81013 1.50854,-3.32083 5.22075,-5.56732 9.18634,-5.55925 1.2793,0.002 2.89697,0.24646 3.59781,0.54235 l 1.27339,0.53762 0.19928,-3.34015 c 0.1096,-1.83709 0.10836,-3.90038 -0.003,-4.5851 -0.22793,-1.40454 0.0427,-1.38716 -4.85998,-0.31218 -5.13257,1.12538 -7.70231,0.50609 -7.70231,-1.85621 0,-1.75748 0.87077,-2.40774 3.582,-2.67489 2.46925,-0.24332 6.30242,-1.03665 6.64002,-1.37426 0.34074,-0.34074 -1.92227,-3.3053 -3.66154,-4.79665 -3.35344,-2.87545 -8.62518,-4.8202 -15.26234,-5.63028 -4.66409,-0.56926 -14.81667,-0.90787 -14.81667,-0.49415 0,0.19418 0.61545,0.76034 1.36767,1.25813 1.62689,1.07663 2.66143,2.64494 3.22589,4.89027 1.26309,5.02435 -3.50701,9.89903 -9.68667,9.89903 -5.5211,0 -9.70806,-3.6093 -9.70806,-8.36867 0,-2.33696 0.94374,-4.3049 2.88257,-6.01088 l 1.58819,-1.39746 -10.9581,0.1074 -10.95811,0.1074 -1.25099,1.34381 c -1.60924,1.72863 -3.16654,2.51035 -5.47578,2.74869 -2.50441,0.25848 -5.17435,-0.65365 -6.8665,-2.34579 -5.40418,-5.40426 1.21785,-13.63501 8.88704,-11.04591 l 2.02792,0.68462 v -1.53043 c 0,-0.84173 -0.31928,-7.68303 -0.70951,-15.20288 -0.39024,-7.51985 -0.83405,-18.38929 -0.98626,-24.15432 -0.1522,-5.76502 -0.36378,-10.5689 -0.47017,-10.67529 -0.10639,-0.10639 -0.87316,0.0464 -1.70394,0.33949 -0.83077,0.29312 -1.94682,0.53294 -2.48012,0.53294 -1.65051,0 -3.99757,-1.03063 -5.3475,-2.34818 l -1.29055,-1.25959 -0.23147,2.42124 c -0.12731,1.33169 -0.53875,5.13792 -0.91431,8.45828 -0.37556,3.32037 -0.68283,7.48363 -0.68283,9.25169 v 3.21465 l 0.95421,-0.36279 c 0.52482,-0.19954 1.88592,-0.36725 3.02467,-0.3727 5.10003,-0.0244 8.71073,3.81592 7.85857,8.35832 -0.34758,1.8528 -3.0343,4.45718 -5.23321,5.07283 -1.64004,0.45919 -4.63074,0.37047 -6.02734,-0.1788 -0.63663,-0.25038 -0.65011,0.10935 -0.41859,11.17164 0.13157,6.28669 0.38594,16.58973 0.56526,22.89563 0.17932,6.3059 0.328,12.80264 0.3304,14.43719 l 0.004,2.97191 0.79814,-0.74982 c 2.28294,-2.1447 7.14035,-2.9437 10.43442,-1.71637 6.28856,2.34305 8.14263,10.01828 3.54031,14.65571 -3.34439,3.36989 -9.47925,3.85764 -13.40059,1.0654 l -1.05055,-0.74806 0.17135,0.82882 c 0.0942,0.45586 0.26803,4.71821 0.38617,9.47189 0.19469,7.83315 0.84736,25.44802 1.46071,39.42291 l 0.23612,5.37987 h 5.9439 c 3.26915,0 5.97148,-0.0397 6.00518,-0.0882 0.0337,-0.0485 0.15276,-10.04552 0.26458,-22.21558 l 0.20332,-22.1274 0.7933,-0.79292 c 0.71479,-0.71444 1.27342,-0.8184 5.64444,-1.05048 2.66813,-0.14166 6.97,-0.16207 9.5597,-0.0453 l 4.70856,0.21221 -0.79977,-1.55239 c -1.80318,-3.50009 -0.54368,-8.34406 2.783,-10.70321 2.10677,-1.49403 3.39564,-1.90369 6.08751,-1.93485 3.22516,-0.0373 5.41203,0.79084 7.43798,2.81679 2.78918,2.78918 3.43986,6.63757 1.69944,10.05128 -0.37699,0.73946 -0.62213,1.40777 -0.54476,1.48514 0.0774,0.0774 2.33716,0.28129 5.02175,0.45315 10.079,0.64523 11.01501,0.85269 12.15278,2.69365 0.75551,1.22243 0.76929,1.44293 0.54436,8.70881 -0.12692,4.10004 -0.46859,10.23275 -0.75926,13.62823 -0.29068,3.39549 -0.63349,9.46768 -0.76182,13.49375 l -0.23333,7.32014 2.43723,-0.003 c 1.34047,-10e-4 3.77491,-0.15646 5.40985,-0.34442 z m -73.55005,-165.34849 c 2.34246,-1.93086 4.72369,-2.29617 7.66997,-1.17667 1.00916,0.38345 1.85967,0.66126 1.89001,0.61736 0.3683,-0.53286 1.93095,-12.09821 1.92933,-14.27913 l -0.002,-2.91041 h -1.23472 c -1.59788,0 -2.64583,-1.0263 -2.64583,-2.59116 0,-1.5785 0.8489,-2.26776 3.3661,-2.73309 2.43486,-0.45011 4.93076,-1.64312 6.13219,-2.93111 l 0.85525,-0.91687 h -1.92489 c -1.05869,0 -2.79293,0.24412 -3.85386,0.54248 -3.61979,1.018 -4.73548,1.04531 -5.58201,0.13668 -0.41617,-0.44671 -0.75667,-1.10805 -0.75667,-1.46964 0,-0.60884 -0.22816,-0.64042 -3.08681,-0.42728 -1.69774,0.12658 -4.7444,0.43687 -6.77036,0.68953 -3.82982,0.47762 -5.14983,0.30371 -5.54681,-0.7308 -0.14619,-0.38096 -0.64837,0.35484 -1.54706,2.26675 -0.73183,1.55695 -1.79988,3.51496 -2.37344,4.35113 -1.71104,2.49448 -3.13907,5.59931 -4.61479,10.03354 -1.4857,4.46423 -2.87185,10.56427 -2.87185,12.63817 v 1.26016 l 2.02848,-1.02918 c 1.11566,-0.56604 3.13972,-1.68277 4.49791,-2.48161 3.2437,-1.90784 4.57335,-2.25259 6.8252,-1.76966 2.15283,0.46171 4.0366,2.32358 4.64794,4.59393 l 0.39861,1.4803 0.63699,-1.04746 c 0.35035,-0.5761 1.22033,-1.52828 1.93329,-2.11596 z m -10.95718,-15.78632 c -1.46782,-0.63883 -2.25012,-1.83328 -2.25012,-3.43554 0,-2.63394 1.42463,-4.20124 3.8492,-4.23471 0.7349,-0.0101 1.68609,0.16883 2.11375,0.39771 1.00784,0.53938 2.1483,2.4219 2.1483,3.54612 0,1.88554 -2.32007,4.25878 -4.12499,4.21952 -0.34918,-0.008 -1.13045,-0.22949 -1.73614,-0.4931 z"
+           style="fill:#000000;stroke-width:0.352778" />
+        <path
+           sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+           inkscape:connector-curvature="0"
+           id="path2801"
+           transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)"
+           d="m -990.69336,-203.23047 c -0.72413,2.18092 -0.65023,4.67577 -1.01562,6.97266 -1.93273,20.6186 -4.88178,41.15125 -5.92383,61.8457 -0.23039,6.29307 -0.41988,12.64031 -0.35157,18.90625 3.13928,-1.05371 6.3769,-2.16228 9.73633,-2.35156 6.42628,-0.53113 13.06825,-0.36343 19.03321,2.30273 8.62723,3.61737 15.75805,11.84487 16.30078,21.447268 0.3795,4.041166 0.12234,8.324624 -2.05274,11.882813 -2.83441,5.11813 -7.5459,8.968176 -12.35156,12.1875 -6.47515,4.81064 -17.97607,2.176862 -30.11969,1.77566 -1.52317,14.107494 0.51261,28.73156 0.4068,42.894252 1.24784,49.625432 1.43349,102.141435 3.21004,151.749037 0.0823,2.54007 3.42531,-3.90455 4.33683,-4.26466 11.10869,-7.58519 26.23202,-9.56755 38.8711,-4.85157 14.06439,5.30782 24.03343,20.24572 22.08008,35.38477 -1.27083,18.30956 -19.78045,31.47459 -37.40235,30.65234 -9.40147,0.22321 -19.14303,-4.07717 -26.66967,-9.65085 1.6132,27.18186 2.6314,56.31648 3.71069,83.86765 1.4261,41.32999 3.16015,83.07612 4.80969,124.39751 14.87742,0.31066 30.37549,-0.0986 45.24304,-0.43676 0.30753,-55.80769 1.06035,-111.61358 1.57422,-167.41992 2.96487,-4.24355 7.69893,-6.36297 12.81641,-6.32227 21.48631,-1.73532 44.03095,-0.21664 65.55554,-0.0863 -3.13102,-6.67281 -7.13373,-14.93705 -6.02625,-22.72233 1.14529,-15.11029 13.63019,-27.78487 28.36915,-30.37305 11.81642,-1.62252 25.19896,-0.51829 34.23046,8.08398 11.2328,9.05062 15.1926,25.50905 8.93165,38.66323 -0.47321,2.07749 -2.86753,5.04567 -2.41156,7.02372 18.55812,2.99631 38.12219,1.6026 56.55023,5.46149 6.47343,2.06521 10.78238,9.16068 10.61718,15.59961 -0.23089,55.6971 -7.02662,104.15812 -6.84765,153.71875 13.89385,0.0363 28.85623,0.14546 42.31034,-3.43173 4.04169,-22.89354 5.14987,-46.56784 7.49874,-69.67764 3.31107,-39.86433 6.94783,-80.6375 8.90097,-120.58896 -17.12596,1.72351 -29.91805,2.01087 -41.95615,-7.8505 -5.98029,-5.48025 -11.50667,-12.71191 -10.46289,-21.34765 -0.15026,-6.82521 -0.61902,-14.18573 3.75586,-19.96485 7.71659,-12.26984 22.7261,-18.9267 37.00586,-17.10156 4.51627,0.0293 10.00104,2.70204 14.23618,3.81356 1.26505,-8.06843 1.83637,-26.69624 0.30084,-33.282695 -7.56999,-4.370724 -29.88311,6.098215 -42.08194,2.922261 -6.93337,-2.594022 -6.85169,-14.730865 0.73242,-16.353515 10.6579,-3.024928 23.08991,-2.424899 33.41972,-6.694728 -21.69443,-45.280364 -87.37221,-42.327471 -125.90711,-41.919039 -5.65433,-0.395057 0.25067,3.521325 1.46913,4.269818 9.77376,6.649335 16.22057,17.796964 14.36397,29.759964 -3.12905,16.780114 -20.7238,27.921319 -37.22071,27.011719 -16.06949,1.028843 -33.30735,-9.558106 -36.23828,-26.035156 -2.09789,-10.508207 2.20539,-20.742063 10.3874,-27.513871 6.66855,-3.867358 12.07068,-7.043155 -0.86201,-5.78691 -25.25652,0.373292 -50.63308,0.249638 -75.86703,0.726423 -7.67181,10.045393 -20.11056,17.620113 -33.03922,15.470842 -12.71644,-1.463772 -25.60169,-11.404223 -25.8164,-24.996094 -0.36023,-12.988892 10.38533,-24.01921554 22.6875,-26.6367184 8.67936,-2.9020934 17.65989,1.69540393 26.04537,3.6029275 0.66328,-7.6371427 -0.87874,-16.5424901 -0.96139,-24.3587871 -0.33743,-17.637058 -0.70948,-33.185841 -2.15815,-49.177722 -1.71463,-34.360239 -2.83058,-68.745339 -3.94532,-103.128909 -0.34129,-6.02647 -0.28657,-12.11309 -1.15429,-18.08984 -0.78671,-0.7879 -2.17859,0.066 -3.13282,0.13281 -5.47609,1.54506 -11.16025,3.94805 -16.91601,2.42578 -7.34487,-1.44008 -13.72498,-5.87138 -18.8457,-11.1875 -0.58384,-0.38256 -1.04918,-1.24314 -1.75977,-1.34961 z"
+           style="opacity:1;fill:#fee51f;fill-opacity:1;stroke:#000000;stroke-width:0.377953;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccc"
+           transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)"
+           inkscape:connector-curvature="0"
+           id="path2805"
+           d="m -1021.1418,-349.43123 c -2.5149,3.24383 -3.9482,7.19131 -5.9322,10.76386 -3.3983,7.07103 -7.5582,13.71971 -11.6846,20.37954 -8.1977,14.58673 -13.5773,30.5648 -18.0463,46.63045 -2.5018,9.65052 -4.9876,19.36588 -6.1255,29.28794 -0.2304,2.70972 -0.3048,5.48435 -0.1283,8.17057 6.0148,-2.69029 11.8854,-5.97973 17.6982,-9.18021 6.8025,-3.64409 13.289,-8.09696 20.6905,-10.46795 5.5501,-1.45039 11.6856,-0.78169 16.857,1.68323 6.3343,3.1904 11.05127,9.28721 12.78816,16.15228 0.52888,1.41832 0.57416,3.71002 1.40924,4.96703 1.03114,-0.56986 1.49065,-2.64523 2.24122,-3.56327 4.35521,-6.33437 10.49651,-11.82188 17.92133,-14.14904 4.41827,-1.17657 9.19378,-1.20618 13.65728,-0.14107 4.00817,0.9669 7.76655,2.84108 11.78544,3.70767 0.93543,-0.22946 0.64198,-1.68326 0.99141,-2.37198 2.62066,-14.41005 4.4583,-28.95783 6.02347,-43.51699 0.63543,-6.24472 0.45511,-12.52834 0.48185,-18.79433 -1.64946,-0.47193 -3.67212,-0.0292 -5.4397,-0.33399 -4.13679,-0.13045 -8.34258,-3.10335 -8.87834,-7.40069 -0.83536,-3.67416 0.27128,-8.18867 3.94326,-9.89514 5.36335,-2.6521 11.54823,-2.84317 17.08929,-5.06083 6.66822,-2.33168 12.93973,-6.2173 17.35243,-11.79921 0.54795,-1.36058 -2.5541,-0.24437 -3.47228,-0.53811 -7.17681,-0.0293 -14.30014,1.08908 -21.20627,3.00401 -4.8507,1.13232 -9.94445,2.8009 -14.94267,1.60298 -2.84036,-1.23548 -4.87524,-4.07723 -5.6313,-7.02421 0.239,-1.60086 -1.35257,-2.66529 -2.81491,-2.54906 -9.94599,0.20448 -19.83546,1.51103 -29.72257,2.5295 -6.81544,0.64103 -13.66764,1.81197 -20.52414,1.17078 -2.2897,-0.15271 -4.4487,-1.45957 -5.4596,-3.58199 -0.2964,-0.56815 -0.677,0.13363 -0.9214,0.31823 z m 1.7899,17.87398 c 3.8288,0.0605 8.1973,0.60002 10.7877,3.77229 2.6857,2.91607 4.8339,6.61345 5.2534,10.58391 0.099,5.93406 -4.2282,10.95026 -8.8669,14.11402 -2.848,2.06997 -6.5807,2.66444 -9.9062,1.50536 -4.7884,-1.22144 -9.4371,-4.3749 -10.9208,-9.30001 -1.0236,-3.4725 -0.7643,-7.27869 0.038,-10.78121 1.2645,-4.93689 5.5433,-8.88754 10.6133,-9.62292 0.9985,-0.17858 1.9882,-0.26085 3.0015,-0.27144 z"
+           style="opacity:1;fill:#fee51f;fill-opacity:1;stroke:#000000;stroke-width:0.477237;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/hen.svg b/pieces/Dobutsu/hen.svg
new file mode 100644
index 0000000..7256bc8
--- /dev/null
+++ b/pieces/Dobutsu/hen.svg
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wh.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="61.263113"
+     inkscape:cx="76.812636"
+     inkscape:zoom="3.2959793"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(1.3015765,0,0,1.3015765,69.894622,-344.97829)">
+    <rect
+       style="opacity:1;fill:#edf0a1;fill-opacity:1;stroke:#000000;stroke-width:0.554749;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="rect5497-2"
+       width="34.787006"
+       height="34.787006"
+       x="-48.044888"
+       y="270.70197"
+       ry="2.1199546" />
+    <circle
+       style="opacity:1;fill:#0b58ad;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-0"
+       cx="-45.230762"
+       cy="273.83298"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0f57ab;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-6"
+       cx="-30.870708"
+       cy="273.73923"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-8-8"
+       cx="-16.774204"
+       cy="273.77951"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-7"
+       cx="-45.185184"
+       cy="288.05371"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-9"
+       cx="-16.831141"
+       cy="287.99677"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-0"
+       cx="-30.934549"
+       cy="302.49182"
+       r="1.2745767" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path16248"
+       d="m -31.09696,299.47227 c 0.0095,-0.52127 0.05802,-0.92293 -0.610947,-0.59557 -0.85443,0.12097 -0.889136,-0.21522 -0.04394,-0.68328 0.93344,-0.51693 0.52879,-0.85019 -1.466111,-1.20745 -2.172542,-0.38907 -4.192654,-1.58878 -5.050403,-2.9994 -0.790183,-1.29947 -1.765666,-4.85974 -1.812239,-6.61418 -0.03751,-1.41302 -0.05424,-1.44006 -0.949045,-1.5346 -0.500963,-0.0529 -0.90076,-0.18737 -0.888426,-0.29876 0.06064,-0.54649 1.342995,-1.84264 1.901485,-1.92171 0.482403,-0.0683 1.065208,-0.79567 2.448718,-3.05617 1.954396,-3.19322 2.867922,-4.06623 3.678698,-3.51552 0.265415,0.18029 0.46239,0.5189 0.437711,0.75248 -0.03154,0.29849 0.101737,0.38169 0.448527,0.28 1.273294,-0.37343 1.425603,-0.32365 1.432469,0.46816 0.006,0.69712 0.09848,0.77933 0.884518,0.78669 0.74543,0.007 0.892853,0.11479 0.977824,0.71489 0.06623,0.46781 -0.113395,0.99447 -0.531534,1.55797 -0.629719,0.84878 -0.630543,0.85657 -0.2787,2.63746 0.869604,4.40167 0.790743,4.35218 7.614537,4.77933 0.859128,0.0538 0.944998,0.13794 1.048722,1.02673 0.405679,3.47681 -2.476426,6.46366 -6.863114,7.11257 -1.040053,0.15384 -1.901664,0.38069 -1.914705,0.50411 -0.01304,0.12343 0.327472,0.39004 0.756694,0.59247 0.878196,0.21503 0.605417,1.04634 -0.07841,0.65159 -0.538509,-0.31086 -0.556179,0.21386 -0.534405,0.51221 0.01781,0.24926 -0.125219,0.49028 -0.317816,0.53561 -0.322434,-0.10319 -0.300026,-0.43899 -0.290175,-0.48563 z m 3.855138,-3.3002 c 1.789437,-0.5862 3.607835,-1.93987 4.21373,-3.13686 0.481217,-1.07046 0.593381,-1.72965 0.548888,-3.21437 l -2.331599,-0.2393 c -4.2878,-0.11664 -5.813264,-1.13043 -6.17034,-4.10067 -0.398195,-3.3123 -1.414362,-4.74162 -3.529954,-4.96514 -1.907317,-0.20152 -4.161011,2.02701 -4.806714,4.75304 -0.538639,2.27398 0.656754,7.22981 2.172678,9.00746 1.846031,2.16474 6.413132,3.03892 9.903338,1.89558 z m -10.328753,-11.9987 c -0.194949,-0.72947 0.243336,-1.26905 0.89727,-1.10464 0.439844,0.11057 0.580945,0.33114 0.528624,0.82634 -0.05232,0.49518 -0.236396,0.68137 -0.689638,0.6976 -0.370854,0.0133 -0.665494,-0.15447 -0.736256,-0.4193 z m -2.26204,0.79557 c 0.119527,-0.23429 0.162966,-0.49309 0.09663,-0.57514 -0.154261,-0.1907 -1.00841,0.3706 -1.04157,0.68445 -0.04434,0.41963 0.720291,0.33116 0.944968,-0.10931 z m 9.376027,-4.4866 c 0.05251,-0.49702 -0.332386,-0.47964 -0.914509,0.0413 -0.390364,0.34933 -0.432657,0.52056 -0.199728,0.80852 0.23294,0.28799 0.378264,0.27412 0.6862,-0.0654 0.21487,-0.23691 0.407487,-0.5899 0.428037,-0.7844 z m -3.665022,-1.55259 c 0.118213,-1.1819 -0.209237,-1.48617 -0.928772,-0.86422 -0.350194,0.30269 -0.789171,0.85243 -0.975474,1.22162 l -0.338739,0.67128 1.087255,-0.17052 c 0.905397,-0.14199 1.09871,-0.28554 1.15573,-0.85816 z m 1.85388,0.59382 c 0.07792,-0.73751 -0.162538,-0.88163 -0.71942,-0.43118 -0.549736,0.44468 -0.494206,0.98599 0.10767,1.04958 0.396278,0.0419 0.559412,-0.12304 0.61175,-0.6184 z"
+       style="fill:#242424;stroke-width:0.271694"
+       sodipodi:nodetypes="scccccsccccsccscccccsscscccsccccscssccscccssccscccscsccccsssss" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0487106;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -39.830057,284.33445 c -0.276783,0.0304 -0.52047,0.20358 -0.728611,0.37929 -0.134606,0.13365 -0.335461,0.34709 -0.197293,0.542 0.175344,0.20382 0.487878,0.10429 0.686156,-0.009 0.269695,-0.16936 0.410184,-0.5177 0.368867,-0.82721 -0.01834,-0.0544 -0.07488,-0.0821 -0.129118,-0.085 z"
+       id="path16437"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -34.447197,277.72847 c -0.419763,0.0618 -0.71458,0.42649 -0.993164,0.71605 -0.369959,0.43048 -0.687414,0.92159 -0.891201,1.44922 0.07046,0.12741 0.26613,0.0135 0.37938,0.0226 0.51652,-0.105 1.074208,-0.11594 1.547292,-0.36848 0.340453,-0.20386 0.275841,-0.64853 0.318688,-0.98481 0.01323,-0.29611 -0.0075,-0.7473 -0.360993,-0.83461 z"
+       id="path16439"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -32.481251,278.81671 c -0.329586,0.0528 -0.580074,0.32865 -0.79179,0.56755 -0.18367,0.23394 -0.125644,0.63657 0.183044,0.72862 0.251854,0.0839 0.620882,0.089 0.73923,-0.20154 0.112891,-0.29634 0.166194,-0.65188 0.05015,-0.95441 -0.03733,-0.0659 -0.10301,-0.12984 -0.180634,-0.14022 z"
+       id="path16441"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -30.632906,280.10995 c -0.390383,0.0441 -0.706502,0.33919 -0.956638,0.6228 -0.137489,0.13193 -0.200958,0.3555 -0.0539,0.5062 0.10895,0.16841 0.327234,0.4162 0.539306,0.24398 0.308294,-0.25144 0.565873,-0.60239 0.661609,-0.99218 0.02413,-0.14399 -0.0098,-0.36433 -0.190367,-0.3808 z"
+       id="path16443"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.944882;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m 62.210574,49.835998 c -7.602046,1.113589 -12.839704,8.152794 -15.526367,14.861328 -2.782026,8.63198 -0.699623,18.001013 1.951172,26.40625 2.141547,6.547016 5.991927,13.082154 12.644531,15.822264 10.003611,4.74327 22.121465,5.06319 32.338587,0.82686 6.810733,-3.16294 14.033553,-8.233637 15.188753,-16.237991 0.10685,-1.965379 1.38602,-4.718681 -0.18554,-6.318359 C 100.73961,83.249937 92.072342,84.616587 84.523074,81.166076 78.029951,78.154331 77.466206,70.171571 76.255496,64.033264 75.140123,58.455107 72.598968,52.001309 66.478152,50.464904 65.103259,50.027013 63.656885,49.770857 62.210574,49.835998 Z m -6.719726,9.563477 c 2.643469,-0.0866 2.498373,3.829211 1.166015,5.207031 -1.860948,0.956683 -5.182134,0.440375 -4.657226,-2.348633 -0.09317,-1.894851 1.727671,-3.085295 3.491211,-2.858398 z"
+       id="path16538"
+       inkscape:connector-curvature="0"
+       transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)" />
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/lion.svg b/pieces/Dobutsu/lion.svg
new file mode 100644
index 0000000..8324454
--- /dev/null
+++ b/pieces/Dobutsu/lion.svg
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wk.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="59.217968"
+     inkscape:cx="48.32528"
+     inkscape:zoom="3.2959793"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(1.3015766,0,0,1.3015766,69.894626,-344.97834)">
+    <g
+       id="g6119"
+       transform="translate(0.39418863,0.66145971)">
+      <g
+         transform="matrix(0.85211113,0,0,0.85211113,-9.1404396,-13.169231)"
+         id="g5971">
+        <rect
+           style="opacity:1;fill:#f7c1c1;fill-opacity:1;stroke:#000000;stroke-width:0.651029;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           id="rect5497-2"
+           width="40.824493"
+           height="40.824493"
+           x="-46.119144"
+           y="332.36246"
+           ry="2.487885" />
+        <g
+           id="g5958"
+           transform="translate(-0.2672693,0.13363444)">
+          <path
+             sodipodi:nodetypes="ccsccccccssscsscsscsccccsccscccccc"
+             style="opacity:1;fill:#e4ac19;stroke:#000000;stroke-width:0.819396"
+             d="m -37.301682,366.13828 c 0.002,-0.11924 0.522273,-1.64077 1.156259,-3.3812 0.633988,-1.74043 1.152704,-3.2662 1.152704,-3.39063 0,-0.32365 -2.195217,-1.40951 -3.974473,-1.96596 l -1.517424,-0.47457 1.566993,-1.35283 c 1.954106,-1.68705 3.211789,-4.05509 2.964154,-5.58109 -0.183744,-1.13229 -1.423763,-3.2143 -3.267038,-5.48538 -0.88153,-1.08614 -1.070029,-1.47555 -0.722662,-1.49293 1.098081,-0.055 4.031467,0.58601 5.173172,1.13032 1.995908,0.95155 2.170218,0.71969 1.992058,-2.64997 -0.08402,-1.58904 -0.0512,-2.88917 0.0729,-2.88917 0.273899,0 4.946292,2.91618 5.998452,3.74381 0.414055,0.32569 0.941179,0.59217 1.171389,0.59217 0.23021,0 1.846068,-1.11994 3.590793,-2.48876 1.744725,-1.36882 3.25661,-2.40439 3.359742,-2.30126 0.103132,0.10314 0.258321,1.34246 0.344862,2.75405 0.08655,1.41159 0.254071,2.6631 0.372286,2.78116 0.118214,0.11804 0.718661,0.0636 1.334328,-0.12074 0.615665,-0.18447 2.200527,-0.33539 3.521914,-0.33539 h 2.402519 l -2.363791,2.42173 c -1.300086,1.33196 -2.36252,2.66527 -2.360967,2.96292 0.0045,0.86651 1.300484,3.41307 2.639495,5.18657 1.235324,1.63617 1.591495,2.4367 1.084151,2.4367 -0.42153,0 -5.13769,2.31254 -5.320982,2.6091 -0.09015,0.14587 0.02946,0.56156 0.265771,0.92377 0.398788,0.61121 2.195934,5.41847 2.195934,5.87398 0,0.11169 -0.162598,0.0745 -0.36133,-0.083 -0.682067,-0.53989 -3.542695,-1.44967 -5.682151,-1.80711 -2.96605,-0.49556 -8.442689,-0.42722 -10.530672,0.13138 -0.928028,0.24827 -2.669895,0.90539 -3.870816,1.46024 -2.390315,1.1044 -2.392717,1.1052 -2.387583,0.79204 z"
+             id="path5748"
+             inkscape:connector-curvature="0" />
+          <ellipse
+             ry="7.7261157"
+             rx="7.3980627"
+             transform="scale(1,-1)"
+             cy="-353.74197"
+             cx="-25.470678"
+             id="path5766"
+             style="opacity:1;fill:#fce377;fill-opacity:1;stroke:#000000;stroke-width:0.575783;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <circle
+             r="0.90203464"
+             cy="352.09091"
+             cx="-27.963072"
+             id="path5835"
+             style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.58824;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <g
+             style="stroke:#000000"
+             transform="translate(-0.73499179,1.5368003)"
+             id="g5904">
+            <path
+               style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+               d="m -24.989701,353.86158 c 0.639558,1.14696 1.410085,1.83553 2.405426,1.73725"
+               id="path5881"
+               inkscape:connector-curvature="0"
+               sodipodi:nodetypes="cc" />
+            <g
+               style="stroke:#000000"
+               id="g5898">
+              <path
+                 sodipodi:nodetypes="cc"
+                 inkscape:connector-curvature="0"
+                 id="path5879"
+                 d="m -27.127856,355.19792 c 2.20977,-0.3229 2.294654,-1.1816 2.228597,-2.43715"
+                 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+              <path
+                 sodipodi:nodetypes="cc"
+                 inkscape:connector-curvature="0"
+                 id="path5883"
+                 d="m -26.552828,355.01933 c 0.905568,0.93577 2.165489,1.15095 3.071057,0.44884"
+                 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+            </g>
+          </g>
+          <circle
+             r="0.90203464"
+             cy="352.12433"
+             cx="-23.252451"
+             id="path5835-6"
+             style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.58824;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+        </g>
+      </g>
+      <circle
+         r="1.2745767"
+         cy="273.17151"
+         cx="-45.62495"
+         id="path5973-0"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="273.07776"
+         cx="-31.264896"
+         id="path5973-1-6"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="273.11804"
+         cx="-17.168392"
+         id="path5973-1-8-1"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="287.39224"
+         cx="-45.579372"
+         id="path5973-1-7"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="287.3353"
+         cx="-17.225328"
+         id="path5973-1-9"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.85394"
+         cx="-45.636307"
+         id="path5973-1-2-5"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.83035"
+         cx="-31.328737"
+         id="path5973-1-0"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.80676"
+         cx="-17.248911"
+         id="path5973-1-23-5"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+    </g>
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/rev_chick.svg b/pieces/Dobutsu/rev_chick.svg
new file mode 100644
index 0000000..bbefcfc
--- /dev/null
+++ b/pieces/Dobutsu/rev_chick.svg
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wpi.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="36.115918"
+     inkscape:cx="41.902375"
+     inkscape:zoom="6.5919586"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     transform="matrix(-0.34437547,0,0,-0.34437547,61.909799,30.15043)"
+     id="g5891">
+    <g
+       transform="matrix(3.7795276,0,0,3.7795276,208.50911,-1088.428)"
+       id="layer1"
+       inkscape:label="Layer 1">
+      <g
+         transform="translate(0.39418863,0.66145971)"
+         id="g6119">
+        <g
+           id="g5971"
+           transform="matrix(0.85211113,0,0,0.85211113,-9.1404396,-13.169231)">
+          <rect
+             ry="2.487885"
+             y="332.36246"
+             x="-46.119144"
+             height="40.824493"
+             width="40.824493"
+             id="rect5497-3"
+             style="opacity:1;fill:#edf0a1;fill-opacity:1;stroke:#000000;stroke-width:0.651029;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <g
+             transform="translate(-0.2672693,0.13363444)"
+             id="g5958">
+            <g
+               id="g5904"
+               transform="translate(-0.73499179,1.5368003)"
+               style="stroke:#000000">
+              <g
+                 id="g5898"
+                 style="stroke:#000000" />
+            </g>
+          </g>
+        </g>
+        <circle
+           style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           id="path5973-1"
+           cx="-31.264896"
+           cy="273.07776"
+           r="1.2745767" />
+      </g>
+      <g
+         transform="matrix(0.35009033,-0.05050649,0.05050649,0.35009033,-19.229149,187.55284)"
+         id="g6286">
+        <path
+           inkscape:connector-curvature="0"
+           id="path6261"
+           d="m -75.660215,305.19885 c 0,-2.48371 -0.0798,-2.56409 -1.87012,-1.88342 -2.41462,0.91804 -2.50427,0.90603 -2.50427,-0.33539 0,-0.57552 1.12506,-1.45323 2.50012,-1.95046 2.25596,-0.81576 3.29198,-2.28919 3.32245,-4.72518 0.005,-0.43776 -1.32276,-1.19522 -2.95163,-1.68324 -5.68459,-1.70315 -11.56197,-8.85642 -11.60371,-14.12269 -0.0165,-2.08889 -1.13247,-2.88354 -5.30173,-3.7754 -2.70122,-0.57782 -2.52265,-1.19262 0.91695,-3.15686 2.03265,-1.16077 2.91064,-2.15435 2.91064,-3.29382 0,-4.0488 5.92224,-9.26078 10.52278,-9.26078 5.63193,0 9.16198,3.90372 9.16198,10.1318 v 2.88799 l 3.82759,0.77982 c 4.43634,0.90384 15.964605,2.9357 16.76851,2.95545 2.09006,0.0514 -1.121794,6.10417 -5.931245,11.17759 -3.706562,3.90999 -6.560135,5.50339 -11.486195,6.41376 -2.34974,0.43425 -4.51329,1.16796 -4.80789,1.63046 -1.1685,1.83447 -0.49993,4.06374 1.6854,5.61984 1.60256,1.14112 2.01907,1.82171 1.45813,2.38265 -0.56094,0.56094 -1.03514,0.53533 -1.66011,-0.0896 -1.35319,-1.35319 -2.64506,-1.03783 -2.90201,0.70841 -0.40945,2.78264 -2.05564,2.45359 -2.05564,-0.41089 z m 12.81142,-13.42174 c 3.779908,-1.94721 10.518678,-9.37439 10.518678,-11.59322 0,-0.60586 -16.293918,-3.90531 -19.137968,-3.87535 -1.06346,0.0112 -1.27586,-0.66016 -1.27586,-4.03263 0,-4.30418 -1.37381,-7.87991 -3.31346,-8.62423 -3.26899,-1.25443 -6.32779,-0.46103 -9.16642,2.37761 -1.55677,1.55677 -2.83049,3.481 -2.83049,4.27607 0,0.79507 -0.65268,2.27533 -1.4504,3.28947 l -1.45041,1.84389 1.783,0.4475 c 1.3768,0.34555 1.88174,1.06902 2.21638,3.17562 1.02892,6.4771 5.05524,11.66104 10.74063,13.82872 3.2351,1.23345 9.88605,0.67941 13.36632,-1.11345 z m -21.49557,-20.11619 c -1.06128,-1.27877 0.10307,-4.08773 1.69442,-4.08773 1.69444,0 2.9949,2.15128 2.13039,3.52417 -1.02722,1.63128 -2.73041,1.88224 -3.82481,0.56356 z"
+           style="opacity:1;fill:#000000;stroke-width:0.729065" />
+        <path
+           style="opacity:1;fill:#fede00;fill-opacity:1;stroke:none;stroke-width:0.0992535;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           d="m -72.758456,293.6586 c -1.882216,-0.11998 -2.936236,-0.3775 -4.486955,-1.09624 -1.915997,-0.88803 -3.377483,-1.92599 -4.910434,-3.4874 -1.364201,-1.38958 -2.197702,-2.54467 -3.080824,-4.26959 -0.885609,-1.72981 -1.346113,-3.08689 -1.81202,-5.33995 -0.588442,-2.84554 -0.880885,-3.20656 -3.001908,-3.70565 -0.506169,-0.11911 -1.43245,-0.2452 -1.472363,-0.28019 -0.03995,-0.0349 0.800051,-0.55434 1.280511,-1.1541 1.096659,-1.36903 1.782525,-2.67779 1.979012,-3.77641 0.07611,-0.42501 0.245588,-0.98016 0.376832,-1.23368 0.44715,-0.86372 1.46694,-2.16712 2.485991,-3.17737 2.489186,-2.46767 5.059796,-3.41614 7.849016,-2.89602 1.608531,0.29995 2.318878,0.69675 3.049811,1.70367 0.571542,0.78732 1.060961,1.91892 1.361115,3.14695 0.352172,1.44093 0.433935,2.16945 0.529032,4.71386 0.04794,1.2832 0.138116,2.51597 0.200362,2.73947 0.166333,0.59726 0.552874,0.87923 1.20528,0.87923 2.473345,0 16.297805,2.64486 19.009187,3.63679 0.587231,0.21484 0.602082,0.27123 0.276396,1.04969 -1.083116,2.58884 -5.958295,7.83649 -9.434093,10.15484 -2.530966,1.68816 -7.172599,2.66179 -11.403948,2.3921 z m -9.351663,-21.1933 c 0.585216,-0.17084 1.391569,-0.88338 1.677789,-1.48258 0.349336,-0.73133 0.138006,-1.84008 -0.487701,-2.55865 -0.635926,-0.73033 -1.83698,-1.0713 -2.530947,-0.71852 -0.390337,0.19841 -0.964046,0.85791 -1.192525,1.37082 -0.248964,0.5589 -0.33732,1.58266 -0.177639,2.05828 0.210201,0.62608 0.820486,1.1739 1.551159,1.39239 0.377572,0.1129 0.602665,0.10091 1.159864,-0.0618 z"
+           id="path6272"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="sscsssscccssccccsccsssscccscs" />
+        <path
+           style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.107755;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           d="m -80.257078,270.14973 c 1.63e-4,1.36347 -0.943221,2.16412 -2.362351,2.44503 -0.924307,0.18296 -2.337613,-0.68871 -2.315104,-2.27966 9.35e-4,-1.36481 1.186961,-2.92759 2.244233,-2.75215 1.338711,0.22215 2.563523,1.22335 2.433222,2.58678 z"
+           id="path6280"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="cscsc" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/rev_elephant.svg b/pieces/Dobutsu/rev_elephant.svg
new file mode 100644
index 0000000..c8b0b3c
--- /dev/null
+++ b/pieces/Dobutsu/rev_elephant.svg
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wei.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="29.422532"
+     inkscape:cx="43.514924"
+     inkscape:zoom="2.3306093"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(-1.3015765,0,0,-1.3015765,-9.895621,404.97829)"
+     style="display:inline">
+    <rect
+       style="opacity:1;fill:#d7b5d6;fill-opacity:1;stroke:#000000;stroke-width:0.554749;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="rect5497-7"
+       width="34.787006"
+       height="34.787006"
+       x="-48.044888"
+       y="270.70197"
+       ry="2.1199546" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="0.17839615"
+       inkscape:transform-center-y="0.22740032"
+       transform="rotate(-43.147535,-16.240471,257.24518)" />
+    <g
+       id="g11547"
+       transform="matrix(0.78674347,0,0,0.78674347,32.599138,66.939052)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path11355"
+         d="m -94.896099,289.4836 c -0.832835,-2.74859 -1.581643,-5.86315 -1.664014,-6.92125 -0.467012,-5.99924 4.023942,-12.41806 9.893478,-14.14054 7.15559,-2.09988 15.648661,0.46744 20.120448,6.08209 2.889226,3.62763 3.109811,8.68772 0.680167,15.60262 l -1.188679,3.38305 -1.845166,-0.31004 c -1.221219,-0.20521 -1.926269,-0.49853 -2.085001,-0.86749 -0.179337,-0.41686 -0.280337,-0.31153 -0.400409,0.41755 -0.19722,1.19774 -1.560314,1.61598 -4.791295,1.47017 -2.10144,-0.0949 -2.185115,-0.13072 -2.221905,-0.95248 -0.128739,-2.87575 -0.112166,-2.83031 -1.089881,-2.98827 -0.523142,-0.0842 -2.377098,-0.10864 -4.12,-0.0536 l -3.168914,0.10008 0.02709,1.27786 c 0.0149,0.70281 0.05363,1.62118 0.08607,2.0408 0.05432,0.70267 -0.19501,0.76884 -3.15584,0.83752 -1.768141,0.041 -3.292906,0.0622 -3.388356,0.047 -0.0954,-0.0152 -0.854944,-2.27645 -1.68779,-5.02505 z m 6.999492,1.84208 c 0.188116,-1.89539 0.240411,-1.98878 1.158991,-2.06995 2.213783,-0.19565 8.285389,-0.13053 8.80068,0.0944 0.382707,0.16704 0.545927,0.79617 0.520421,2.00592 l -0.03716,1.76255 1.657803,0.0199 c 3.33991,0.0401 3.125836,0.31765 3.760825,-4.87578 0.480579,-3.93053 0.635801,-4.57323 1.087192,-4.50142 0.753394,0.11985 0.989944,1.72234 0.69791,4.72798 -0.271477,2.79408 0.07535,3.61094 1.638749,3.85965 0.5682,0.0904 0.86954,-0.1254 1.122462,-0.80378 2.909844,-7.80472 2.958863,-13.06686 0.15323,-16.44895 -4.716874,-5.68602 -14.351731,-8.01527 -20.692941,-5.00258 -2.493104,1.18447 -4.712951,3.36971 -6.035189,5.94111 -1.960575,3.8128 -1.917938,6.53884 0.205994,13.17068 l 1.347994,4.20902 2.208014,-0.0519 2.20802,-0.0519 z m 4.058543,-9.04758 c -0.534212,-0.34694 -1.2957,-1.11772 -1.692197,-1.71284 -0.632674,-0.94961 -0.671074,-1.39528 -0.31376,-3.6414 0.435869,-2.73994 1.052415,-3.6787 3.133991,-4.77185 2.097335,-1.10142 6.211459,-0.0959 7.525233,1.83922 0.827018,1.21816 0.257296,1.9713 -0.62848,0.83082 -0.844153,-1.08689 -2.192374,-1.82945 -3.763758,-2.07298 -1.699947,-0.26334 -2.930766,0.16919 -4.126097,1.45036 -1.205636,1.29221 -1.780878,4.15358 -1.111364,5.52815 0.984525,2.02129 4.340136,3.04014 7.703987,2.33911 2.31126,-0.48166 2.135829,-0.49043 2.041585,0.102 -0.04197,0.26384 -0.903023,0.68947 -1.913452,0.94586 -2.119116,0.53771 -5.346033,0.14401 -6.855688,-0.83645 z m 11.554976,-4.06154 c -0.320041,-0.92338 0.121111,-1.70146 1.043501,-1.84046 1.367284,-0.20606 2.055279,1.59204 0.923372,2.41324 -0.726824,0.52732 -1.681992,0.24916 -1.966873,-0.57278 z"
+         style="fill:#000000;stroke-width:0.352778" />
+    </g>
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-0"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="-0.17839695"
+       inkscape:transform-center-y="0.22739943"
+       transform="matrix(-0.72959515,-0.68387931,-0.68387931,0.72959515,118.7706,58.453823)" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-0-6"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="-0.17839359"
+       inkscape:transform-center-y="-0.22740736"
+       transform="rotate(136.85247,-43.022022,282.2147)" />
+    <path
+       sodipodi:type="star"
+       style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path10754-6-3"
+       sodipodi:sides="3"
+       sodipodi:cx="-28.324093"
+       sodipodi:cy="268.94327"
+       sodipodi:r1="1.5686662"
+       sodipodi:r2="0.78433317"
+       sodipodi:arg1="2.0943951"
+       sodipodi:arg2="3.1415927"
+       inkscape:flatsided="true"
+       inkscape:rounded="0"
+       inkscape:randomized="0"
+       d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+       inkscape:transform-center-x="0.17839801"
+       inkscape:transform-center-y="-0.22739811"
+       transform="matrix(0.72959515,0.68387931,0.68387931,-0.72959515,-180.21624,517.53904)" />
+    <path
+       style="opacity:1;fill:#ececec;fill-opacity:1;stroke:none;stroke-width:0.119309;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m 74.139644,41.577099 c -7.87925,0.0344 -15.910281,1.840605 -22.559814,6.199482 -6.309211,4.167165 -11.500302,10.062477 -14.697207,16.9265 -2.213273,4.568929 -3.674258,9.602623 -3.477825,14.717746 -0.07625,5.230598 1.176924,10.349521 2.482485,15.376736 1.790146,6.470337 3.99909,12.815717 5.997555,19.223117 0.164345,0.47836 0.222918,1.27816 0.931692,1.0867 4.214874,-0.0791 8.437214,-0.13905 12.645897,-0.34626 0.379412,-0.52598 0.19193,-1.32307 0.356599,-1.94493 0.373122,-2.83963 0.437493,-5.74822 1.090315,-8.53244 0.175564,-0.89763 1.04368,-1.34847 1.888439,-1.39378 4.151815,-0.47235 8.389359,-0.34111 12.584414,-0.42264 4.561604,0.033 9.14162,-0.0526 13.675284,0.47728 1.274717,0.10982 1.760504,1.62543 1.86344,2.7231 0.174097,2.83818 -0.02633,5.73095 0.127167,8.55924 1.157999,0.20468 2.467207,0.0243 3.681637,0.0845 2.660029,-0.0672 5.346743,0.15349 7.988739,-0.22064 1.564689,-0.27059 2.585689,-1.67616 2.947099,-3.13749 0.94131,-3.49225 1.14155,-7.13177 1.6611,-10.70037 0.57517,-4.113742 0.90181,-8.276925 1.89945,-12.311257 0.19403,-0.568009 0.45502,-1.535725 1.2066,-1.489997 1.3768,0.342131 1.69685,1.93116 1.98035,3.10543 0.51257,3.057311 0.2501,6.192576 0.10488,9.273317 -0.17813,3.215647 -0.79431,6.554127 0.13903,9.711677 0.46431,1.72784 2.05791,2.86873 3.74837,3.22011 1.26528,0.37979 2.95038,0.4484 3.7454,-0.82543 1.06382,-1.83482 1.58733,-3.93086 2.33274,-5.90606 2.97462,-8.920196 5.37535,-18.255223 4.94735,-27.732617 -0.26758,-6.116481 -2.20529,-12.28491 -6.21025,-16.998922 C 110.48227,52.280052 101.01466,46.886586 91.034372,43.980465 85.555277,42.383517 79.852082,41.511598 74.139644,41.577099 Z m 3.95084,9.000124 c 4.704084,0.128216 9.515821,1.45646 13.302645,4.33754 1.629986,1.331346 3.221186,3.08382 3.490155,5.251757 0.119765,0.634319 -0.532616,1.383296 -1.155657,0.88561 -1.587129,-1.11051 -2.576883,-2.876888 -4.173842,-3.991079 -3.464629,-2.712391 -7.965229,-4.04112 -12.345211,-3.83563 -5.020062,0.273894 -9.368527,3.986053 -11.243999,8.524559 -1.447065,3.507684 -2.18364,7.46856 -1.36663,11.221506 0.825487,3.156378 3.58606,5.377708 6.380729,6.795161 4.668377,2.271569 10.057335,2.587368 15.140881,1.957959 2.582029,-0.345524 5.111886,-1.067583 7.686496,-1.366872 0.457329,0.0249 0.440173,0.409267 0.328497,0.724073 -0.0909,0.92294 -1.169542,1.300315 -1.849152,1.731022 -2.933947,1.441111 -6.220674,2.085822 -9.506865,1.993296 -2.265629,0.06433 -4.539422,0.04373 -6.766801,-0.428497 -3.633383,-0.624177 -7.402018,-1.761493 -10.016357,-4.511249 -1.666534,-1.717506 -3.356136,-3.580704 -4.079167,-5.911375 -0.56297,-2.800071 0.125487,-5.662817 0.516667,-8.462987 0.628315,-3.647725 1.444049,-7.593098 4.247771,-10.215463 2.092951,-1.902154 4.583972,-3.449702 7.304545,-4.260639 1.342727,-0.32288 2.722961,-0.437975 4.105295,-0.438692 z m 28.106706,13.924125 c 2.09728,-0.198003 3.79837,1.910793 3.63936,3.912861 0.005,2.012253 -1.92467,3.776325 -3.91755,3.668215 -2.0441,0.09828 -3.62553,-1.911925 -3.49163,-3.86699 -0.23314,-1.524587 0.85037,-3.002766 2.28789,-3.497801 0.46352,-0.214569 0.98179,-0.216482 1.48193,-0.216285 z"
+       id="path12210"
+       inkscape:connector-curvature="0"
+       transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)" />
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/rev_giraffe.svg b/pieces/Dobutsu/rev_giraffe.svg
new file mode 100644
index 0000000..f6eb024
--- /dev/null
+++ b/pieces/Dobutsu/rev_giraffe.svg
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="60"
+   height="60"
+   version="1.1"
+   id="svg4905"
+   sodipodi:docname="wgi.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="960"
+     inkscape:window-height="1060"
+     id="namedview4907"
+     showgrid="false"
+     inkscape:zoom="6.1989695"
+     inkscape:cx="10.721179"
+     inkscape:cy="23.933131"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4905"
+     inkscape:document-rotation="0" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       in="SourceAlpha"
+       result="blur"
+       stdDeviation="2"
+       id="feGaussianBlur4880" />
+    <feOffset
+       result="offsetBlur"
+       dx="2"
+       dy="2"
+       id="feOffset4882" />
+    <feBlend
+       in="SourceGraphic"
+       in2="offsetBlur"
+       mode="normal"
+       id="feBlend4884" />
+  </filter>
+  <g
+     id="g6562"
+     transform="matrix(-0.95833332,0,0,-0.95833332,54.991397,58.75)">
+    <g
+       transform="matrix(1.0779714,0,0,1.0779714,-2.6340321,-1.1975641)"
+       id="g5697">
+      <rect
+         style="display:inline;opacity:1;fill:#d7b5d6;fill-opacity:1;stroke:#000000;stroke-width:0.698944;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="rect5497"
+         width="43.82914"
+         height="43.82914"
+         x="4.7211394"
+         y="7.0264244"
+         ry="2.6709912" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.314982;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754"
+         sodipodi:sides="3"
+         sodipodi:cx="43.863743"
+         sodipodi:cy="28.832012"
+         sodipodi:r1="1.9764072"
+         sodipodi:r2="0.9882037"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m 42.875539,30.543631 0,-3.423238 2.964611,1.711619 z"
+         inkscape:transform-center-x="-0.13073194"
+         inkscape:transform-center-y="6.3154318e-06" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.314982;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-3"
+         sodipodi:sides="3"
+         sodipodi:cx="-8.4619169"
+         sodipodi:cy="28.832012"
+         sodipodi:r1="1.9764072"
+         sodipodi:r2="0.9882037"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -9.4501205,30.543631 0,-3.423238 2.9646107,1.711619 z"
+         inkscape:transform-center-x="0.13073078"
+         inkscape:transform-center-y="6.3154318e-06"
+         transform="scale(-1,1)" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-6"
+         sodipodi:sides="3"
+         sodipodi:cx="-28.324093"
+         sodipodi:cy="268.94327"
+         sodipodi:r1="1.5686662"
+         sodipodi:r2="0.78433317"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -29.108426,270.30177 0,-2.71701 2.352999,1.35851 z"
+         inkscape:transform-center-x="-2.0711132e-05"
+         inkscape:transform-center-y="-0.13072954"
+         transform="matrix(0,-1.2599285,1.2599285,0,-312.62562,-24.814417)" />
+      <path
+         sodipodi:type="star"
+         style="display:inline;opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.25;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="path10754-1"
+         sodipodi:sides="3"
+         sodipodi:cx="-31.130423"
+         sodipodi:cy="307.49689"
+         sodipodi:r1="1.5686662"
+         sodipodi:r2="0.78433317"
+         sodipodi:arg1="2.0943951"
+         sodipodi:arg2="3.1415927"
+         inkscape:flatsided="true"
+         inkscape:rounded="0"
+         inkscape:randomized="0"
+         d="m -31.914756,308.85539 0,-2.71701 2.353,1.35851 z"
+         inkscape:transform-center-x="1.5695237e-05"
+         inkscape:transform-center-y="0.13072936"
+         transform="matrix(0,1.2599285,-1.2599285,0,413.64771,86.363342)" />
+      <g
+         style="display:inline"
+         id="g2811"
+         transform="matrix(0.18596187,0,0,0.18596187,76.914042,-20.843006)">
+        <path
+           inkscape:connector-curvature="0"
+           id="path2787"
+           d="m -312.65013,373.40136 c -1.85093,-0.1579 -2.91447,-0.42442 -3.32156,-0.83239 -0.50864,-0.50975 -0.66598,-2.02398 -1.04469,-10.05417 -1.58267,-33.55938 -3.31088,-95.28498 -3.31521,-118.40819 -0.002,-12.81813 0.0951,-15.58274 0.74052,-20.99028 0.81944,-6.86597 0.82798,-15.76085 0.0166,-17.2521 -0.63664,-1.17003 -1.87618,-0.99627 -4.57154,0.64086 -3.90181,2.36992 -8.00372,4.22203 -9.82126,4.43454 -2.15904,0.25243 -3.50338,-0.65506 -3.87067,-2.61286 -0.9049,-4.82351 3.38328,-21.29494 7.14443,-27.44269 0.70513,-1.15255 1.80887,-2.96867 2.45276,-4.03582 1.63352,-2.70733 2.74987,-7.77018 2.47391,-11.21963 -0.19215,-2.40177 -0.14758,-2.63512 0.65688,-3.43958 1.09977,-1.09977 1.97452,-1.08757 4.07727,0.0569 1.59784,0.86965 1.74981,1.08361 2.78177,3.91643 0.60038,1.64809 1.0916,3.31836 1.0916,3.71171 0,0.68408 0.14191,0.70479 3.26319,0.47609 4.50202,-0.32987 5.55625,-0.54682 5.55625,-1.14341 0,-1.43638 4.16187,-7.05525 5.46873,-7.38325 2.08607,-0.52357 3.72085,2.1852 3.98227,6.59846 l 0.15912,2.68622 4.91114,0.17662 c 4.51602,0.16241 4.9809,0.24216 5.77827,0.99125 0.49302,0.46317 0.8638,1.20665 0.85939,1.72328 -0.02,2.33989 -3.02181,6.55139 -6.05387,8.49336 l -1.92782,1.23472 -0.0213,4.05695 c -0.0154,2.92974 -0.34062,5.87027 -1.17049,10.58333 -1.13393,6.43989 -1.14843,6.66199 -1.09446,16.75695 0.0668,12.49427 0.86399,33.04778 2.03752,52.53156 l 0.23178,3.84822 12.91084,-0.32044 c 16.92717,-0.42014 27.99848,-0.35267 32.13723,0.19583 10.71002,1.41938 17.71182,5.09252 21.86824,11.47205 0.82389,1.26454 1.57955,2.29917 1.67925,2.29917 0.0997,0 1.46272,-0.67469 3.02891,-1.49931 3.76743,-1.98359 7.13819,-5.21592 7.46828,-7.16156 0.50888,-2.99948 1.02895,-3.68635 2.79113,-3.68635 1.52437,0 2.32253,0.92752 2.32253,2.69896 0,4.67767 -3.81137,9.15295 -10.85336,12.74395 -3.23267,1.64847 -3.26567,1.67845 -2.99861,2.72514 0.36046,1.41281 0.17194,16.72296 -0.26064,21.16667 -0.18888,1.94028 -0.51211,6.22653 -0.71828,9.525 -0.94344,15.09368 -2.75484,36.44507 -3.73908,44.07347 -0.92172,7.14377 -1.37218,7.42945 -12.36259,7.84003 -6.80759,0.25431 -7.88037,0.0429 -8.60018,-1.69487 -0.5464,-1.31914 -0.24558,-15.84814 0.51981,-25.1057 0.28577,-3.45636 0.63065,-8.95472 0.76641,-12.21857 l 0.24684,-5.93427 -2.07864,-0.2311 c -5.6162,-0.6244 -13.66945,-0.95825 -27.68744,-1.1478 l -15.37826,-0.20794 -0.23619,2.78143 c -0.12991,1.52979 -0.13736,6.43268 -0.0165,10.89532 0.25003,9.23665 -0.36761,30.19407 -0.92029,31.22675 -0.20118,0.37591 -0.66492,0.84357 -1.03054,1.03924 -0.81597,0.43669 -12.58976,0.74084 -16.32929,0.42183 z m 74.48702,-4.98909 c 1.63495,-0.18796 3.08352,-0.52117 3.21906,-0.74047 0.57509,-0.93053 2.70657,-24.25893 3.84912,-42.12771 0.21091,-3.29847 0.4466,-6.49853 0.52374,-7.11125 0.13832,-1.09859 0.1248,-1.10959 -0.97648,-0.79375 -2.14933,0.61642 -5.57393,0.37673 -7.4714,-0.52294 -2.08205,-0.98718 -4.15786,-3.04462 -4.92166,-4.87809 -0.74078,-1.7782 -0.68618,-5.04937 0.11368,-6.81013 1.50854,-3.32083 5.22075,-5.56732 9.18634,-5.55925 1.2793,0.002 2.89697,0.24646 3.59781,0.54235 l 1.27339,0.53762 0.19928,-3.34015 c 0.1096,-1.83709 0.10836,-3.90038 -0.003,-4.5851 -0.22793,-1.40454 0.0427,-1.38716 -4.85998,-0.31218 -5.13257,1.12538 -7.70231,0.50609 -7.70231,-1.85621 0,-1.75748 0.87077,-2.40774 3.582,-2.67489 2.46925,-0.24332 6.30242,-1.03665 6.64002,-1.37426 0.34074,-0.34074 -1.92227,-3.3053 -3.66154,-4.79665 -3.35344,-2.87545 -8.62518,-4.8202 -15.26234,-5.63028 -4.66409,-0.56926 -14.81667,-0.90787 -14.81667,-0.49415 0,0.19418 0.61545,0.76034 1.36767,1.25813 1.62689,1.07663 2.66143,2.64494 3.22589,4.89027 1.26309,5.02435 -3.50701,9.89903 -9.68667,9.89903 -5.5211,0 -9.70806,-3.6093 -9.70806,-8.36867 0,-2.33696 0.94374,-4.3049 2.88257,-6.01088 l 1.58819,-1.39746 -10.9581,0.1074 -10.95811,0.1074 -1.25099,1.34381 c -1.60924,1.72863 -3.16654,2.51035 -5.47578,2.74869 -2.50441,0.25848 -5.17435,-0.65365 -6.8665,-2.34579 -5.40418,-5.40426 1.21785,-13.63501 8.88704,-11.04591 l 2.02792,0.68462 v -1.53043 c 0,-0.84173 -0.31928,-7.68303 -0.70951,-15.20288 -0.39024,-7.51985 -0.83405,-18.38929 -0.98626,-24.15432 -0.1522,-5.76502 -0.36378,-10.5689 -0.47017,-10.67529 -0.10639,-0.10639 -0.87316,0.0464 -1.70394,0.33949 -0.83077,0.29312 -1.94682,0.53294 -2.48012,0.53294 -1.65051,0 -3.99757,-1.03063 -5.3475,-2.34818 l -1.29055,-1.25959 -0.23147,2.42124 c -0.12731,1.33169 -0.53875,5.13792 -0.91431,8.45828 -0.37556,3.32037 -0.68283,7.48363 -0.68283,9.25169 v 3.21465 l 0.95421,-0.36279 c 0.52482,-0.19954 1.88592,-0.36725 3.02467,-0.3727 5.10003,-0.0244 8.71073,3.81592 7.85857,8.35832 -0.34758,1.8528 -3.0343,4.45718 -5.23321,5.07283 -1.64004,0.45919 -4.63074,0.37047 -6.02734,-0.1788 -0.63663,-0.25038 -0.65011,0.10935 -0.41859,11.17164 0.13157,6.28669 0.38594,16.58973 0.56526,22.89563 0.17932,6.3059 0.328,12.80264 0.3304,14.43719 l 0.004,2.97191 0.79814,-0.74982 c 2.28294,-2.1447 7.14035,-2.9437 10.43442,-1.71637 6.28856,2.34305 8.14263,10.01828 3.54031,14.65571 -3.34439,3.36989 -9.47925,3.85764 -13.40059,1.0654 l -1.05055,-0.74806 0.17135,0.82882 c 0.0942,0.45586 0.26803,4.71821 0.38617,9.47189 0.19469,7.83315 0.84736,25.44802 1.46071,39.42291 l 0.23612,5.37987 h 5.9439 c 3.26915,0 5.97148,-0.0397 6.00518,-0.0882 0.0337,-0.0485 0.15276,-10.04552 0.26458,-22.21558 l 0.20332,-22.1274 0.7933,-0.79292 c 0.71479,-0.71444 1.27342,-0.8184 5.64444,-1.05048 2.66813,-0.14166 6.97,-0.16207 9.5597,-0.0453 l 4.70856,0.21221 -0.79977,-1.55239 c -1.80318,-3.50009 -0.54368,-8.34406 2.783,-10.70321 2.10677,-1.49403 3.39564,-1.90369 6.08751,-1.93485 3.22516,-0.0373 5.41203,0.79084 7.43798,2.81679 2.78918,2.78918 3.43986,6.63757 1.69944,10.05128 -0.37699,0.73946 -0.62213,1.40777 -0.54476,1.48514 0.0774,0.0774 2.33716,0.28129 5.02175,0.45315 10.079,0.64523 11.01501,0.85269 12.15278,2.69365 0.75551,1.22243 0.76929,1.44293 0.54436,8.70881 -0.12692,4.10004 -0.46859,10.23275 -0.75926,13.62823 -0.29068,3.39549 -0.63349,9.46768 -0.76182,13.49375 l -0.23333,7.32014 2.43723,-0.003 c 1.34047,-10e-4 3.77491,-0.15646 5.40985,-0.34442 z m -73.55005,-165.34849 c 2.34246,-1.93086 4.72369,-2.29617 7.66997,-1.17667 1.00916,0.38345 1.85967,0.66126 1.89001,0.61736 0.3683,-0.53286 1.93095,-12.09821 1.92933,-14.27913 l -0.002,-2.91041 h -1.23472 c -1.59788,0 -2.64583,-1.0263 -2.64583,-2.59116 0,-1.5785 0.8489,-2.26776 3.3661,-2.73309 2.43486,-0.45011 4.93076,-1.64312 6.13219,-2.93111 l 0.85525,-0.91687 h -1.92489 c -1.05869,0 -2.79293,0.24412 -3.85386,0.54248 -3.61979,1.018 -4.73548,1.04531 -5.58201,0.13668 -0.41617,-0.44671 -0.75667,-1.10805 -0.75667,-1.46964 0,-0.60884 -0.22816,-0.64042 -3.08681,-0.42728 -1.69774,0.12658 -4.7444,0.43687 -6.77036,0.68953 -3.82982,0.47762 -5.14983,0.30371 -5.54681,-0.7308 -0.14619,-0.38096 -0.64837,0.35484 -1.54706,2.26675 -0.73183,1.55695 -1.79988,3.51496 -2.37344,4.35113 -1.71104,2.49448 -3.13907,5.59931 -4.61479,10.03354 -1.4857,4.46423 -2.87185,10.56427 -2.87185,12.63817 v 1.26016 l 2.02848,-1.02918 c 1.11566,-0.56604 3.13972,-1.68277 4.49791,-2.48161 3.2437,-1.90784 4.57335,-2.25259 6.8252,-1.76966 2.15283,0.46171 4.0366,2.32358 4.64794,4.59393 l 0.39861,1.4803 0.63699,-1.04746 c 0.35035,-0.5761 1.22033,-1.52828 1.93329,-2.11596 z m -10.95718,-15.78632 c -1.46782,-0.63883 -2.25012,-1.83328 -2.25012,-3.43554 0,-2.63394 1.42463,-4.20124 3.8492,-4.23471 0.7349,-0.0101 1.68609,0.16883 2.11375,0.39771 1.00784,0.53938 2.1483,2.4219 2.1483,3.54612 0,1.88554 -2.32007,4.25878 -4.12499,4.21952 -0.34918,-0.008 -1.13045,-0.22949 -1.73614,-0.4931 z"
+           style="fill:#000000;stroke-width:0.352778" />
+        <path
+           sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+           inkscape:connector-curvature="0"
+           id="path2801"
+           transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)"
+           d="m -990.69336,-203.23047 c -0.72413,2.18092 -0.65023,4.67577 -1.01562,6.97266 -1.93273,20.6186 -4.88178,41.15125 -5.92383,61.8457 -0.23039,6.29307 -0.41988,12.64031 -0.35157,18.90625 3.13928,-1.05371 6.3769,-2.16228 9.73633,-2.35156 6.42628,-0.53113 13.06825,-0.36343 19.03321,2.30273 8.62723,3.61737 15.75805,11.84487 16.30078,21.447268 0.3795,4.041166 0.12234,8.324624 -2.05274,11.882813 -2.83441,5.11813 -7.5459,8.968176 -12.35156,12.1875 -6.47515,4.81064 -17.97607,2.176862 -30.11969,1.77566 -1.52317,14.107494 0.51261,28.73156 0.4068,42.894252 1.24784,49.625432 1.43349,102.141435 3.21004,151.749037 0.0823,2.54007 3.42531,-3.90455 4.33683,-4.26466 11.10869,-7.58519 26.23202,-9.56755 38.8711,-4.85157 14.06439,5.30782 24.03343,20.24572 22.08008,35.38477 -1.27083,18.30956 -19.78045,31.47459 -37.40235,30.65234 -9.40147,0.22321 -19.14303,-4.07717 -26.66967,-9.65085 1.6132,27.18186 2.6314,56.31648 3.71069,83.86765 1.4261,41.32999 3.16015,83.07612 4.80969,124.39751 14.87742,0.31066 30.37549,-0.0986 45.24304,-0.43676 0.30753,-55.80769 1.06035,-111.61358 1.57422,-167.41992 2.96487,-4.24355 7.69893,-6.36297 12.81641,-6.32227 21.48631,-1.73532 44.03095,-0.21664 65.55554,-0.0863 -3.13102,-6.67281 -7.13373,-14.93705 -6.02625,-22.72233 1.14529,-15.11029 13.63019,-27.78487 28.36915,-30.37305 11.81642,-1.62252 25.19896,-0.51829 34.23046,8.08398 11.2328,9.05062 15.1926,25.50905 8.93165,38.66323 -0.47321,2.07749 -2.86753,5.04567 -2.41156,7.02372 18.55812,2.99631 38.12219,1.6026 56.55023,5.46149 6.47343,2.06521 10.78238,9.16068 10.61718,15.59961 -0.23089,55.6971 -7.02662,104.15812 -6.84765,153.71875 13.89385,0.0363 28.85623,0.14546 42.31034,-3.43173 4.04169,-22.89354 5.14987,-46.56784 7.49874,-69.67764 3.31107,-39.86433 6.94783,-80.6375 8.90097,-120.58896 -17.12596,1.72351 -29.91805,2.01087 -41.95615,-7.8505 -5.98029,-5.48025 -11.50667,-12.71191 -10.46289,-21.34765 -0.15026,-6.82521 -0.61902,-14.18573 3.75586,-19.96485 7.71659,-12.26984 22.7261,-18.9267 37.00586,-17.10156 4.51627,0.0293 10.00104,2.70204 14.23618,3.81356 1.26505,-8.06843 1.83637,-26.69624 0.30084,-33.282695 -7.56999,-4.370724 -29.88311,6.098215 -42.08194,2.922261 -6.93337,-2.594022 -6.85169,-14.730865 0.73242,-16.353515 10.6579,-3.024928 23.08991,-2.424899 33.41972,-6.694728 -21.69443,-45.280364 -87.37221,-42.327471 -125.90711,-41.919039 -5.65433,-0.395057 0.25067,3.521325 1.46913,4.269818 9.77376,6.649335 16.22057,17.796964 14.36397,29.759964 -3.12905,16.780114 -20.7238,27.921319 -37.22071,27.011719 -16.06949,1.028843 -33.30735,-9.558106 -36.23828,-26.035156 -2.09789,-10.508207 2.20539,-20.742063 10.3874,-27.513871 6.66855,-3.867358 12.07068,-7.043155 -0.86201,-5.78691 -25.25652,0.373292 -50.63308,0.249638 -75.86703,0.726423 -7.67181,10.045393 -20.11056,17.620113 -33.03922,15.470842 -12.71644,-1.463772 -25.60169,-11.404223 -25.8164,-24.996094 -0.36023,-12.988892 10.38533,-24.01921554 22.6875,-26.6367184 8.67936,-2.9020934 17.65989,1.69540393 26.04537,3.6029275 0.66328,-7.6371427 -0.87874,-16.5424901 -0.96139,-24.3587871 -0.33743,-17.637058 -0.70948,-33.185841 -2.15815,-49.177722 -1.71463,-34.360239 -2.83058,-68.745339 -3.94532,-103.128909 -0.34129,-6.02647 -0.28657,-12.11309 -1.15429,-18.08984 -0.78671,-0.7879 -2.17859,0.066 -3.13282,0.13281 -5.47609,1.54506 -11.16025,3.94805 -16.91601,2.42578 -7.34487,-1.44008 -13.72498,-5.87138 -18.8457,-11.1875 -0.58384,-0.38256 -1.04918,-1.24314 -1.75977,-1.34961 z"
+           style="opacity:1;fill:#fee51f;fill-opacity:1;stroke:#000000;stroke-width:0.377953;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccc"
+           transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)"
+           inkscape:connector-curvature="0"
+           id="path2805"
+           d="m -1021.1418,-349.43123 c -2.5149,3.24383 -3.9482,7.19131 -5.9322,10.76386 -3.3983,7.07103 -7.5582,13.71971 -11.6846,20.37954 -8.1977,14.58673 -13.5773,30.5648 -18.0463,46.63045 -2.5018,9.65052 -4.9876,19.36588 -6.1255,29.28794 -0.2304,2.70972 -0.3048,5.48435 -0.1283,8.17057 6.0148,-2.69029 11.8854,-5.97973 17.6982,-9.18021 6.8025,-3.64409 13.289,-8.09696 20.6905,-10.46795 5.5501,-1.45039 11.6856,-0.78169 16.857,1.68323 6.3343,3.1904 11.05127,9.28721 12.78816,16.15228 0.52888,1.41832 0.57416,3.71002 1.40924,4.96703 1.03114,-0.56986 1.49065,-2.64523 2.24122,-3.56327 4.35521,-6.33437 10.49651,-11.82188 17.92133,-14.14904 4.41827,-1.17657 9.19378,-1.20618 13.65728,-0.14107 4.00817,0.9669 7.76655,2.84108 11.78544,3.70767 0.93543,-0.22946 0.64198,-1.68326 0.99141,-2.37198 2.62066,-14.41005 4.4583,-28.95783 6.02347,-43.51699 0.63543,-6.24472 0.45511,-12.52834 0.48185,-18.79433 -1.64946,-0.47193 -3.67212,-0.0292 -5.4397,-0.33399 -4.13679,-0.13045 -8.34258,-3.10335 -8.87834,-7.40069 -0.83536,-3.67416 0.27128,-8.18867 3.94326,-9.89514 5.36335,-2.6521 11.54823,-2.84317 17.08929,-5.06083 6.66822,-2.33168 12.93973,-6.2173 17.35243,-11.79921 0.54795,-1.36058 -2.5541,-0.24437 -3.47228,-0.53811 -7.17681,-0.0293 -14.30014,1.08908 -21.20627,3.00401 -4.8507,1.13232 -9.94445,2.8009 -14.94267,1.60298 -2.84036,-1.23548 -4.87524,-4.07723 -5.6313,-7.02421 0.239,-1.60086 -1.35257,-2.66529 -2.81491,-2.54906 -9.94599,0.20448 -19.83546,1.51103 -29.72257,2.5295 -6.81544,0.64103 -13.66764,1.81197 -20.52414,1.17078 -2.2897,-0.15271 -4.4487,-1.45957 -5.4596,-3.58199 -0.2964,-0.56815 -0.677,0.13363 -0.9214,0.31823 z m 1.7899,17.87398 c 3.8288,0.0605 8.1973,0.60002 10.7877,3.77229 2.6857,2.91607 4.8339,6.61345 5.2534,10.58391 0.099,5.93406 -4.2282,10.95026 -8.8669,14.11402 -2.848,2.06997 -6.5807,2.66444 -9.9062,1.50536 -4.7884,-1.22144 -9.4371,-4.3749 -10.9208,-9.30001 -1.0236,-3.4725 -0.7643,-7.27869 0.038,-10.78121 1.2645,-4.93689 5.5433,-8.88754 10.6133,-9.62292 0.9985,-0.17858 1.9882,-0.26085 3.0015,-0.27144 z"
+           style="opacity:1;fill:#fee51f;fill-opacity:1;stroke:#000000;stroke-width:0.477237;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/rev_hen.svg b/pieces/Dobutsu/rev_hen.svg
new file mode 100644
index 0000000..e172f8b
--- /dev/null
+++ b/pieces/Dobutsu/rev_hen.svg
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="whi.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="61.263113"
+     inkscape:cx="76.812636"
+     inkscape:zoom="3.2959793"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(-1.3015765,0,0,-1.3015765,-9.895621,404.97829)">
+    <rect
+       style="opacity:1;fill:#edf0a1;fill-opacity:1;stroke:#000000;stroke-width:0.554749;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="rect5497-2"
+       width="34.787006"
+       height="34.787006"
+       x="-48.044888"
+       y="270.70197"
+       ry="2.1199546" />
+    <circle
+       style="opacity:1;fill:#0b58ad;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-0"
+       cx="-45.230762"
+       cy="273.83298"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0f57ab;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-6"
+       cx="-30.870708"
+       cy="273.73923"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-8-8"
+       cx="-16.774204"
+       cy="273.77951"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-7"
+       cx="-45.185184"
+       cy="288.05371"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-9"
+       cx="-16.831141"
+       cy="287.99677"
+       r="1.2745767" />
+    <circle
+       style="opacity:1;fill:#0d5aac;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       id="path5973-1-0"
+       cx="-30.934549"
+       cy="302.49182"
+       r="1.2745767" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path16248"
+       d="m -31.09696,299.47227 c 0.0095,-0.52127 0.05802,-0.92293 -0.610947,-0.59557 -0.85443,0.12097 -0.889136,-0.21522 -0.04394,-0.68328 0.93344,-0.51693 0.52879,-0.85019 -1.466111,-1.20745 -2.172542,-0.38907 -4.192654,-1.58878 -5.050403,-2.9994 -0.790183,-1.29947 -1.765666,-4.85974 -1.812239,-6.61418 -0.03751,-1.41302 -0.05424,-1.44006 -0.949045,-1.5346 -0.500963,-0.0529 -0.90076,-0.18737 -0.888426,-0.29876 0.06064,-0.54649 1.342995,-1.84264 1.901485,-1.92171 0.482403,-0.0683 1.065208,-0.79567 2.448718,-3.05617 1.954396,-3.19322 2.867922,-4.06623 3.678698,-3.51552 0.265415,0.18029 0.46239,0.5189 0.437711,0.75248 -0.03154,0.29849 0.101737,0.38169 0.448527,0.28 1.273294,-0.37343 1.425603,-0.32365 1.432469,0.46816 0.006,0.69712 0.09848,0.77933 0.884518,0.78669 0.74543,0.007 0.892853,0.11479 0.977824,0.71489 0.06623,0.46781 -0.113395,0.99447 -0.531534,1.55797 -0.629719,0.84878 -0.630543,0.85657 -0.2787,2.63746 0.869604,4.40167 0.790743,4.35218 7.614537,4.77933 0.859128,0.0538 0.944998,0.13794 1.048722,1.02673 0.405679,3.47681 -2.476426,6.46366 -6.863114,7.11257 -1.040053,0.15384 -1.901664,0.38069 -1.914705,0.50411 -0.01304,0.12343 0.327472,0.39004 0.756694,0.59247 0.878196,0.21503 0.605417,1.04634 -0.07841,0.65159 -0.538509,-0.31086 -0.556179,0.21386 -0.534405,0.51221 0.01781,0.24926 -0.125219,0.49028 -0.317816,0.53561 -0.322434,-0.10319 -0.300026,-0.43899 -0.290175,-0.48563 z m 3.855138,-3.3002 c 1.789437,-0.5862 3.607835,-1.93987 4.21373,-3.13686 0.481217,-1.07046 0.593381,-1.72965 0.548888,-3.21437 l -2.331599,-0.2393 c -4.2878,-0.11664 -5.813264,-1.13043 -6.17034,-4.10067 -0.398195,-3.3123 -1.414362,-4.74162 -3.529954,-4.96514 -1.907317,-0.20152 -4.161011,2.02701 -4.806714,4.75304 -0.538639,2.27398 0.656754,7.22981 2.172678,9.00746 1.846031,2.16474 6.413132,3.03892 9.903338,1.89558 z m -10.328753,-11.9987 c -0.194949,-0.72947 0.243336,-1.26905 0.89727,-1.10464 0.439844,0.11057 0.580945,0.33114 0.528624,0.82634 -0.05232,0.49518 -0.236396,0.68137 -0.689638,0.6976 -0.370854,0.0133 -0.665494,-0.15447 -0.736256,-0.4193 z m -2.26204,0.79557 c 0.119527,-0.23429 0.162966,-0.49309 0.09663,-0.57514 -0.154261,-0.1907 -1.00841,0.3706 -1.04157,0.68445 -0.04434,0.41963 0.720291,0.33116 0.944968,-0.10931 z m 9.376027,-4.4866 c 0.05251,-0.49702 -0.332386,-0.47964 -0.914509,0.0413 -0.390364,0.34933 -0.432657,0.52056 -0.199728,0.80852 0.23294,0.28799 0.378264,0.27412 0.6862,-0.0654 0.21487,-0.23691 0.407487,-0.5899 0.428037,-0.7844 z m -3.665022,-1.55259 c 0.118213,-1.1819 -0.209237,-1.48617 -0.928772,-0.86422 -0.350194,0.30269 -0.789171,0.85243 -0.975474,1.22162 l -0.338739,0.67128 1.087255,-0.17052 c 0.905397,-0.14199 1.09871,-0.28554 1.15573,-0.85816 z m 1.85388,0.59382 c 0.07792,-0.73751 -0.162538,-0.88163 -0.71942,-0.43118 -0.549736,0.44468 -0.494206,0.98599 0.10767,1.04958 0.396278,0.0419 0.559412,-0.12304 0.61175,-0.6184 z"
+       style="fill:#242424;stroke-width:0.271694"
+       sodipodi:nodetypes="scccccsccccsccscccccsscscccsccccscssccscccssccscccscsccccsssss" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0487106;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -39.830057,284.33445 c -0.276783,0.0304 -0.52047,0.20358 -0.728611,0.37929 -0.134606,0.13365 -0.335461,0.34709 -0.197293,0.542 0.175344,0.20382 0.487878,0.10429 0.686156,-0.009 0.269695,-0.16936 0.410184,-0.5177 0.368867,-0.82721 -0.01834,-0.0544 -0.07488,-0.0821 -0.129118,-0.085 z"
+       id="path16437"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -34.447197,277.72847 c -0.419763,0.0618 -0.71458,0.42649 -0.993164,0.71605 -0.369959,0.43048 -0.687414,0.92159 -0.891201,1.44922 0.07046,0.12741 0.26613,0.0135 0.37938,0.0226 0.51652,-0.105 1.074208,-0.11594 1.547292,-0.36848 0.340453,-0.20386 0.275841,-0.64853 0.318688,-0.98481 0.01323,-0.29611 -0.0075,-0.7473 -0.360993,-0.83461 z"
+       id="path16439"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -32.481251,278.81671 c -0.329586,0.0528 -0.580074,0.32865 -0.79179,0.56755 -0.18367,0.23394 -0.125644,0.63657 0.183044,0.72862 0.251854,0.0839 0.620882,0.089 0.73923,-0.20154 0.112891,-0.29634 0.166194,-0.65188 0.05015,-0.95441 -0.03733,-0.0659 -0.10301,-0.12984 -0.180634,-0.14022 z"
+       id="path16441"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#d82a17;fill-opacity:1;stroke:#000000;stroke-width:0.0688872;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m -30.632906,280.10995 c -0.390383,0.0441 -0.706502,0.33919 -0.956638,0.6228 -0.137489,0.13193 -0.200958,0.3555 -0.0539,0.5062 0.10895,0.16841 0.327234,0.4162 0.539306,0.24398 0.308294,-0.25144 0.565873,-0.60239 0.661609,-0.99218 0.02413,-0.14399 -0.0098,-0.36433 -0.190367,-0.3808 z"
+       id="path16443"
+       inkscape:connector-curvature="0" />
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.944882;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+       d="m 62.210574,49.835998 c -7.602046,1.113589 -12.839704,8.152794 -15.526367,14.861328 -2.782026,8.63198 -0.699623,18.001013 1.951172,26.40625 2.141547,6.547016 5.991927,13.082154 12.644531,15.822264 10.003611,4.74327 22.121465,5.06319 32.338587,0.82686 6.810733,-3.16294 14.033553,-8.233637 15.188753,-16.237991 0.10685,-1.965379 1.38602,-4.718681 -0.18554,-6.318359 C 100.73961,83.249937 92.072342,84.616587 84.523074,81.166076 78.029951,78.154331 77.466206,70.171571 76.255496,64.033264 75.140123,58.455107 72.598968,52.001309 66.478152,50.464904 65.103259,50.027013 63.656885,49.770857 62.210574,49.835998 Z m -6.719726,9.563477 c 2.643469,-0.0866 2.498373,3.829211 1.166015,5.207031 -1.860948,0.956683 -5.182134,0.440375 -4.657226,-2.348633 -0.09317,-1.894851 1.727671,-3.085295 3.491211,-2.858398 z"
+       id="path16538"
+       inkscape:connector-curvature="0"
+       transform="matrix(0.26458333,0,0,0.26458333,-51.389145,267.35772)" />
+  </g>
+</svg>
diff --git a/pieces/Dobutsu/rev_lion.svg b/pieces/Dobutsu/rev_lion.svg
new file mode 100644
index 0000000..33c11d3
--- /dev/null
+++ b/pieces/Dobutsu/rev_lion.svg
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
+   sodipodi:docname="wki.svg"
+   id="svg4905"
+   version="1.1"
+   height="60"
+   width="60">
+  <metadata
+     id="metadata4911">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4909" />
+  <sodipodi:namedview
+     inkscape:document-rotation="0"
+     inkscape:current-layer="svg4905"
+     inkscape:window-maximized="0"
+     inkscape:window-y="20"
+     inkscape:window-x="0"
+     inkscape:cy="59.217968"
+     inkscape:cx="48.32528"
+     inkscape:zoom="3.2959793"
+     showgrid="false"
+     id="namedview4907"
+     inkscape:window-height="1060"
+     inkscape:window-width="960"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0"
+     guidetolerance="10"
+     gridtolerance="10"
+     objecttolerance="10"
+     borderopacity="1"
+     bordercolor="#666666"
+     pagecolor="#ffffff" />
+  <!--
+Created by Hari Seldon and modified by orangain, licensed under Creative Commons Attribution-Share Alike 3.0 Unported.
+See: https://creativecommons.org/licenses/by-sa/3.0/deed.en
+
+Original file is available at: https://commons.wikimedia.org/wiki/File:Shogi_osho(svg).svg
+-->
+  <filter
+     id="drop-shadow">
+    <feGaussianBlur
+       id="feGaussianBlur4880"
+       stdDeviation="2"
+       result="blur"
+       in="SourceAlpha" />
+    <feOffset
+       id="feOffset4882"
+       dy="2"
+       dx="2"
+       result="offsetBlur" />
+    <feBlend
+       id="feBlend4884"
+       mode="normal"
+       in2="offsetBlur"
+       in="SourceGraphic" />
+  </filter>
+  <g
+     inkscape:label="Layer 1"
+     id="layer1"
+     transform="matrix(-1.3015766,0,0,-1.3015766,-9.895624,404.97833)">
+    <g
+       id="g6119"
+       transform="translate(0.39418863,0.66145971)">
+      <g
+         transform="matrix(0.85211113,0,0,0.85211113,-9.1404396,-13.169231)"
+         id="g5971">
+        <rect
+           style="opacity:1;fill:#f7c1c1;fill-opacity:1;stroke:#000000;stroke-width:0.651029;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+           id="rect5497-2"
+           width="40.824493"
+           height="40.824493"
+           x="-46.119144"
+           y="332.36246"
+           ry="2.487885" />
+        <g
+           id="g5958"
+           transform="translate(-0.2672693,0.13363444)">
+          <path
+             sodipodi:nodetypes="ccsccccccssscsscsscsccccsccscccccc"
+             style="opacity:1;fill:#e4ac19;stroke:#000000;stroke-width:0.819396"
+             d="m -37.301682,366.13828 c 0.002,-0.11924 0.522273,-1.64077 1.156259,-3.3812 0.633988,-1.74043 1.152704,-3.2662 1.152704,-3.39063 0,-0.32365 -2.195217,-1.40951 -3.974473,-1.96596 l -1.517424,-0.47457 1.566993,-1.35283 c 1.954106,-1.68705 3.211789,-4.05509 2.964154,-5.58109 -0.183744,-1.13229 -1.423763,-3.2143 -3.267038,-5.48538 -0.88153,-1.08614 -1.070029,-1.47555 -0.722662,-1.49293 1.098081,-0.055 4.031467,0.58601 5.173172,1.13032 1.995908,0.95155 2.170218,0.71969 1.992058,-2.64997 -0.08402,-1.58904 -0.0512,-2.88917 0.0729,-2.88917 0.273899,0 4.946292,2.91618 5.998452,3.74381 0.414055,0.32569 0.941179,0.59217 1.171389,0.59217 0.23021,0 1.846068,-1.11994 3.590793,-2.48876 1.744725,-1.36882 3.25661,-2.40439 3.359742,-2.30126 0.103132,0.10314 0.258321,1.34246 0.344862,2.75405 0.08655,1.41159 0.254071,2.6631 0.372286,2.78116 0.118214,0.11804 0.718661,0.0636 1.334328,-0.12074 0.615665,-0.18447 2.200527,-0.33539 3.521914,-0.33539 h 2.402519 l -2.363791,2.42173 c -1.300086,1.33196 -2.36252,2.66527 -2.360967,2.96292 0.0045,0.86651 1.300484,3.41307 2.639495,5.18657 1.235324,1.63617 1.591495,2.4367 1.084151,2.4367 -0.42153,0 -5.13769,2.31254 -5.320982,2.6091 -0.09015,0.14587 0.02946,0.56156 0.265771,0.92377 0.398788,0.61121 2.195934,5.41847 2.195934,5.87398 0,0.11169 -0.162598,0.0745 -0.36133,-0.083 -0.682067,-0.53989 -3.542695,-1.44967 -5.682151,-1.80711 -2.96605,-0.49556 -8.442689,-0.42722 -10.530672,0.13138 -0.928028,0.24827 -2.669895,0.90539 -3.870816,1.46024 -2.390315,1.1044 -2.392717,1.1052 -2.387583,0.79204 z"
+             id="path5748"
+             inkscape:connector-curvature="0" />
+          <ellipse
+             ry="7.7261157"
+             rx="7.3980627"
+             transform="scale(1,-1)"
+             cy="-353.74197"
+             cx="-25.470678"
+             id="path5766"
+             style="opacity:1;fill:#fce377;fill-opacity:1;stroke:#000000;stroke-width:0.575783;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <circle
+             r="0.90203464"
+             cy="352.09091"
+             cx="-27.963072"
+             id="path5835"
+             style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.58824;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+          <g
+             style="stroke:#000000"
+             transform="translate(-0.73499179,1.5368003)"
+             id="g5904">
+            <path
+               style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+               d="m -24.989701,353.86158 c 0.639558,1.14696 1.410085,1.83553 2.405426,1.73725"
+               id="path5881"
+               inkscape:connector-curvature="0"
+               sodipodi:nodetypes="cc" />
+            <g
+               style="stroke:#000000"
+               id="g5898">
+              <path
+                 sodipodi:nodetypes="cc"
+                 inkscape:connector-curvature="0"
+                 id="path5879"
+                 d="m -27.127856,355.19792 c 2.20977,-0.3229 2.294654,-1.1816 2.228597,-2.43715"
+                 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+              <path
+                 sodipodi:nodetypes="cc"
+                 inkscape:connector-curvature="0"
+                 id="path5883"
+                 d="m -26.552828,355.01933 c 0.905568,0.93577 2.165489,1.15095 3.071057,0.44884"
+                 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+            </g>
+          </g>
+          <circle
+             r="0.90203464"
+             cy="352.12433"
+             cx="-23.252451"
+             id="path5835-6"
+             style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.58824;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+        </g>
+      </g>
+      <circle
+         r="1.2745767"
+         cy="273.17151"
+         cx="-45.62495"
+         id="path5973-0"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="273.07776"
+         cx="-31.264896"
+         id="path5973-1-6"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="273.11804"
+         cx="-17.168392"
+         id="path5973-1-8-1"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="287.39224"
+         cx="-45.579372"
+         id="path5973-1-7"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="287.3353"
+         cx="-17.225328"
+         id="path5973-1-9"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.85394"
+         cx="-45.636307"
+         id="path5973-1-2-5"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.83035"
+         cx="-31.328737"
+         id="path5973-1-0"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+      <circle
+         r="1.2745767"
+         cy="301.80676"
+         cx="-17.248911"
+         id="path5973-1-23-5"
+         style="opacity:1;fill:#e95c35;fill-opacity:1;stroke:#000000;stroke-width:0.340634;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" />
+    </g>
+  </g>
+</svg>
diff --git a/utils/array.js b/utils/array.js
index c2cb25f..af8462e 100644
--- a/utils/array.js
+++ b/utils/array.js
@@ -5,8 +5,12 @@ export const ArrayFun = {
     return [...Array(size1)].map(() => Array(size2).fill(initElem));
   },
 
-  range: function(max) {
-    return [...Array(max).keys()];
+  range: function(min, max) {
+    if (!max) {
+      max = min;
+      min = 0;
+    }
+    return [...Array(max - min).keys()].map(k => k + min);
   },
 
   toObject: function(keys, values) {
diff --git a/variants.js b/variants.js
index cad081a..f42033e 100644
--- a/variants.js
+++ b/variants.js
@@ -43,8 +43,8 @@ const variants = [
   {name: 'Dark', desc: 'In the shadow'},
   {name: 'Diamond', desc: 'Rotating board'},
   {name: 'Dice', desc: 'Roll the dice'},
-//  {name: 'Discoduel', desc: 'Enter the disco', disp: 'Disco Duel'},
-//  {name: 'Dobutsu', desc: "Let's catch the Lion!"},
+  {name: 'Discoduel', desc: 'Enter the disco', disp: 'Disco Duel'},
+  {name: 'Dobutsu', desc: "Let's catch the Lion!"},
 //  {name: 'Doublearmy', desc: '64 pieces on the board', disp: 'Double Army'},
   {name: 'Doublemove', desc: 'Double moves'},
 //  {name: 'Dynamo', desc: 'Push and pull'},
diff --git a/variants/Avalanche/class.js b/variants/Avalanche/class.js
index ee80550..8a4981f 100644
--- a/variants/Avalanche/class.js
+++ b/variants/Avalanche/class.js
@@ -167,7 +167,7 @@ export default class AvalancheRules extends ChessRules {
     }
   }
 
-  atLeastOneMove(color, lastMove) {
+  atLeastOneMove(color) {
     if (this.subTurn == 0)
       return true;
     return super.atLeastOneMove(color);
diff --git a/variants/Chaining/class.js b/variants/Chaining/class.js
index 9d771da..4ae7208 100644
--- a/variants/Chaining/class.js
+++ b/variants/Chaining/class.js
@@ -20,8 +20,8 @@ export default class ChainingRules extends ChessRules {
     return true; //self captures induce chaining
   }
 
-  setOtherVariables(fenParsed, pieceArray) {
-    super.setOtherVariables(fenParsed, pieceArray);
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
     // Stack of "last move" only for intermediate chaining
     this.lastMoveEnd = [];
   }
diff --git a/variants/Clorange/class.js b/variants/Clorange/class.js
index 485bbd6..e8506db 100644
--- a/variants/Clorange/class.js
+++ b/variants/Clorange/class.js
@@ -14,14 +14,6 @@ export default class ClorangeRules extends ChessRules {
     return true;
   }
 
-  getReserveFen(o) {
-    if (o.init)
-      return "00000000000000000000";
-    return (
-      ["w","b"].map(c => Object.values(this.reserve[c]).join("")).join("")
-    );
-  }
-
   pieces(color, x, y) {
     let res = super.pieces(color, x, y);
     res['s'] = {"class": "nv-pawn", moveas: "p"};
@@ -38,9 +30,8 @@ export default class ClorangeRules extends ChessRules {
   static get NV_PIECES() {
     return ['s', 'u', 'o', 'c', 't'];
   }
-
-  setOtherVariables(fen) {
-    super.setOtherVariables(fen, V.V_PIECES.concat(V.NV_PIECES));
+  static get ReserveArray() {
+    return V.V_PIECES.concat(V.NV_PIECES);
   }
 
   // Forbid non-violent pieces to capture
diff --git a/variants/Convert/class.js b/variants/Convert/class.js
index bc99c63..cd2d1a9 100644
--- a/variants/Convert/class.js
+++ b/variants/Convert/class.js
@@ -15,8 +15,8 @@ export default class ConvertRules extends ChessRules {
     return false;
   }
 
-  setOtherVariables(fenParsed, pieceArray) {
-    super.setOtherVariables(fenParsed, pieceArray);
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
     // Stack of "last move" only for intermediate chaining
     this.lastMoveEnd = [];
   }
diff --git a/variants/Coregal/class.js b/variants/Coregal/class.js
index 23f67c7..bbc35a4 100644
--- a/variants/Coregal/class.js
+++ b/variants/Coregal/class.js
@@ -61,8 +61,8 @@ export default class CoregalRules extends ChessRules {
     );
   }
 
-  setOtherVariables(fenParsed, pieceArray) {
-    super.setOtherVariables(fenParsed, pieceArray);
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
     this.relPos = {
       'w': {
         'k': fenParsed.relpos[0],
diff --git a/variants/Discoduel/class.js b/variants/Discoduel/class.js
new file mode 100644
index 0000000..c081b31
--- /dev/null
+++ b/variants/Discoduel/class.js
@@ -0,0 +1,51 @@
+import ChessRules from "/base_rules.js";
+import {ArrayFun} from "/utils/array.js"
+
+export default class DiscoduelRules extends ChessRules {
+
+  static get Options() {
+    return {}; //nothing would make sense
+  }
+
+  get pawnPromotions() {
+    return ['p'];
+  }
+
+  get hasFlags() {
+    return false;
+  }
+
+  genRandInitBaseFen() {
+    return {
+      fen: "1n4n1/8/8/8/8/8/PPPPPPPP/8",
+      o: {}
+    };
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const moves = super.getPotentialMovesFrom([x, y]);
+    if (this.turn == 'b')
+      // Prevent pawn captures on last rank:
+      return moves.filter(m => m.vanish.length == 1 || m.vanish[1].x != 0);
+    return moves;
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCurrentScore() {
+    // No real winning condition (promotions count...)
+    if (
+      ArrayFun.range(1, this.size.x).every(row_idx => {
+        this.board[row_idx].every(square => square.charAt(0) != 'w')
+      })
+      ||
+      !this.atLeastOneMove(this.turn)
+    ) {
+      return "1/2";
+    }
+    return "*";
+  }
+
+};
diff --git a/variants/Discoduel/rules.html b/variants/Discoduel/rules.html
new file mode 100644
index 0000000..490d279
--- /dev/null
+++ b/variants/Discoduel/rules.html
@@ -0,0 +1,5 @@
+<p>
+  Eight pawns try to promote, while the knights attempts to prevent them.
+  A pawn reaching last rank does not transform, but become immune to captures.
+</p>
+<p>Goal: "promoting" as many pawns as possible.</p>
diff --git a/variants/Discoduel/style.css b/variants/Discoduel/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Discoduel/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
diff --git a/variants/Dobutsu/class.js b/variants/Dobutsu/class.js
new file mode 100644
index 0000000..7632a17
--- /dev/null
+++ b/variants/Dobutsu/class.js
@@ -0,0 +1,100 @@
+import ChessRules from "/base_rules.js";
+
+export default class DobutsuRules extends ChessRules {
+
+  static get Options() {
+    return {};
+  }
+
+  get hasFlags() {
+    return false;
+  }
+
+  get hasEnpassant() {
+    return false;
+  }
+
+  pieces(color, x, y) {
+    const pawnShift = this.getPawnShift(color || 'w');
+    // NOTE: classs change according to playerColor (orientation)
+    const mySide = (this.playerColor == color);
+    return {
+      'c': {
+        "class": (mySide ? "" : "rev-") + "chick",
+        both: [{steps: [[pawnShift, 0]], range: 1}]
+      },
+      'h': {
+        "class": (mySide ? "" : "rev-") + "hen",
+        both: [
+          {
+            steps: [
+              [pawnShift, 1], [pawnShift, -1],
+              [0, 1], [0, -1], [1, 0], [-1, 0]
+            ],
+            range: 1
+          }
+        ]
+      },
+      'e': {
+        "class": (mySide ? "" : "rev-") + "elephant",
+        both: [{steps: [[-1, 1], [-1, -1], [1, 1], [1, -1]], range: 1}]
+      },
+      'g': {
+        "class": (mySide ? "" : "rev-") + "giraffe",
+        both: [{steps: [[0, 1], [0, -1], [1, 0], [-1, 0]], range: 1}]
+      },
+      'l': {
+        "class": (mySide ? "" : "rev-") + "lion",
+        both: [{
+          steps: [[-1, 1], [-1, -1], [1, 1], [1, -1],
+                  [0, 1], [0, -1], [1, 0], [-1, 0]],
+          range: 1
+        }]
+      }
+    };
+  }
+
+  isKing(x, y, p) {
+    if (!p)
+      p = this.getPiece(x, y);
+    return (p == 'l');
+  }
+
+  static get ReserveArray() {
+    return ['p', 'h', 'e', 'g'];
+  }
+
+  constructor(o) {
+    o.options = {crazyhouse: true, taking: true};
+    super(o);
+  }
+
+  get pawnPromotions() {
+    return ['h'];
+  }
+
+  genRandInitBaseFen() {
+    return {
+      fen: "gle/1c1/1C1/ELG",
+      o: {}
+    };
+  }
+
+  get size() {
+    return {x: 4, y: 4};
+  }
+
+  getCurrentScore(move_s) {
+    const res = super.getCurrentScore(move_s);
+    if (res != '*')
+      return res;
+    const oppCol = C.GetOppTurn(this.turn);
+    const oppLastRank = (oppCol == 'b' ? 3 : 0);
+    for (let j=0; j < this.size.y; j++) {
+      if (this.board[oppLastRank][j] == oppCol + 'l')
+        return (oppCol == 'w' ? "1-0" : "0-1");
+    }
+    return "*";
+  }
+
+};
diff --git a/variants/Dobutsu/rules.html b/variants/Dobutsu/rules.html
new file mode 100644
index 0000000..45bbc5d
--- /dev/null
+++ b/variants/Dobutsu/rules.html
@@ -0,0 +1,12 @@
+<p>
+  Simplified Shogi game. Goal: capture the Lion.
+  Pieces move as indicated on them (red arrows).
+</p>
+
+<p>Captured units can be landed on the board later.</p>
+
+<p>
+  <a href="https://en.wikipedia.org/wiki/D%C5%8Dbutsu_sh%C5%8Dgi">
+    Wikipedia page
+  </a>.
+</p>
diff --git a/variants/Dobutsu/style.css b/variants/Dobutsu/style.css
new file mode 100644
index 0000000..fb861f1
--- /dev/null
+++ b/variants/Dobutsu/style.css
@@ -0,0 +1,34 @@
+piece.chick {
+  background-image: url('/pieces/Dobutsu/chick.svg');
+}
+piece.rev-chick {
+  background-image: url('/pieces/Dobutsu/rev_chick.svg');
+}
+
+piece.hen {
+  background-image: url('/pieces/Dobutsu/hen.svg');
+}
+piece.rev-hen {
+  background-image: url('/pieces/Dobutsu/rev_hen.svg');
+}
+
+piece.elephant {
+  background-image: url('/pieces/Dobutsu/elephant.svg');
+}
+piece.rev-elephant {
+  background-image: url('/pieces/Dobutsu/rev_elephant.svg');
+}
+
+piece.giraffe {
+  background-image: url('/pieces/Dobutsu/giraffe.svg');
+}
+piece.rev-giraffe {
+  background-image: url('/pieces/Dobutsu/rev_giraffe.svg');
+}
+
+piece.lion {
+  background-image: url('/pieces/Dobutsu/lion.svg');
+}
+piece.rev-lion {
+  background-image: url('/pieces/Dobutsu/rev_lion.svg');
+}
-- 
2.48.1


From c4d2eb5bdf1b23d8c4a9d09322f84a9e0da9d60c Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 3 Jan 2024 17:10:33 +0100
Subject: [PATCH 05/16] Fix Dobutsu

---
 base_rules.js             | 17 +++++++++--------
 variants/Dobutsu/class.js | 21 ++++++++++-----------
 2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/base_rules.js b/base_rules.js
index 217d055..9a364e8 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -465,7 +465,7 @@ export default class ChessRules {
   // VISUAL UTILS
 
   getPieceWidth(rwidth) {
-    return (rwidth / this.size.y);
+    return (rwidth / Math.max(this.size.x, this.size.y));
   }
 
   getReserveSquareSize(rwidth, nbR) {
@@ -856,9 +856,10 @@ export default class ChessRules {
       y = (this.playerColor == i ? y = r.height + 5 : - 5 - rsqSize);
     }
     else {
-      const sqSize = r.width / this.size.y;
+      const sqSize = r.width / Math.max(this.size.x, this.size.y);
       const flipped = this.flippedBoard;
-      x = (flipped ? this.size.y - 1 - j : j) * sqSize;
+      x = (flipped ? this.size.y - 1 - j : j) * sqSize +
+          Math.abs(this.size.x - this.size.y) * sqSize / 2;
       y = (flipped ? this.size.x - 1 - i : i) * sqSize;
     }
     return [r.x + x, r.y + y];
@@ -2284,11 +2285,6 @@ export default class ChessRules {
     if (this.hasCastle)
       this.updateCastleFlags(move);
     if (this.options["crazyhouse"]) {
-      move.vanish.forEach(v => {
-        const square = C.CoordsToSquare({x: v.x, y: v.y});
-        if (this.ispawn[square])
-          delete this.ispawn[square];
-      });
       if (move.appear.length > 0 && move.vanish.length > 0) {
         // Assumption: something is moving
         const initSquare = C.CoordsToSquare(move.start);
@@ -2307,6 +2303,11 @@ export default class ChessRules {
           delete this.ispawn[destSquare];
         }
       }
+      move.vanish.forEach(v => {
+        const square = C.CoordsToSquare({x: v.x, y: v.y});
+        if (this.ispawn[square])
+          delete this.ispawn[square];
+      });
     }
     const minSize = Math.min(move.appear.length, move.vanish.length);
     if (
diff --git a/variants/Dobutsu/class.js b/variants/Dobutsu/class.js
index 7632a17..29b0708 100644
--- a/variants/Dobutsu/class.js
+++ b/variants/Dobutsu/class.js
@@ -19,7 +19,7 @@ export default class DobutsuRules extends ChessRules {
     // NOTE: classs change according to playerColor (orientation)
     const mySide = (this.playerColor == color);
     return {
-      'c': {
+      'p': {
         "class": (mySide ? "" : "rev-") + "chick",
         both: [{steps: [[pawnShift, 0]], range: 1}]
       },
@@ -43,7 +43,7 @@ export default class DobutsuRules extends ChessRules {
         "class": (mySide ? "" : "rev-") + "giraffe",
         both: [{steps: [[0, 1], [0, -1], [1, 0], [-1, 0]], range: 1}]
       },
-      'l': {
+      'k': {
         "class": (mySide ? "" : "rev-") + "lion",
         both: [{
           steps: [[-1, 1], [-1, -1], [1, 1], [1, -1],
@@ -54,16 +54,15 @@ export default class DobutsuRules extends ChessRules {
     };
   }
 
-  isKing(x, y, p) {
-    if (!p)
-      p = this.getPiece(x, y);
-    return (p == 'l');
-  }
-
   static get ReserveArray() {
     return ['p', 'h', 'e', 'g'];
   }
 
+  updateReserve(color, piece, count) {
+    if (piece != 'k')
+      super.updateReserve(color, piece, count);
+  }
+
   constructor(o) {
     o.options = {crazyhouse: true, taking: true};
     super(o);
@@ -75,13 +74,13 @@ export default class DobutsuRules extends ChessRules {
 
   genRandInitBaseFen() {
     return {
-      fen: "gle/1c1/1C1/ELG",
+      fen: "gke/1p1/1P1/EKG",
       o: {}
     };
   }
 
   get size() {
-    return {x: 4, y: 4};
+    return {x: 4, y: 3};
   }
 
   getCurrentScore(move_s) {
@@ -91,7 +90,7 @@ export default class DobutsuRules extends ChessRules {
     const oppCol = C.GetOppTurn(this.turn);
     const oppLastRank = (oppCol == 'b' ? 3 : 0);
     for (let j=0; j < this.size.y; j++) {
-      if (this.board[oppLastRank][j] == oppCol + 'l')
+      if (this.board[oppLastRank][j] == oppCol + 'k')
         return (oppCol == 'w' ? "1-0" : "0-1");
     }
     return "*";
-- 
2.48.1


From 4fcd7ab062f5250757804d633df01bd0d06da137 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 4 Jan 2024 10:23:47 +0100
Subject: [PATCH 06/16] Fix Discoduel

---
 variants/Discoduel/class.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/variants/Discoduel/class.js b/variants/Discoduel/class.js
index c081b31..ec96a43 100644
--- a/variants/Discoduel/class.js
+++ b/variants/Discoduel/class.js
@@ -38,7 +38,9 @@ export default class DiscoduelRules extends ChessRules {
     // No real winning condition (promotions count...)
     if (
       ArrayFun.range(1, this.size.x).every(row_idx => {
-        this.board[row_idx].every(square => square.charAt(0) != 'w')
+        return this.board[row_idx].every(square => {
+          return (!square || square.charAt(0) != 'w');
+        })
       })
       ||
       !this.atLeastOneMove(this.turn)
-- 
2.48.1


From d66135396f3a6e140947545630004ce11f8eee7b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 4 Jan 2024 10:37:28 +0100
Subject: [PATCH 07/16] Fix Dobutsu, extend Align4

---
 variants/Align4/class.js  | 19 +++++++++++++++++++
 variants/Dobutsu/class.js | 15 ++++++++++-----
 2 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/variants/Align4/class.js b/variants/Align4/class.js
index 09a79ed..f5b8f83 100644
--- a/variants/Align4/class.js
+++ b/variants/Align4/class.js
@@ -13,6 +13,14 @@ export default class Align4Rules extends ChessRules {
           {label: "Random", value: 1}
         ]
       }],
+      input: [
+        {
+          label: "Pawn first",
+          variable: "pawnfirst",
+          type: "checkbox",
+          defaut: false
+        }
+      ],
       styles: ["atomic", "capture", "cylinder"]
     };
   }
@@ -39,6 +47,17 @@ export default class Align4Rules extends ChessRules {
   // Just do not update any reserve (infinite supply)
   updateReserve() {}
 
+  canDrop([c, p], [i, j]) {
+    return (
+      this.board[i][j] == "" &&
+      (
+        p != "p" || this.options["pawnfirst"] ||
+        (c == 'w' && i < this.size.x - 1) ||
+        (c == 'b' && i > 0)
+      )
+    );
+  }
+
   getCurrentScore(move_s) {
     const score = super.getCurrentScore(move_s);
     if (score != "*")
diff --git a/variants/Dobutsu/class.js b/variants/Dobutsu/class.js
index 29b0708..d8ceafa 100644
--- a/variants/Dobutsu/class.js
+++ b/variants/Dobutsu/class.js
@@ -87,11 +87,16 @@ export default class DobutsuRules extends ChessRules {
     const res = super.getCurrentScore(move_s);
     if (res != '*')
       return res;
-    const oppCol = C.GetOppTurn(this.turn);
-    const oppLastRank = (oppCol == 'b' ? 3 : 0);
-    for (let j=0; j < this.size.y; j++) {
-      if (this.board[oppLastRank][j] == oppCol + 'k')
-        return (oppCol == 'w' ? "1-0" : "0-1");
+    for (let lastRank of [0, 3]) {
+      const color = (lastRank == 0 ? 'w' : 'b');
+      for (let j=0; j < this.size.y; j++) {
+        if (
+          this.board[lastRank][j] == color + 'k' &&
+          !this.underAttack([lastRank, j], [C.GetOppTurn(color)])
+        ) {
+          return (color == 'w' ? "1-0" : "0-1");
+        }
+      }
     }
     return "*";
   }
-- 
2.48.1


From 66ab134b7ab3ba00204fb316ba7636c904331d6c Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 4 Jan 2024 11:19:10 +0100
Subject: [PATCH 08/16] Add Doublearmy. Start thinking about Dynamo

---
 README.md                           |  10 +-
 TODO                                |   3 -
 initialize.sh                       |   8 +
 pieces/black_commoner.svg           | 105 ++++
 pieces/white_commoner.svg           |  94 +++
 variants.js                         |   4 +-
 variants/Doublearmy/class.js        |  43 ++
 variants/Doublearmy/rules.html      |   6 +
 variants/Doublearmy/style.css       |   9 +
 variants/Dynamo/class.js            | 921 ++++++++++++++++++++++++++++
 variants/Dynamo/complete_rules.html | 142 +++++
 variants/Dynamo/rules.html          |  24 +
 variants/Dynamo/style.css           |   1 +
 13 files changed, 1359 insertions(+), 11 deletions(-)
 create mode 100755 initialize.sh
 create mode 100644 pieces/black_commoner.svg
 create mode 100644 pieces/white_commoner.svg
 create mode 100644 variants/Doublearmy/class.js
 create mode 100644 variants/Doublearmy/rules.html
 create mode 100644 variants/Doublearmy/style.css
 create mode 100644 variants/Dynamo/class.js
 create mode 100644 variants/Dynamo/complete_rules.html
 create mode 100644 variants/Dynamo/rules.html
 create mode 100644 variants/Dynamo/style.css

diff --git a/README.md b/README.md
index 4636ac4..0b900c5 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,10 @@ PHP + Node.js + npm.
 
 ## Usage
 
-```wget https://xogo.live/assets.zip && unzip assets.zip``` <br/>
-```wget https://xogo.live/extras.zip && unzip extras.zip``` <br/>
-Rename parameters.js.dist &rarr; parameters.js, and edit file. <br/>
-```npm i```
+Initialisation (done once):
 
-Generate some pieces: <br/>
-```python generateSVG.py``` in pieces/Avalam
+```./initialize.sh```
+
+You may want to edit the parameters.js file. Then:
 
 ```./start.sh``` (and later, ```./stop.sh```)
diff --git a/TODO b/TODO
index fac46e3..8afe466 100644
--- a/TODO
+++ b/TODO
@@ -1,6 +1,3 @@
-add variants :
-Dark Racing Kings ? Checkered-Teleport ?
-
 Hmm... non ? -->
 Otage, Emergo, Pacosako : fonction "buildPiece(arg1, arg2)" returns HTML element with 2 SVG or SVG + number
 ==> plus simple : deux classes, images superposées.
diff --git a/initialize.sh b/initialize.sh
new file mode 100755
index 0000000..b7a9f4f
--- /dev/null
+++ b/initialize.sh
@@ -0,0 +1,8 @@
+!#/bin/sh
+
+wget https://xogo.live/assets.zip && unzip assets.zip
+wget https://xogo.live/extras.zip && unzip extras.zip
+cp parameters.js.dist parameters.js
+npm i
+cd pieces/Avalam && python generateSVG.py
+#cd pieces/Emergo && python generateSVG.py
diff --git a/pieces/black_commoner.svg b/pieces/black_commoner.svg
new file mode 100644
index 0000000..0995449
--- /dev/null
+++ b/pieces/black_commoner.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="45"
+   height="45"
+   viewBox="0 0 11.90625 11.90625"
+   version="1.1"
+   id="svg4393"
+   sodipodi:docname="CommonerB_Transparent.svg"
+   inkscape:version="0.92.1 r15371">
+  <defs
+     id="defs4387" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="3"
+     inkscape:cx="30.240069"
+     inkscape:cy="21.353804"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:window-width="1600"
+     inkscape:window-height="837"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata4390">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-285.09373)">
+    <g
+       id="g4572"
+       transform="matrix(0.28921369,0,0,0.28921369,-0.54713251,283.72613)">
+      <circle
+         id="circle4537"
+         r="2.5"
+         cy="13.5"
+         cx="22.5"
+         style="fill:#000000;stroke:#000000;stroke-width:1.5;stroke-linejoin:round" />
+      <circle
+         id="circle4539"
+         r="1.5"
+         cy="13.5"
+         cx="22.5"
+         style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linejoin:round" />
+      <g
+         id="g4543"
+         style="fill:#000000;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
+        <!-- test -->
+        <path
+           id="path4541"
+           d="m 11.5,37 c 5.5,3.5 15.5,3.5 21,0 v -7 c 0,0 9,-4.5 6,-11 -5.5,-7 -26.5,-7 -32,0 -3,6.5 5,10.5 5,10.5 z"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g4553"
+         style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round">
+        <!-- test -->
+        <path
+           id="path4545"
+           d="m 32,29.5 c 0,0 8.5,-4 6,-9.65 C 32.65,13 12.35,13 7,19.85 c -2.5,5.65 4.85,9 4.85,9"
+           inkscape:connector-curvature="0" />
+        <!-- talp -->
+        <path
+           id="path4547"
+           d="M 11.5,30 C 17,27 27,27 32.5,30"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path4549"
+           d="m 11.5,33.5 c 5.5,-3 15.5,-3 21,0"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path4551"
+           d="M 11.5,37 C 17,34 27,34 32.5,37"
+           inkscape:connector-curvature="0" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/pieces/white_commoner.svg b/pieces/white_commoner.svg
new file mode 100644
index 0000000..12f2b27
--- /dev/null
+++ b/pieces/white_commoner.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="45"
+   height="45"
+   viewBox="0 0 11.90625 11.90625"
+   version="1.1"
+   id="svg4393"
+   inkscape:version="0.92.1 r15371"
+   sodipodi:docname="Commoner_Transparent.svg">
+  <defs
+     id="defs4387" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="5.1998528"
+     inkscape:cy="24.301903"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:window-width="1600"
+     inkscape:window-height="837"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata4390">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-285.09373)">
+    <g
+       id="g4499"
+       transform="matrix(0.28598519,0,0,0.28598519,-0.47456997,283.80785)">
+      <g
+         id="g4399"
+         style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
+        <!-- bojt -->
+        <circle
+           id="circle4395"
+           r="2.5"
+           cy="13.5"
+           cx="22.5" />
+        <!-- test -->
+        <path
+           id="path4397"
+           d="m 11.5,37 c 5.5,3.5 15.5,3.5 21,0 v -7 c 0,0 9,-4.5 6,-11 -5.5,-7 -26.5,-7 -32,0 -3,6.5 5,10.5 5,10.5 z"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g4407"
+         style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round">
+        <!-- talp -->
+        <path
+           id="path4401"
+           d="M 11.5,30 C 17,27 27,27 32.5,30"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path4403"
+           d="m 11.5,33.5 c 5.5,-3 15.5,-3 21,0"
+           inkscape:connector-curvature="0" />
+        <path
+           id="path4405"
+           d="M 11.5,37 C 17,34 27,34 32.5,37"
+           inkscape:connector-curvature="0" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/variants.js b/variants.js
index f42033e..10cf42a 100644
--- a/variants.js
+++ b/variants.js
@@ -45,9 +45,9 @@ const variants = [
   {name: 'Dice', desc: 'Roll the dice'},
   {name: 'Discoduel', desc: 'Enter the disco', disp: 'Disco Duel'},
   {name: 'Dobutsu', desc: "Let's catch the Lion!"},
-//  {name: 'Doublearmy', desc: '64 pieces on the board', disp: 'Double Army'},
+  {name: 'Doublearmy', desc: '64 pieces on the board', disp: 'Double Army'},
   {name: 'Doublemove', desc: 'Double moves'},
-//  {name: 'Dynamo', desc: 'Push and pull'},
+  {name: 'Dynamo', desc: 'Push and pull'},
 //  {name: 'Eightpieces', desc: 'Each piece is unique', disp: '8 Pieces'},
 //  {name: 'Emergo', desc: 'Stacking Checkers variant'},
 //  {name: 'Empire', desc: 'Empire versus Kingdom'},
diff --git a/variants/Doublearmy/class.js b/variants/Doublearmy/class.js
new file mode 100644
index 0000000..abbb564
--- /dev/null
+++ b/variants/Doublearmy/class.js
@@ -0,0 +1,43 @@
+import ChessRules from "/base_rules.js";
+
+export default class DoublearmyRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.select,
+      input: C.Options.input,
+      styles: C.Options.styles.filter(s => s != "madrasi")
+    };
+  }
+
+  pieces(color, x, y) {
+    let res = super.pieces(color, x, y);
+    return Object.assign(
+      {
+        'c': {
+          "class": "commoner",
+          moveas: 'k'
+        }
+      },
+      res
+    );
+  }
+
+  genRandInitBaseFen() {
+    const s = super.genRandInitBaseFen();
+    const rows = s.fen.split('/');
+    return {
+      fen:
+        rows[0] + "/" +
+        rows[1] + "/" +
+        rows[0].replace('k', 'c') + "/" +
+        rows[1] + "/" +
+        rows[6] + "/" +
+        rows[7].replace('K', 'C') + "/" +
+        rows[6] + "/" +
+        rows[7],
+      o: s.o
+    };
+  }
+
+};
diff --git a/variants/Doublearmy/rules.html b/variants/Doublearmy/rules.html
new file mode 100644
index 0000000..b9908ab
--- /dev/null
+++ b/variants/Doublearmy/rules.html
@@ -0,0 +1,6 @@
+<p>
+  The four middle ranks contain a replica of the initial pieces.
+  The central "king" has no royal status, and is thus named "commoner".
+</p>
+
+<p class="author">Vincent Rothuis (2020).</p>
diff --git a/variants/Doublearmy/style.css b/variants/Doublearmy/style.css
new file mode 100644
index 0000000..e50c2f4
--- /dev/null
+++ b/variants/Doublearmy/style.css
@@ -0,0 +1,9 @@
+@import url("/base_pieces.css");
+
+piece.black.commoner {
+  background-image: url('/pieces/black_commoner.svg');
+}
+
+piece.white.commoner {
+  background-image: url('/pieces/white_commoner.svg');
+}
diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
new file mode 100644
index 0000000..0996a70
--- /dev/null
+++ b/variants/Dynamo/class.js
@@ -0,0 +1,921 @@
+import ChessRules from "/base_rules.js";
+
+export default class DynamoRules extends ChessRules {
+
+  // TODO? later, allow to push out pawns on a and h files
+  get hasEnpassant() {
+    return false;
+  }
+
+/// TODO:::
+
+  canIplay(side, [x, y]) {
+    // Sometimes opponent's pieces can be moved directly
+    return this.turn == side;
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.subTurn = 1;
+    // Local stack of "action moves"
+    this.amoves = [];
+    const amove = V.ParseFen(fen).amove;
+    if (amove != "-") {
+      const amoveParts = amove.split("/");
+      let move = {
+        // No need for start & end
+        appear: [],
+        vanish: []
+      };
+      [0, 1].map(i => {
+        if (amoveParts[i] != "-") {
+          amoveParts[i].split(".").forEach(av => {
+            // Format is "bpe3"
+            const xy = V.SquareToCoords(av.substr(2));
+            move[i == 0 ? "appear" : "vanish"].push(
+              new PiPo({
+                x: xy.x,
+                y: xy.y,
+                c: av[0],
+                p: av[1]
+              })
+            );
+          });
+        }
+      });
+      this.amoves.push(move);
+    }
+    // Stack "first moves" (on subTurn 1) to merge and check opposite moves
+    this.firstMove = [];
+  }
+
+  static ParseFen(fen) {
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { amove: fen.split(" ")[4] }
+    );
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParts = fen.split(" ");
+    if (fenParts.length != 5) return false;
+    if (fenParts[4] != "-") {
+      // TODO: a single regexp instead.
+      // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-'
+      const amoveParts = fenParts[4].split("/");
+      if (amoveParts.length != 2) return false;
+      for (let part of amoveParts) {
+        if (part != "-") {
+          for (let psq of part.split("."))
+            if (!psq.match(/^[a-z]{3}[1-8]$/)) return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getAmoveFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getAmoveFen();
+  }
+
+  getAmoveFen() {
+    const L = this.amoves.length;
+    if (L == 0) return "-";
+    return (
+      ["appear","vanish"].map(
+        mpart => {
+          if (this.amoves[L-1][mpart].length == 0) return "-";
+          return (
+            this.amoves[L-1][mpart].map(
+              av => {
+                const square = V.CoordsToSquare({ x: av.x, y: av.y });
+                return av.c + av.p + square;
+              }
+            ).join(".")
+          );
+        }
+      ).join("/")
+    );
+  }
+
+  canTake() {
+    // Captures don't occur (only pulls & pushes)
+    return false;
+  }
+
+  // Step is right, just add (push/pull) moves in this direction
+  // Direction is assumed normalized.
+  getMovesInDirection([x, y], [dx, dy], nbSteps) {
+    nbSteps = nbSteps || 8; //max 8 steps anyway
+    let [i, j] = [x + dx, y + dy];
+    let moves = [];
+    const color = this.getColor(x, y);
+    const piece = this.getPiece(x, y);
+    const lastRank = (color == 'w' ? 0 : 7);
+    let counter = 1;
+    while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+      if (i == lastRank && piece == V.PAWN) {
+        // Promotion by push or pull
+        V.PawnSpecs.promotions.forEach(p => {
+          let move = super.getBasicMove([x, y], [i, j], { c: color, p: p });
+          moves.push(move);
+        });
+      }
+      else moves.push(super.getBasicMove([x, y], [i, j]));
+      if (++counter > nbSteps) break;
+      i += dx;
+      j += dy;
+    }
+    if (!V.OnBoard(i, j) && piece != V.KING) {
+      // Add special "exit" move, by "taking king"
+      moves.push(
+        new Move({
+          start: { x: x, y: y },
+          end: { x: this.kingPos[color][0], y: this.kingPos[color][1] },
+          appear: [],
+          vanish: [{ x: x, y: y, c: color, p: piece }]
+        })
+      );
+    }
+    return moves;
+  }
+
+  // Normalize direction to know the step
+  getNormalizedDirection([dx, dy]) {
+    const absDir = [Math.abs(dx), Math.abs(dy)];
+    let divisor = 0;
+    if (absDir[0] != 0 && absDir[1] != 0 && absDir[0] != absDir[1])
+      // Knight
+      divisor = Math.min(absDir[0], absDir[1]);
+    else
+      // Standard slider (or maybe a pawn or king: same)
+      divisor = Math.max(absDir[0], absDir[1]);
+    return [dx / divisor, dy / divisor];
+  }
+
+  // There was something on x2,y2, maybe our color, pushed or (self)pulled
+  isAprioriValidExit([x1, y1], [x2, y2], color2, piece2) {
+    const color1 = this.getColor(x1, y1);
+    const pawnShift = (color1 == 'w' ? -1 : 1);
+    const lastRank = (color1 == 'w' ? 0 : 7);
+    const deltaX = Math.abs(x1 - x2);
+    const deltaY = Math.abs(y1 - y2);
+    const checkSlider = () => {
+      const dir = this.getNormalizedDirection([x2 - x1, y2 - y1]);
+      let [i, j] = [x1 + dir[0], y1 + dir[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += dir[0];
+        j += dir[1];
+      }
+      return !V.OnBoard(i, j);
+    };
+    switch (piece2 || this.getPiece(x1, y1)) {
+      case V.PAWN:
+        return (
+          x1 + pawnShift == x2 &&
+          (
+            (color1 == color2 && x2 == lastRank && y1 == y2) ||
+            (
+              color1 != color2 &&
+              deltaY == 1 &&
+              !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+            )
+          )
+        );
+      case V.ROOK:
+        if (x1 != x2 && y1 != y2) return false;
+        return checkSlider();
+      case V.KNIGHT:
+        return (
+          deltaX + deltaY == 3 &&
+          (deltaX == 1 || deltaY == 1) &&
+          !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+        );
+      case V.BISHOP:
+        if (deltaX != deltaY) return false;
+        return checkSlider();
+      case V.QUEEN:
+        if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) return false;
+        return checkSlider();
+      case V.KING:
+        return (
+          deltaX <= 1 &&
+          deltaY <= 1 &&
+          !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+        );
+    }
+    return false;
+  }
+
+  isAprioriValidVertical([x1, y1], x2) {
+    const piece = this.getPiece(x1, y1);
+    const deltaX = Math.abs(x1 - x2);
+    const startRank = (this.getColor(x1, y1) == 'w' ? 6 : 1);
+    return (
+      [V.QUEEN, V.ROOK].includes(piece) ||
+      (
+        [V.KING, V.PAWN].includes(piece) &&
+        (
+          deltaX == 1 ||
+          (deltaX == 2 && piece == V.PAWN && x1 == startRank)
+        )
+      )
+    );
+  }
+
+  // NOTE: for pushes, play the pushed piece first.
+  //       for pulls: play the piece doing the action first
+  // NOTE: to push a piece out of the board, make it slide until its king
+  getPotentialMovesFrom([x, y]) {
+    const color = this.turn;
+    const sqCol = this.getColor(x, y);
+    const pawnShift = (color == 'w' ? -1 : 1);
+    const pawnStartRank = (color == 'w' ? 6 : 1);
+    const getMoveHash = (m) => {
+      return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end);
+    };
+    if (this.subTurn == 1) {
+      const addMoves = (dir, nbSteps) => {
+        const newMoves =
+          this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps)
+          .filter(m => !movesHash[getMoveHash(m)]);
+        newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; });
+        Array.prototype.push.apply(moves, newMoves);
+      };
+      // Free to play any move (if piece of my color):
+      let moves =
+        sqCol == color
+          ? super.getPotentialMovesFrom([x, y])
+          : [];
+      // There may be several suicide moves: keep only one
+      let hasExit = false;
+      moves = moves.filter(m => {
+        const suicide = (m.appear.length == 0);
+        if (suicide) {
+          if (hasExit) return false;
+          hasExit = true;
+        }
+        return true;
+      });
+      // Structure to avoid adding moves twice (can be action & move)
+      let movesHash = {};
+      moves.forEach(m => { movesHash[getMoveHash(m)] = true; });
+      // [x, y] is pushed by 'color'
+      for (let step of V.steps[V.KNIGHT]) {
+        const [i, j] = [x + step[0], y + step[1]];
+        if (
+          V.OnBoard(i, j) &&
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == color &&
+          this.getPiece(i, j) == V.KNIGHT
+        ) {
+          addMoves(step, 1);
+        }
+      }
+      for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+        let [i, j] = [x + step[0], y + step[1]];
+        while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+          i += step[0];
+          j += step[1];
+        }
+        if (
+          V.OnBoard(i, j) &&
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) == color
+        ) {
+          const deltaX = Math.abs(i - x);
+          const deltaY = Math.abs(j - y);
+          switch (this.getPiece(i, j)) {
+            case V.PAWN:
+              if (
+                (x - i) / deltaX == pawnShift &&
+                deltaX <= 2 &&
+                deltaY <= 1
+              ) {
+                if (sqCol == color && deltaY == 0) {
+                  // Pushed forward
+                  const maxSteps = (i == pawnStartRank && deltaX == 1 ? 2 : 1);
+                  addMoves(step, maxSteps);
+                }
+                else if (sqCol != color && deltaY == 1 && deltaX == 1)
+                  // Pushed diagonally
+                  addMoves(step, 1);
+              }
+              break;
+            case V.ROOK:
+              if (deltaX == 0 || deltaY == 0) addMoves(step);
+              break;
+            case V.BISHOP:
+              if (deltaX == deltaY) addMoves(step);
+              break;
+            case V.QUEEN:
+              // All steps are valid for a queen:
+              addMoves(step);
+              break;
+            case V.KING:
+              if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1);
+              break;
+          }
+        }
+      }
+      return moves;
+    }
+    // If subTurn == 2 then we should have a first move,
+    // which restrict what we can play now: only in the first move direction
+    const L = this.firstMove.length;
+    const fm = this.firstMove[L-1];
+    if (
+      (fm.appear.length == 2 && fm.vanish.length == 2) ||
+      (fm.vanish[0].c == sqCol && sqCol != color)
+    ) {
+      // Castle or again opponent color: no move playable then.
+      return [];
+    }
+    const piece = this.getPiece(x, y);
+    const getPushExit = () => {
+      // Piece at subTurn 1 exited: can I have caused the exit?
+      if (
+        this.isAprioriValidExit(
+          [x, y],
+          [fm.start.x, fm.start.y],
+          fm.vanish[0].c
+        )
+      ) {
+        // Seems so:
+        const dir = this.getNormalizedDirection(
+          [fm.start.x - x, fm.start.y - y]);
+        const nbSteps =
+          [V.PAWN, V.KING, V.KNIGHT].includes(piece)
+            ? 1
+            : null;
+        return this.getMovesInDirection([x, y], dir, nbSteps);
+      }
+      return [];
+    }
+    const getPushMoves = () => {
+      // Piece from subTurn 1 is still on board:
+      const dirM = this.getNormalizedDirection(
+        [fm.end.x - fm.start.x, fm.end.y - fm.start.y]);
+      const dir = this.getNormalizedDirection(
+        [fm.start.x - x, fm.start.y - y]);
+      // Normalized directions should match
+      if (dir[0] == dirM[0] && dir[1] == dirM[1]) {
+        // We don't know if first move is a pushed piece or normal move,
+        // so still must check if the push is valid.
+        const deltaX = Math.abs(fm.start.x - x);
+        const deltaY = Math.abs(fm.start.y - y);
+        switch (piece) {
+          case V.PAWN:
+            if (x == pawnStartRank) {
+              if (
+                (fm.start.x - x) * pawnShift < 0 ||
+                deltaX >= 3 ||
+                deltaY >= 2 ||
+                (fm.vanish[0].c == color && deltaY > 0) ||
+                (fm.vanish[0].c != color && deltaY == 0) ||
+                Math.abs(fm.end.x - fm.start.x) > deltaX ||
+                fm.end.y - fm.start.y != fm.start.y - y
+              ) {
+                return [];
+              }
+            }
+            else {
+              if (
+                fm.start.x - x != pawnShift ||
+                deltaY >= 2 ||
+                (fm.vanish[0].c == color && deltaY == 1) ||
+                (fm.vanish[0].c != color && deltaY == 0) ||
+                fm.end.x - fm.start.x != pawnShift ||
+                fm.end.y - fm.start.y != fm.start.y - y
+              ) {
+                return [];
+              }
+            }
+            break;
+          case V.KNIGHT:
+            if (
+              (deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) ||
+              (fm.end.x - fm.start.x != fm.start.x - x) ||
+              (fm.end.y - fm.start.y != fm.start.y - y)
+            ) {
+              return [];
+            }
+            break;
+          case V.KING:
+            if (
+              (deltaX >= 2 || deltaY >= 2) ||
+              (fm.end.x - fm.start.x != fm.start.x - x) ||
+              (fm.end.y - fm.start.y != fm.start.y - y)
+            ) {
+              return [];
+            }
+            break;
+          case V.BISHOP:
+            if (deltaX != deltaY) return [];
+            break;
+          case V.ROOK:
+            if (deltaX != 0 && deltaY != 0) return [];
+            break;
+          case V.QUEEN:
+            if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return [];
+            break;
+        }
+        // Nothing should stand between [x, y] and the square fm.start
+        let [i, j] = [x + dir[0], y + dir[1]];
+        while (
+          (i != fm.start.x || j != fm.start.y) &&
+          this.board[i][j] == V.EMPTY
+        ) {
+          i += dir[0];
+          j += dir[1];
+        }
+        if (i == fm.start.x && j == fm.start.y)
+          return this.getMovesInDirection([x, y], dir);
+      }
+      return [];
+    }
+    const getPullExit = () => {
+      // Piece at subTurn 1 exited: can I be pulled?
+      // Note: kings cannot suicide, so fm.vanish[0].p is not KING.
+      // Could be PAWN though, if a pawn was pushed out of board.
+      if (
+        fm.vanish[0].p != V.PAWN && //pawns cannot pull
+        this.isAprioriValidExit(
+          [x, y],
+          [fm.start.x, fm.start.y],
+          fm.vanish[0].c,
+          fm.vanish[0].p
+        )
+      ) {
+        // Seems so:
+        const dir = this.getNormalizedDirection(
+          [fm.start.x - x, fm.start.y - y]);
+        const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null);
+        return this.getMovesInDirection([x, y], dir, nbSteps);
+      }
+      return [];
+    };
+    const getPullMoves = () => {
+      if (fm.vanish[0].p == V.PAWN)
+        // pawns cannot pull
+        return [];
+      const dirM = this.getNormalizedDirection(
+        [fm.end.x - fm.start.x, fm.end.y - fm.start.y]);
+      const dir = this.getNormalizedDirection(
+        [fm.start.x - x, fm.start.y - y]);
+      // Normalized directions should match
+      if (dir[0] == dirM[0] && dir[1] == dirM[1]) {
+        // Am I at the right distance?
+        const deltaX = Math.abs(x - fm.start.x);
+        const deltaY = Math.abs(y - fm.start.y);
+        if (
+          (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) ||
+          (fm.vanish[0].p == V.KNIGHT &&
+            (deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0))
+        ) {
+          return [];
+        }
+        // Nothing should stand between [x, y] and the square fm.start
+        let [i, j] = [x + dir[0], y + dir[1]];
+        while (
+          (i != fm.start.x || j != fm.start.y) &&
+          this.board[i][j] == V.EMPTY
+        ) {
+          i += dir[0];
+          j += dir[1];
+        }
+        if (i == fm.start.x && j == fm.start.y)
+          return this.getMovesInDirection([x, y], dir);
+      }
+      return [];
+    };
+    if (fm.vanish[0].c != color) {
+      // Only possible action is a push:
+      if (fm.appear.length == 0) return getPushExit();
+      return getPushMoves();
+    }
+    else if (sqCol != color) {
+      // Only possible action is a pull, considering moving piece abilities
+      if (fm.appear.length == 0) return getPullExit();
+      return getPullMoves();
+    }
+    else {
+      // My color + my color: both actions possible
+      // Structure to avoid adding moves twice (can be action & move)
+      let movesHash = {};
+      if (fm.appear.length == 0) {
+        const pushes = getPushExit();
+        pushes.forEach(m => { movesHash[getMoveHash(m)] = true; });
+        return (
+          pushes.concat(getPullExit().filter(m => !movesHash[getMoveHash(m)]))
+        );
+      }
+      const pushes = getPushMoves();
+      pushes.forEach(m => { movesHash[getMoveHash(m)] = true; });
+      return (
+        pushes.concat(getPullMoves().filter(m => !movesHash[getMoveHash(m)]))
+      );
+    }
+    return [];
+  }
+
+  getSlideNJumpMoves([x, y], steps, oneStep) {
+    let moves = [];
+    const c = this.getColor(x, y);
+    const piece = this.getPiece(x, y);
+    outerLoop: for (let step of steps) {
+      let i = x + step[0];
+      let j = y + step[1];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        if (oneStep) continue outerLoop;
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        if (this.canTake([x, y], [i, j]))
+          moves.push(this.getBasicMove([x, y], [i, j]));
+      }
+      else {
+        // Add potential board exit (suicide), except for the king
+        if (piece != V.KING) {
+          moves.push({
+            start: { x: x, y: y},
+            end: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
+            appear: [],
+            vanish: [
+              new PiPo({
+                x: x,
+                y: y,
+                c: c,
+                p: piece
+              })
+            ]
+          });
+        }
+      }
+    }
+    return moves;
+  }
+
+  // Does m2 un-do m1 ? (to disallow undoing actions)
+  oppositeMoves(m1, m2) {
+    const isEqual = (av1, av2) => {
+      for (let av of av1) {
+        const avInAv2 = av2.find(elt => {
+          return (
+            elt.x == av.x &&
+            elt.y == av.y &&
+            elt.c == av.c &&
+            elt.p == av.p
+          );
+        });
+        if (!avInAv2) return false;
+      }
+      return true;
+    };
+    // All appear and vanish arrays must have the same length
+    const mL = m1.appear.length;
+    return (
+      m2.appear.length == mL &&
+      m1.vanish.length == mL &&
+      m2.vanish.length == mL &&
+      isEqual(m1.appear, m2.vanish) &&
+      isEqual(m1.vanish, m2.appear)
+    );
+  }
+
+  getAmove(move1, move2) {
+    // Just merge (one is action one is move, one may be empty)
+    return {
+      appear: move1.appear.concat(move2.appear),
+      vanish: move1.vanish.concat(move2.vanish)
+    }
+  }
+
+  filterValid(moves) {
+    const color = this.turn;
+    const La = this.amoves.length;
+    if (this.subTurn == 1) {
+      return moves.filter(m => {
+        // A move is valid either if it doesn't result in a check,
+        // or if a second move is possible to counter the check
+        // (not undoing a potential move + action of the opponent)
+        this.play(m);
+        let res = this.underCheck(color);
+        if (this.subTurn == 2) {
+          let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
+          if (res || isOpposite) {
+            const moves2 = this.getAllPotentialMoves();
+            for (let m2 of moves2) {
+              this.play(m2);
+              const res2 = this.underCheck(color);
+              const amove = this.getAmove(m, m2);
+              isOpposite =
+                La > 0 && this.oppositeMoves(this.amoves[La-1], amove);
+              this.undo(m2);
+              if (!res2 && !isOpposite) {
+                res = false;
+                break;
+              }
+            }
+          }
+        }
+        this.undo(m);
+        return !res;
+      });
+    }
+    if (La == 0) return super.filterValid(moves);
+    const Lf = this.firstMove.length;
+    return (
+      super.filterValid(
+        moves.filter(m => {
+          // Move shouldn't undo another:
+          const amove = this.getAmove(this.firstMove[Lf-1], m);
+          return !this.oppositeMoves(this.amoves[La-1], amove);
+        })
+      )
+    );
+  }
+
+  isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = y + step[1];
+      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
+        rx += step[0];
+        ry += step[1];
+      }
+      if (
+        V.OnBoard(rx, ry) &&
+        this.getPiece(rx, ry) == piece &&
+        this.getColor(rx, ry) == color
+      ) {
+        // Continue some steps in the same direction (pull)
+        rx += step[0];
+        ry += step[1];
+        while (
+          V.OnBoard(rx, ry) &&
+          this.board[rx][ry] == V.EMPTY &&
+          !oneStep
+        ) {
+          rx += step[0];
+          ry += step[1];
+        }
+        if (!V.OnBoard(rx, ry)) return true;
+        // Step in the other direction (push)
+        rx = x - step[0];
+        ry = y - step[1];
+        while (
+          V.OnBoard(rx, ry) &&
+          this.board[rx][ry] == V.EMPTY &&
+          !oneStep
+        ) {
+          rx -= step[0];
+          ry -= step[1];
+        }
+        if (!V.OnBoard(rx, ry)) return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByPawn([x, y], color) {
+    // The king can be pushed out by a pawn on last rank or near the edge
+    const pawnShift = (color == "w" ? 1 : -1);
+    for (let i of [-1, 1]) {
+      if (
+        V.OnBoard(x + pawnShift, y + i) &&
+        this.board[x + pawnShift][y + i] != V.EMPTY &&
+        this.getPiece(x + pawnShift, y + i) == V.PAWN &&
+        this.getColor(x + pawnShift, y + i) == color
+      ) {
+        if (!V.OnBoard(x - pawnShift, y - i)) return true;
+      }
+    }
+    return false;
+  }
+
+  static OnTheEdge(x, y) {
+    return (x == 0 || x == 7 || y == 0 || y == 7);
+  }
+
+  isAttackedByKing([x, y], color) {
+    // Attacked if I'm on the edge and the opponent king just next,
+    // but not on the edge.
+    if (V.OnTheEdge(x, y)) {
+      for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+        const [i, j] = [x + step[0], y + step[1]];
+        if (
+          V.OnBoard(i, j) &&
+          !V.OnTheEdge(i, j) &&
+          this.board[i][j] != V.EMPTY &&
+          this.getPiece(i, j) == V.KING
+          // NOTE: since only one king of each color, and (x, y) is occupied
+          // by our king, no need to check other king's color.
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  // No consideration of color: all pieces could be played
+  getAllPotentialMoves() {
+    let potentialMoves = [];
+    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) {
+          Array.prototype.push.apply(
+            potentialMoves,
+            this.getPotentialMovesFrom([i, j])
+          );
+        }
+      }
+    }
+    return potentialMoves;
+  }
+
+  getEmptyMove() {
+    return new Move({
+      start: { x: -1, y: -1 },
+      end: { x: -1, y: -1 },
+      appear: [],
+      vanish: []
+    });
+  }
+
+  doClick(square) {
+    // A click to promote a piece on subTurn 2 would trigger this.
+    // For now it would then return [NaN, NaN] because surrounding squares
+    // have no IDs in the promotion modal. TODO: improve this?
+    if (isNaN(square[0])) return null;
+    // If subTurn == 2 && square is empty && !underCheck && !isOpposite,
+    // then return an empty move, allowing to "pass" subTurn2
+    const La = this.amoves.length;
+    const Lf = this.firstMove.length;
+    if (
+      this.subTurn == 2 &&
+      this.board[square[0]][square[1]] == V.EMPTY &&
+      !this.underCheck(this.turn) &&
+      (La == 0 || !this.oppositeMoves(this.amoves[La-1], this.firstMove[Lf-1]))
+    ) {
+      return this.getEmptyMove();
+    }
+    return null;
+  }
+
+  play(move) {
+    if (this.subTurn == 1 && move.vanish.length == 0) {
+      // Patch to work with old format: (TODO: remove later)
+      move.ignore = true;
+      return;
+    }
+    const color = this.turn;
+    move.subTurn = this.subTurn; //for undo
+    const gotoNext = (mv) => {
+      const L = this.firstMove.length;
+      this.amoves.push(this.getAmove(this.firstMove[L-1], mv));
+      this.turn = V.GetOppCol(color);
+      this.subTurn = 1;
+      this.movesCount++;
+    };
+    move.flags = JSON.stringify(this.aggregateFlags());
+    V.PlayOnBoard(this.board, move);
+    if (this.subTurn == 2) gotoNext(move);
+    else {
+      this.subTurn = 2;
+      this.firstMove.push(move);
+      this.toNewKingPos(move);
+      if (
+        // Condition is true on empty arrays:
+        this.getAllPotentialMoves().every(m => {
+          V.PlayOnBoard(this.board, m);
+          this.toNewKingPos(m);
+          const res = this.underCheck(color);
+          V.UndoOnBoard(this.board, m);
+          this.toOldKingPos(m);
+          return res;
+        })
+      ) {
+        // No valid move at subTurn 2
+        gotoNext(this.getEmptyMove());
+      }
+      this.toOldKingPos(move);
+    }
+    this.postPlay(move);
+  }
+
+  toNewKingPos(move) {
+    for (let a of move.appear)
+      if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y];
+  }
+
+  postPlay(move) {
+    if (move.start.x < 0) return;
+    this.toNewKingPos(move);
+    this.updateCastleFlags(move);
+  }
+
+  updateCastleFlags(move) {
+    const firstRank = { 'w': V.size.x - 1, 'b': 0 };
+    for (let v of move.vanish) {
+      if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y];
+      else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) {
+        const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1);
+        this.castleFlags[v.c][flagIdx] = V.size.y;
+      }
+    }
+  }
+
+  undo(move) {
+    if (!!move.ignore) return; //TODO: remove that later
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    if (this.subTurn == 1) {
+      this.amoves.pop();
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+    }
+    if (move.subTurn == 1) this.firstMove.pop();
+    this.subTurn = move.subTurn;
+    this.toOldKingPos(move);
+  }
+
+  toOldKingPos(move) {
+    // (Potentially) Reset king position
+    for (let v of move.vanish)
+      if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
+  }
+
+  getComputerMove() {
+    let moves = this.getAllValidMoves();
+    if (moves.length == 0) return null;
+    // "Search" at depth 1 for now
+    const maxeval = V.INFINITY;
+    const color = this.turn;
+    const emptyMove = {
+      start: { x: -1, y: -1 },
+      end: { x: -1, y: -1 },
+      appear: [],
+      vanish: []
+    };
+    moves.forEach(m => {
+      this.play(m);
+      if (this.turn != color) m.eval = this.evalPosition();
+      else {
+        m.eval = (color == "w" ? -1 : 1) * maxeval;
+        const moves2 = this.getAllValidMoves().concat([emptyMove]);
+        m.next = moves2[0];
+        moves2.forEach(m2 => {
+          this.play(m2);
+          const score = this.getCurrentScore();
+          let mvEval = 0;
+          if (score != "1/2") {
+            if (score != "*") mvEval = (score == "1-0" ? 1 : -1) * maxeval;
+            else mvEval = this.evalPosition();
+          }
+          if (
+            (color == 'w' && mvEval > m.eval) ||
+            (color == 'b' && mvEval < m.eval)
+          ) {
+            m.eval = mvEval;
+            m.next = m2;
+          }
+          this.undo(m2);
+        });
+      }
+      this.undo(m);
+    });
+    moves.sort((a, b) => {
+      return (color == "w" ? 1 : -1) * (b.eval - a.eval);
+    });
+    let candidates = [0];
+    for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
+      candidates.push(i);
+    const mIdx = candidates[randInt(candidates.length)];
+    if (!moves[mIdx].next) return moves[mIdx];
+    const move2 = moves[mIdx].next;
+    delete moves[mIdx]["next"];
+    return [moves[mIdx], move2];
+  }
+
+  getNotation(move) {
+    if (move.start.x < 0)
+      // A second move is always required, but may be empty
+      return "-";
+    const initialSquare = V.CoordsToSquare(move.start);
+    const finalSquare = V.CoordsToSquare(move.end);
+    if (move.appear.length == 0)
+      // Pushed or pulled out of the board
+      return initialSquare + "R";
+    return move.appear[0].p.toUpperCase() + initialSquare + finalSquare;
+  }
+
+};
diff --git a/variants/Dynamo/complete_rules.html b/variants/Dynamo/complete_rules.html
new file mode 100644
index 0000000..68e9cc0
--- /dev/null
+++ b/variants/Dynamo/complete_rules.html
@@ -0,0 +1,142 @@
+<html>
+<head>
+  <title>Dynamo Rules</title>
+  <link href="/common.css" rel="stylesheet"/>
+  <link href="/variants/Dynamo/style.css" rel="stylesheet"/>
+</head>
+<body>
+<div class="full-rules">
+<h1>Dynamo Rules</h1>
+
+<p>
+  Pieces have the same movement as in orthodox chess, but they cannot
+  take other pieces in the usual way. Instead of the normal captures, pieces
+  can pull or push other pieces, potentially off the board.
+  The goal is to send the enemy king off the board.
+</p>
+
+<p>Each turn, a player has the following options:</p>
+<ul>
+  <li>
+    Move one of his pieces normally, then optionally pull something as an
+    effect of this move.
+  </li>
+  <li>
+    Push any piece with one of his pieces, then optionally follow the pushed
+    piece.
+  </li>
+
+<p>
+  It seems easier to understand with some examples. For a detailed
+  introduction please visit
+  <a href="https://echekk.fr/spip.php?page=article&id_article=599">
+    this page
+  </a> (in French).
+</p>
+
+<figure>
+  <div class="diag"
+       data-fen='rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR ...'
+       data-mks="e1,e3,e4,c3,f3,g4,h5,d3,c4,b5,a6">
+  </div>
+  <figcaption>Possible "pawn moves" in the initial position.</figcaption>
+</figure>
+
+<p>
+  The e2 pawn can move to e3 and e4 as usual. It can also slide diagonally,
+  being pushed by the bishop or the queen (which may or may not move along
+  this line afterward). It can also go to c3, being pushed by the knight from
+  g1; then the knight can move to e2, or stay motionless.
+  Finally, the pawn can "take the king": this is a special move indicating that
+  you want it to exit the board. Indeed it could be pushed off the board by the
+  bishop or the queen.
+</p>
+
+<p>
+  Note: if an action is possible but you don't want to play a second part in
+  a move, click on any empty square: this will send an empty move.
+</p>
+
+<figure>
+  <div class="diag left"
+       data-fen='rnbqkbnr/ppp1pppp/8/3p4/8/2N5/PPPPPPPP/R1BQKBNR ...'>
+  </div>
+  <div class="diag right"
+       data-fen='rnbqkbnr/ppp1pppp/8/8/8/2p5/PPPPPPPP/RNBQKBNR ...'>
+  </div>
+  <figcaption>
+    Pulling the d5 pawn to c3 (left: before, right: after).
+  </figcaption>
+</figure>
+
+<ul>
+  <li>Pawns cannot pull (because they only move forward).</li>
+  <li>
+    When they could reach the square beyond the edge,
+    pieces can exit the board by themselves, possibly dragging another piece
+    out (friendly or enemy).
+  </li>
+</ul>
+
+<figure>
+  <div class="diag"
+       data-fen='rnb1qbnr/pppkpppp/3p4/8/Q1P5/5NP1/PP1PPP1P/RNB1KB1R ...'>
+  </div>
+  <figcaption>
+    Check: the queen threatens to pull the king off the board
+    along the a4-e8 diagonal.
+  </figcaption>
+</figure>
+
+<p>
+  It is forbidden to undo a "move + action". For example here, white could
+  push back the black bishop on g7 but not return to d4 then.
+</p>
+
+<figure>
+  <div class="diag left"
+       data-fen='rnbqk1nr/ppppppbp/6p1/8/3B4/1P6/P1PPPPPP/RN1QKBNR ...'>
+  </div>
+  <div class="diag right"
+       data-fen='rnbqk1nr/pppppp1p/6p1/8/3b4/1P6/PBPPPPPP/RN1QKBNR ...'>
+  <figcaption>
+    Pushing the d4 bishop to b2 (left: before, right: after).
+  </figcaption>
+</figure>
+
+<p>
+  Castling is possible as long as the king and rook have not moved and
+  haven't been pushed or pulled (this differs from the chessvariants
+  description).
+</p>
+
+<h3>End of the game</h3>
+
+<p>
+  The game ends when a push or pull action threatens to send the king off the
+  board, and he has no way to escape it.
+</p>
+
+<figure>
+  <div class="diag"
+       data-fen='8/4B3/8/8/6Qk/8/4N3/K7 ...'>
+  </div>
+  <figcaption>Dynamo checkmate ("Dynamate" :) )</figcaption>
+</figure>
+
+<p>
+  The king cannot "take" on g4: this would just push the queen one step to the
+  left, and she would then push the king beyond the 'h' file.
+  There are no en-passant captures.
+</p>
+
+<h3>Source</h3>
+
+<p>
+  <a href="https://www.chessvariants.com/mvopponent.dir/dynamo.html">
+    Dynamo chess
+  </a>
+  on chessvariants.com. The short description given on 
+  <a href="http://www.pion.ch/echecs/variante.php?jeu=dynamo">this page</a>
+  might help too.
+</p>
diff --git a/variants/Dynamo/rules.html b/variants/Dynamo/rules.html
new file mode 100644
index 0000000..4a78e5a
--- /dev/null
+++ b/variants/Dynamo/rules.html
@@ -0,0 +1,24 @@
+<p>
+  Moves are potentially played in two times:
+  move a piece, and / or push or pull something with that unit.
+</p>
+
+<p>Each turn, a player has the following options:</p>
+<ul>
+  <li>
+    Move one of his pieces normally,
+    then optionally pull something as an effect of this move.
+  </li>
+  <li>
+    Push any piece with one of his pieces,
+    then optionally follow the pushed piece.
+  </li>
+</ul>
+
+<p>
+  <a target="_blank" href="/variants/Dynamo/complete_rules.html">
+    Full rules description.
+  </a>
+</p>
+
+<p class="author">Hans Kluever and Peter Kahl (1968).</p>
diff --git a/variants/Dynamo/style.css b/variants/Dynamo/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Dynamo/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
-- 
2.48.1


From 939e06bf9febef0a7935c7f6a58c5e28dee4dedc Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Fri, 5 Jan 2024 09:58:16 +0100
Subject: [PATCH 09/16] Start working on Dynamo

---
 base_rules.js            |  22 ++-
 variants/Dynamo/class.js | 289 +++++----------------------------------
 2 files changed, 52 insertions(+), 259 deletions(-)

diff --git a/base_rules.js b/base_rules.js
index 9a364e8..3d8e463 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -175,6 +175,22 @@ export default class ChessRules {
     return Object.values(cd).map(c => c.toString(36)).join("");
   }
 
+  // c10 --> 02 (assuming 10 rows)
+  static SquareFromUsual(sq) {
+    return (
+      (this.size.x - parseInt(sq.substring(1), 10)).toString(36) +
+      (sq.charCodeAt(0) - 97).toString(36)
+    );
+  }
+
+  // 02 --> c10
+  static UsualFromSquare(sq) {
+    return (
+      String.fromCharCode(parseInt(sq.charAt(1), 36) + 97) +
+      (this.size.x - parseInt(sq.charAt(0), 36)).toString(10)
+    );
+  }
+
   coordsToId(cd) {
     if (typeof cd.x == "number") {
       return (
@@ -651,10 +667,8 @@ export default class ChessRules {
         this[arrName] = ArrayFun.init(this.size.x, this.size.y, null);
       if (arrName == "d_pieces")
         this.marks.forEach((m) => {
-          const formattedSquare =
-            (this.size.x - parseInt(m.substring(1), 10)).toString(36) +
-            (m.charCodeAt(0) - 97).toString(36);
-          const mCoords = V.SquareToCoords(formattedSquare);
+          const formattedSquare = C.SquareFromUsual(m);
+          const mCoords = C.SquareToCoords(formattedSquare);
           addPiece(mCoords.x, mCoords.y, arrName, "mark");
         });
     };
diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index 0996a70..05f2aed 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -2,110 +2,51 @@ import ChessRules from "/base_rules.js";
 
 export default class DynamoRules extends ChessRules {
 
-  // TODO? later, allow to push out pawns on a and h files
-  get hasEnpassant() {
-    return false;
+  static get Options() {
+    // TODO
   }
 
-/// TODO:::
+  get hasEnpassant() {
+    return this.options["enpassant"];
+  }
 
-  canIplay(side, [x, y]) {
+  canIplay(x, y) {
     // Sometimes opponent's pieces can be moved directly
-    return this.turn == side;
+    return this.playerColor == this.turn;
+  }
+
+  canTake() {
+    // Captures don't occur (only pulls & pushes)
+    return false;
   }
 
-  setOtherVariables(fen) {
-    super.setOtherVariables(fen);
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
     this.subTurn = 1;
-    // Local stack of "action moves"
-    this.amoves = [];
-    const amove = V.ParseFen(fen).amove;
-    if (amove != "-") {
-      const amoveParts = amove.split("/");
-      let move = {
-        // No need for start & end
-        appear: [],
-        vanish: []
-      };
-      [0, 1].map(i => {
-        if (amoveParts[i] != "-") {
-          amoveParts[i].split(".").forEach(av => {
-            // Format is "bpe3"
-            const xy = V.SquareToCoords(av.substr(2));
-            move[i == 0 ? "appear" : "vanish"].push(
-              new PiPo({
-                x: xy.x,
-                y: xy.y,
-                c: av[0],
-                p: av[1]
-              })
-            );
-          });
-        }
+    // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5
+    // for example, and moving herself to g4. If just move: e2h5
+    this.lastAction = [];
+    if (fenParsed.amove != '-') {
+      this.lastAction = fenParsed.amove.split('/').map(a => {
+        return {
+          c1: C.SquareToCoords(C.SquareFromUsual(a.substr(0, 2))),
+          c2: C.SquareToCoords(C.SquareFromUsual(a.substr(2, 2)))
+        };
       });
-      this.amoves.push(move);
     }
-    // Stack "first moves" (on subTurn 1) to merge and check opposite moves
-    this.firstMove = [];
-  }
-
-  static ParseFen(fen) {
-    return Object.assign(
-      ChessRules.ParseFen(fen),
-      { amove: fen.split(" ")[4] }
-    );
   }
 
-  static IsGoodFen(fen) {
-    if (!ChessRules.IsGoodFen(fen)) return false;
-    const fenParts = fen.split(" ");
-    if (fenParts.length != 5) return false;
-    if (fenParts[4] != "-") {
-      // TODO: a single regexp instead.
-      // Format is [bpa2[.wpd3]] || '-'/[bbc3[.wrd5]] || '-'
-      const amoveParts = fenParts[4].split("/");
-      if (amoveParts.length != 2) return false;
-      for (let part of amoveParts) {
-        if (part != "-") {
-          for (let psq of part.split("."))
-            if (!psq.match(/^[a-z]{3}[1-8]$/)) return false;
-        }
-      }
+  getPartFen(o) {
+    let res = super.getPartFen(o);
+    if (o.init)
+      res["amove"] = '-';
+    else {
+      res["amove"] = this.lastAction.map(a => {
+        C.UsualFromSquare(C.CoordsToSquare(a.c1)) +
+        C.UsualFromSquare(C.CoordsToSquare(a.c2))
+      }).join('/');
     }
-    return true;
-  }
-
-  getFen() {
-    return super.getFen() + " " + this.getAmoveFen();
-  }
-
-  getFenForRepeat() {
-    return super.getFenForRepeat() + "_" + this.getAmoveFen();
-  }
-
-  getAmoveFen() {
-    const L = this.amoves.length;
-    if (L == 0) return "-";
-    return (
-      ["appear","vanish"].map(
-        mpart => {
-          if (this.amoves[L-1][mpart].length == 0) return "-";
-          return (
-            this.amoves[L-1][mpart].map(
-              av => {
-                const square = V.CoordsToSquare({ x: av.x, y: av.y });
-                return av.c + av.p + square;
-              }
-            ).join(".")
-          );
-        }
-      ).join("/")
-    );
-  }
-
-  canTake() {
-    // Captures don't occur (only pulls & pushes)
-    return false;
+    return res;
   }
 
   // Step is right, just add (push/pull) moves in this direction
@@ -590,6 +531,7 @@ export default class DynamoRules extends ChessRules {
     );
   }
 
+  // TODO: just stack in this.lastAction instead
   getAmove(move1, move2) {
     // Just merge (one is action one is move, one may be empty)
     return {
@@ -643,105 +585,6 @@ export default class DynamoRules extends ChessRules {
     );
   }
 
-  isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
-    for (let step of steps) {
-      let rx = x + step[0],
-          ry = y + step[1];
-      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
-        rx += step[0];
-        ry += step[1];
-      }
-      if (
-        V.OnBoard(rx, ry) &&
-        this.getPiece(rx, ry) == piece &&
-        this.getColor(rx, ry) == color
-      ) {
-        // Continue some steps in the same direction (pull)
-        rx += step[0];
-        ry += step[1];
-        while (
-          V.OnBoard(rx, ry) &&
-          this.board[rx][ry] == V.EMPTY &&
-          !oneStep
-        ) {
-          rx += step[0];
-          ry += step[1];
-        }
-        if (!V.OnBoard(rx, ry)) return true;
-        // Step in the other direction (push)
-        rx = x - step[0];
-        ry = y - step[1];
-        while (
-          V.OnBoard(rx, ry) &&
-          this.board[rx][ry] == V.EMPTY &&
-          !oneStep
-        ) {
-          rx -= step[0];
-          ry -= step[1];
-        }
-        if (!V.OnBoard(rx, ry)) return true;
-      }
-    }
-    return false;
-  }
-
-  isAttackedByPawn([x, y], color) {
-    // The king can be pushed out by a pawn on last rank or near the edge
-    const pawnShift = (color == "w" ? 1 : -1);
-    for (let i of [-1, 1]) {
-      if (
-        V.OnBoard(x + pawnShift, y + i) &&
-        this.board[x + pawnShift][y + i] != V.EMPTY &&
-        this.getPiece(x + pawnShift, y + i) == V.PAWN &&
-        this.getColor(x + pawnShift, y + i) == color
-      ) {
-        if (!V.OnBoard(x - pawnShift, y - i)) return true;
-      }
-    }
-    return false;
-  }
-
-  static OnTheEdge(x, y) {
-    return (x == 0 || x == 7 || y == 0 || y == 7);
-  }
-
-  isAttackedByKing([x, y], color) {
-    // Attacked if I'm on the edge and the opponent king just next,
-    // but not on the edge.
-    if (V.OnTheEdge(x, y)) {
-      for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
-        const [i, j] = [x + step[0], y + step[1]];
-        if (
-          V.OnBoard(i, j) &&
-          !V.OnTheEdge(i, j) &&
-          this.board[i][j] != V.EMPTY &&
-          this.getPiece(i, j) == V.KING
-          // NOTE: since only one king of each color, and (x, y) is occupied
-          // by our king, no need to check other king's color.
-        ) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  // No consideration of color: all pieces could be played
-  getAllPotentialMoves() {
-    let potentialMoves = [];
-    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) {
-          Array.prototype.push.apply(
-            potentialMoves,
-            this.getPotentialMovesFrom([i, j])
-          );
-        }
-      }
-    }
-    return potentialMoves;
-  }
-
   getEmptyMove() {
     return new Move({
       start: { x: -1, y: -1 },
@@ -854,68 +697,4 @@ export default class DynamoRules extends ChessRules {
       if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
   }
 
-  getComputerMove() {
-    let moves = this.getAllValidMoves();
-    if (moves.length == 0) return null;
-    // "Search" at depth 1 for now
-    const maxeval = V.INFINITY;
-    const color = this.turn;
-    const emptyMove = {
-      start: { x: -1, y: -1 },
-      end: { x: -1, y: -1 },
-      appear: [],
-      vanish: []
-    };
-    moves.forEach(m => {
-      this.play(m);
-      if (this.turn != color) m.eval = this.evalPosition();
-      else {
-        m.eval = (color == "w" ? -1 : 1) * maxeval;
-        const moves2 = this.getAllValidMoves().concat([emptyMove]);
-        m.next = moves2[0];
-        moves2.forEach(m2 => {
-          this.play(m2);
-          const score = this.getCurrentScore();
-          let mvEval = 0;
-          if (score != "1/2") {
-            if (score != "*") mvEval = (score == "1-0" ? 1 : -1) * maxeval;
-            else mvEval = this.evalPosition();
-          }
-          if (
-            (color == 'w' && mvEval > m.eval) ||
-            (color == 'b' && mvEval < m.eval)
-          ) {
-            m.eval = mvEval;
-            m.next = m2;
-          }
-          this.undo(m2);
-        });
-      }
-      this.undo(m);
-    });
-    moves.sort((a, b) => {
-      return (color == "w" ? 1 : -1) * (b.eval - a.eval);
-    });
-    let candidates = [0];
-    for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
-      candidates.push(i);
-    const mIdx = candidates[randInt(candidates.length)];
-    if (!moves[mIdx].next) return moves[mIdx];
-    const move2 = moves[mIdx].next;
-    delete moves[mIdx]["next"];
-    return [moves[mIdx], move2];
-  }
-
-  getNotation(move) {
-    if (move.start.x < 0)
-      // A second move is always required, but may be empty
-      return "-";
-    const initialSquare = V.CoordsToSquare(move.start);
-    const finalSquare = V.CoordsToSquare(move.end);
-    if (move.appear.length == 0)
-      // Pushed or pulled out of the board
-      return initialSquare + "R";
-    return move.appear[0].p.toUpperCase() + initialSquare + finalSquare;
-  }
-
 };
-- 
2.48.1


From 253e65f6c4f342e5ac8230d7340ed413354f9c7f Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Fri, 19 Jan 2024 17:53:42 +0100
Subject: [PATCH 10/16] New variant idea

---
 TODO | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/TODO b/TODO
index 8afe466..f49a584 100644
--- a/TODO
+++ b/TODO
@@ -4,3 +4,7 @@ Otage, Emergo, Pacosako : fonction "buildPiece(arg1, arg2)" returns HTML element
 
 https://fr.wikipedia.org/wiki/Unlur
 Yoxii ?
+
+Idée new variant: "bed" random moves and "capture" (opponent?) pieces which cannot move on next turn (sleeping). (Crazybed ?)
+one "bed" per player.
+one turn = place bed (potentially with opponent piece), then normal move, then random bed move - dropping potential piece on initial square (awaken).
-- 
2.48.1


From d4be1a764e43f208ef34ee5c7298249b98e0baf1 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 28 Jan 2025 18:50:55 +0100
Subject: [PATCH 11/16] Work on Dynamo

---
 variants/Dynamo/class.js | 227 +++++++++++++++++++--------------------
 1 file changed, 113 insertions(+), 114 deletions(-)

diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index 05f2aed..ed0ea27 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -3,7 +3,11 @@ import ChessRules from "/base_rules.js";
 export default class DynamoRules extends ChessRules {
 
   static get Options() {
-    // TODO
+    return {
+      select: C.Options.select,
+      input: [],
+      styles: ["cylinder", "doublemove", "progressive"]
+    };
   }
 
   get hasEnpassant() {
@@ -51,7 +55,7 @@ export default class DynamoRules extends ChessRules {
 
   // Step is right, just add (push/pull) moves in this direction
   // Direction is assumed normalized.
-  getMovesInDirection([x, y], [dx, dy], nbSteps) {
+  getMovesInDirection([x, y], [dx, dy], nbSteps, kp) {
     nbSteps = nbSteps || 8; //max 8 steps anyway
     let [i, j] = [x + dx, y + dy];
     let moves = [];
@@ -59,25 +63,27 @@ export default class DynamoRules extends ChessRules {
     const piece = this.getPiece(x, y);
     const lastRank = (color == 'w' ? 0 : 7);
     let counter = 1;
-    while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
-      if (i == lastRank && piece == V.PAWN) {
+    while (this.onBoard(i, j) && this.board[i][j] == "") {
+      if (i == lastRank && piece == 'p') {
         // Promotion by push or pull
-        V.PawnSpecs.promotions.forEach(p => {
+        this.pawnPromotions.forEach(p => {
           let move = super.getBasicMove([x, y], [i, j], { c: color, p: p });
           moves.push(move);
         });
       }
-      else moves.push(super.getBasicMove([x, y], [i, j]));
-      if (++counter > nbSteps) break;
+      else
+        moves.push(super.getBasicMove([x, y], [i, j]));
+      if (++counter > nbSteps)
+        break;
       i += dx;
       j += dy;
     }
-    if (!V.OnBoard(i, j) && piece != V.KING) {
+    if (!this.onBoard(i, j) && piece != 'k') {
       // Add special "exit" move, by "taking king"
       moves.push(
         new Move({
           start: { x: x, y: y },
-          end: { x: this.kingPos[color][0], y: this.kingPos[color][1] },
+          end: { x: kp[0], y: kp[1] },
           appear: [],
           vanish: [{ x: x, y: y, c: color, p: piece }]
         })
@@ -109,14 +115,14 @@ export default class DynamoRules extends ChessRules {
     const checkSlider = () => {
       const dir = this.getNormalizedDirection([x2 - x1, y2 - y1]);
       let [i, j] = [x1 + dir[0], y1 + dir[1]];
-      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+      while (this.onBoard(i, j) && this.board[i][j] == "") {
         i += dir[0];
         j += dir[1];
       }
-      return !V.OnBoard(i, j);
+      return !this.onBoard(i, j);
     };
     switch (piece2 || this.getPiece(x1, y1)) {
-      case V.PAWN:
+      case 'p':
         return (
           x1 + pawnShift == x2 &&
           (
@@ -124,30 +130,33 @@ export default class DynamoRules extends ChessRules {
             (
               color1 != color2 &&
               deltaY == 1 &&
-              !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+              !this.onBoard(2 * x2 - x1, 2 * y2 - y1)
             )
           )
         );
-      case V.ROOK:
-        if (x1 != x2 && y1 != y2) return false;
+      case 'r':
+        if (x1 != x2 && y1 != y2)
+          return false;
         return checkSlider();
-      case V.KNIGHT:
+      case 'n':
         return (
           deltaX + deltaY == 3 &&
           (deltaX == 1 || deltaY == 1) &&
-          !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+          !this.onBoard(2 * x2 - x1, 2 * y2 - y1)
         );
-      case V.BISHOP:
-        if (deltaX != deltaY) return false;
+      case 'b':
+        if (deltaX != deltaY)
+          return false;
         return checkSlider();
-      case V.QUEEN:
-        if (deltaX != 0 && deltaY != 0 && deltaX != deltaY) return false;
+      case 'q':
+        if (deltaX != 0 && deltaY != 0 && deltaX != deltaY)
+          return false;
         return checkSlider();
-      case V.KING:
+      case 'k':
         return (
           deltaX <= 1 &&
           deltaY <= 1 &&
-          !V.OnBoard(2 * x2 - x1, 2 * y2 - y1)
+          !this.onBoard(2 * x2 - x1, 2 * y2 - y1)
         );
     }
     return false;
@@ -158,12 +167,12 @@ export default class DynamoRules extends ChessRules {
     const deltaX = Math.abs(x1 - x2);
     const startRank = (this.getColor(x1, y1) == 'w' ? 6 : 1);
     return (
-      [V.QUEEN, V.ROOK].includes(piece) ||
+      ['q', 'r'].includes(piece) ||
       (
-        [V.KING, V.PAWN].includes(piece) &&
+        ['k', 'p'].includes(piece) &&
         (
           deltaX == 1 ||
-          (deltaX == 2 && piece == V.PAWN && x1 == startRank)
+          (deltaX == 2 && piece == 'p' && x1 == startRank)
         )
       )
     );
@@ -178,12 +187,13 @@ export default class DynamoRules extends ChessRules {
     const pawnShift = (color == 'w' ? -1 : 1);
     const pawnStartRank = (color == 'w' ? 6 : 1);
     const getMoveHash = (m) => {
-      return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end);
+      return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end);
     };
     if (this.subTurn == 1) {
+      const kp = this.searchKingPos(color);
       const addMoves = (dir, nbSteps) => {
         const newMoves =
-          this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps)
+          this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp)
           .filter(m => !movesHash[getMoveHash(m)]);
         newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; });
         Array.prototype.push.apply(moves, newMoves);
@@ -198,7 +208,8 @@ export default class DynamoRules extends ChessRules {
       moves = moves.filter(m => {
         const suicide = (m.appear.length == 0);
         if (suicide) {
-          if (hasExit) return false;
+          if (hasExit)
+            return false;
           hasExit = true;
         }
         return true;
@@ -207,32 +218,34 @@ export default class DynamoRules extends ChessRules {
       let movesHash = {};
       moves.forEach(m => { movesHash[getMoveHash(m)] = true; });
       // [x, y] is pushed by 'color'
-      for (let step of V.steps[V.KNIGHT]) {
+      for (let step of this.pieces()['n'].both[0].steps) {
         const [i, j] = [x + step[0], y + step[1]];
         if (
-          V.OnBoard(i, j) &&
-          this.board[i][j] != V.EMPTY &&
+          this.onBoard(i, j) &&
+          this.board[i][j] != "" &&
           this.getColor(i, j) == color &&
-          this.getPiece(i, j) == V.KNIGHT
+          this.getPiece(i, j) == 'n'
         ) {
           addMoves(step, 1);
         }
       }
-      for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+      for (let step of this.pieces()['r'].both[0].steps.concat(
+                       this.pieces()['b'].both[0].steps))
+      {
         let [i, j] = [x + step[0], y + step[1]];
-        while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        while (this.onBoard(i, j) && this.board[i][j] == "") {
           i += step[0];
           j += step[1];
         }
         if (
-          V.OnBoard(i, j) &&
-          this.board[i][j] != V.EMPTY &&
+          this.onBoard(i, j) &&
+          this.board[i][j] != "" &&
           this.getColor(i, j) == color
         ) {
           const deltaX = Math.abs(i - x);
           const deltaY = Math.abs(j - y);
           switch (this.getPiece(i, j)) {
-            case V.PAWN:
+            case 'p':
               if (
                 (x - i) / deltaX == pawnShift &&
                 deltaX <= 2 &&
@@ -248,18 +261,21 @@ export default class DynamoRules extends ChessRules {
                   addMoves(step, 1);
               }
               break;
-            case V.ROOK:
-              if (deltaX == 0 || deltaY == 0) addMoves(step);
+            case 'r':
+              if (deltaX == 0 || deltaY == 0)
+                addMoves(step);
               break;
-            case V.BISHOP:
-              if (deltaX == deltaY) addMoves(step);
+            case 'b':
+              if (deltaX == deltaY)
+                addMoves(step);
               break;
-            case V.QUEEN:
+            case 'q':
               // All steps are valid for a queen:
               addMoves(step);
               break;
-            case V.KING:
-              if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1);
+            case 'k':
+              if (deltaX <= 1 && deltaY <= 1)
+                addMoves(step, 1);
               break;
           }
         }
@@ -357,13 +373,16 @@ export default class DynamoRules extends ChessRules {
             }
             break;
           case V.BISHOP:
-            if (deltaX != deltaY) return [];
+            if (deltaX != deltaY)
+              return [];
             break;
           case V.ROOK:
-            if (deltaX != 0 && deltaY != 0) return [];
+            if (deltaX != 0 && deltaY != 0)
+              return [];
             break;
           case V.QUEEN:
-            if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return [];
+            if (deltaX != deltaY && deltaX != 0 && deltaY != 0)
+              return [];
             break;
         }
         // Nothing should stand between [x, y] and the square fm.start
@@ -437,12 +456,14 @@ export default class DynamoRules extends ChessRules {
     };
     if (fm.vanish[0].c != color) {
       // Only possible action is a push:
-      if (fm.appear.length == 0) return getPushExit();
+      if (fm.appear.length == 0)
+        return getPushExit();
       return getPushMoves();
     }
     else if (sqCol != color) {
       // Only possible action is a pull, considering moving piece abilities
-      if (fm.appear.length == 0) return getPullExit();
+      if (fm.appear.length == 0)
+        return getPullExit();
       return getPullMoves();
     }
     else {
@@ -474,7 +495,8 @@ export default class DynamoRules extends ChessRules {
       let j = y + step[1];
       while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
         moves.push(this.getBasicMove([x, y], [i, j]));
-        if (oneStep) continue outerLoop;
+        if (oneStep)
+          continue outerLoop;
         i += step[0];
         j += step[1];
       }
@@ -516,7 +538,8 @@ export default class DynamoRules extends ChessRules {
             elt.p == av.p
           );
         });
-        if (!avInAv2) return false;
+        if (!avInAv2)
+          return false;
       }
       return true;
     };
@@ -540,6 +563,7 @@ export default class DynamoRules extends ChessRules {
     }
   }
 
+  // TODO ::
   filterValid(moves) {
     const color = this.turn;
     const La = this.amoves.length;
@@ -572,7 +596,8 @@ export default class DynamoRules extends ChessRules {
         return !res;
       });
     }
-    if (La == 0) return super.filterValid(moves);
+    if (La == 0)
+      return super.filterValid(moves);
     const Lf = this.firstMove.length;
     return (
       super.filterValid(
@@ -598,7 +623,8 @@ export default class DynamoRules extends ChessRules {
     // A click to promote a piece on subTurn 2 would trigger this.
     // For now it would then return [NaN, NaN] because surrounding squares
     // have no IDs in the promotion modal. TODO: improve this?
-    if (isNaN(square[0])) return null;
+    if (isNaN(square[0]))
+      return null;
     // If subTurn == 2 && square is empty && !underCheck && !isOpposite,
     // then return an empty move, allowing to "pass" subTurn2
     const La = this.amoves.length;
@@ -614,87 +640,60 @@ export default class DynamoRules extends ChessRules {
     return null;
   }
 
-  play(move) {
-    if (this.subTurn == 1 && move.vanish.length == 0) {
-      // Patch to work with old format: (TODO: remove later)
-      move.ignore = true;
-      return;
+  updateCastleFlags(move) {
+    if (move.start.x < 0)
+      return; //empty move (pass subTurn 2)
+    const firstRank = { 'w': V.size.x - 1, 'b': 0 };
+    for (let v of move.vanish) {
+      if (v.p == 'k')
+        this.castleFlags[v.c] = [this.size.y, this.size.y];
+      else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) {
+        const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1);
+        this.castleFlags[v.c][flagIdx] = this.size.y;
+      }
     }
+  }
+
+  play(move) {
+//    if (this.subTurn == 1 && move.vanish.length == 0) {
+//      // Patch to work with old format: (TODO: remove later)
+//      move.ignore = true;
+//      return;
+//    }
+
+    // In preplay ?
+    this.updateCastleFlags(move);
+
+    const oppCol = C.GetOppTurn(color);
+
     const color = this.turn;
-    move.subTurn = this.subTurn; //for undo
     const gotoNext = (mv) => {
       const L = this.firstMove.length;
       this.amoves.push(this.getAmove(this.firstMove[L-1], mv));
-      this.turn = V.GetOppCol(color);
+      this.turn = oppCol;
       this.subTurn = 1;
       this.movesCount++;
     };
-    move.flags = JSON.stringify(this.aggregateFlags());
-    V.PlayOnBoard(this.board, move);
-    if (this.subTurn == 2) gotoNext(move);
+    this.playOnBoard(move);
+    if (this.subTurn == 2)
+      gotoNext(move);
     else {
       this.subTurn = 2;
       this.firstMove.push(move);
-      this.toNewKingPos(move);
       if (
-        // Condition is true on empty arrays:
+        // Condition is true on empty arrays: //TODO: getAllPotentialMoves doesn't exist
         this.getAllPotentialMoves().every(m => {
-          V.PlayOnBoard(this.board, m);
-          this.toNewKingPos(m);
-          const res = this.underCheck(color);
-          V.UndoOnBoard(this.board, m);
-          this.toOldKingPos(m);
+          this.playOnBoard(m);
+          const res = this.underCheck([kp], oppCol); //TODO: find kp first
+          this.undoOnBoard(m);
           return res;
         })
       ) {
         // No valid move at subTurn 2
         gotoNext(this.getEmptyMove());
       }
-      this.toOldKingPos(move);
     }
     this.postPlay(move);
   }
 
-  toNewKingPos(move) {
-    for (let a of move.appear)
-      if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y];
-  }
-
-  postPlay(move) {
-    if (move.start.x < 0) return;
-    this.toNewKingPos(move);
-    this.updateCastleFlags(move);
-  }
-
-  updateCastleFlags(move) {
-    const firstRank = { 'w': V.size.x - 1, 'b': 0 };
-    for (let v of move.vanish) {
-      if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y];
-      else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) {
-        const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1);
-        this.castleFlags[v.c][flagIdx] = V.size.y;
-      }
-    }
-  }
-
-  undo(move) {
-    if (!!move.ignore) return; //TODO: remove that later
-    this.disaggregateFlags(JSON.parse(move.flags));
-    V.UndoOnBoard(this.board, move);
-    if (this.subTurn == 1) {
-      this.amoves.pop();
-      this.turn = V.GetOppCol(this.turn);
-      this.movesCount--;
-    }
-    if (move.subTurn == 1) this.firstMove.pop();
-    this.subTurn = move.subTurn;
-    this.toOldKingPos(move);
-  }
-
-  toOldKingPos(move) {
-    // (Potentially) Reset king position
-    for (let v of move.vanish)
-      if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
-  }
-
 };
-- 
2.48.1


From ab15cf71bdd9be67bfa0df1d7137ff2a6de7139f Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 30 Jan 2025 12:00:00 +0100
Subject: [PATCH 12/16] Some further transformation on Dynamo, prepare next
 variants too

---
 variants/Dynamo/class.js                 |   41 +-
 variants/Eightpieces/class.js            | 1136 ++++++++++++++++++++++
 variants/Eightpieces/complete_rules.html |   48 +
 variants/Eightpieces/rules.html          |    3 +
 variants/Eightpieces/style.css           |    1 +
 variants/Emergo/class.js                 |  576 +++++++++++
 variants/Empire/class.js                 |  432 ++++++++
 variants/Enpassant/class.js              |  209 ++++
 variants/Evolution/class.js              |   34 +
 variants/Extinction/class.js             |  118 +++
 variants/Fanorona/class.js               |  342 +++++++
 variants/Sleepy/class.js                 |  110 +++
 variants/Sleepy/rules.html               |    7 +
 variants/Sleepy/style.css                |    1 +
 14 files changed, 3038 insertions(+), 20 deletions(-)
 create mode 100644 variants/Eightpieces/class.js
 create mode 100644 variants/Eightpieces/complete_rules.html
 create mode 100644 variants/Eightpieces/rules.html
 create mode 100644 variants/Eightpieces/style.css
 create mode 100644 variants/Emergo/class.js
 create mode 100644 variants/Empire/class.js
 create mode 100644 variants/Enpassant/class.js
 create mode 100644 variants/Evolution/class.js
 create mode 100644 variants/Extinction/class.js
 create mode 100644 variants/Fanorona/class.js
 create mode 100644 variants/Sleepy/class.js
 create mode 100644 variants/Sleepy/rules.html
 create mode 100644 variants/Sleepy/style.css

diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index ed0ea27..7b835e0 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -307,7 +307,7 @@ export default class DynamoRules extends ChessRules {
         const dir = this.getNormalizedDirection(
           [fm.start.x - x, fm.start.y - y]);
         const nbSteps =
-          [V.PAWN, V.KING, V.KNIGHT].includes(piece)
+          ['p', 'k', 'n'].includes(piece)
             ? 1
             : null;
         return this.getMovesInDirection([x, y], dir, nbSteps);
@@ -327,7 +327,7 @@ export default class DynamoRules extends ChessRules {
         const deltaX = Math.abs(fm.start.x - x);
         const deltaY = Math.abs(fm.start.y - y);
         switch (piece) {
-          case V.PAWN:
+          case 'p':
             if (x == pawnStartRank) {
               if (
                 (fm.start.x - x) * pawnShift < 0 ||
@@ -354,7 +354,7 @@ export default class DynamoRules extends ChessRules {
               }
             }
             break;
-          case V.KNIGHT:
+          case 'n':
             if (
               (deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) ||
               (fm.end.x - fm.start.x != fm.start.x - x) ||
@@ -363,7 +363,7 @@ export default class DynamoRules extends ChessRules {
               return [];
             }
             break;
-          case V.KING:
+          case 'k':
             if (
               (deltaX >= 2 || deltaY >= 2) ||
               (fm.end.x - fm.start.x != fm.start.x - x) ||
@@ -372,15 +372,15 @@ export default class DynamoRules extends ChessRules {
               return [];
             }
             break;
-          case V.BISHOP:
+          case 'b':
             if (deltaX != deltaY)
               return [];
             break;
-          case V.ROOK:
+          case 'r':
             if (deltaX != 0 && deltaY != 0)
               return [];
             break;
-          case V.QUEEN:
+          case 'q':
             if (deltaX != deltaY && deltaX != 0 && deltaY != 0)
               return [];
             break;
@@ -404,7 +404,7 @@ export default class DynamoRules extends ChessRules {
       // Note: kings cannot suicide, so fm.vanish[0].p is not KING.
       // Could be PAWN though, if a pawn was pushed out of board.
       if (
-        fm.vanish[0].p != V.PAWN && //pawns cannot pull
+        fm.vanish[0].p != 'p' && //pawns cannot pull
         this.isAprioriValidExit(
           [x, y],
           [fm.start.x, fm.start.y],
@@ -415,13 +415,13 @@ export default class DynamoRules extends ChessRules {
         // Seems so:
         const dir = this.getNormalizedDirection(
           [fm.start.x - x, fm.start.y - y]);
-        const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null);
+        const nbSteps = (fm.vanish[0].p == 'n' ? 1 : null);
         return this.getMovesInDirection([x, y], dir, nbSteps);
       }
       return [];
     };
     const getPullMoves = () => {
-      if (fm.vanish[0].p == V.PAWN)
+      if (fm.vanish[0].p == 'p')
         // pawns cannot pull
         return [];
       const dirM = this.getNormalizedDirection(
@@ -434,8 +434,8 @@ export default class DynamoRules extends ChessRules {
         const deltaX = Math.abs(x - fm.start.x);
         const deltaY = Math.abs(y - fm.start.y);
         if (
-          (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) ||
-          (fm.vanish[0].p == V.KNIGHT &&
+          (fm.vanish[0].p == 'k' && (deltaX > 1 || deltaY > 1)) ||
+          (fm.vanish[0].p == 'n' &&
             (deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0))
         ) {
           return [];
@@ -444,7 +444,7 @@ export default class DynamoRules extends ChessRules {
         let [i, j] = [x + dir[0], y + dir[1]];
         while (
           (i != fm.start.x || j != fm.start.y) &&
-          this.board[i][j] == V.EMPTY
+          this.board[i][j] == ""
         ) {
           i += dir[0];
           j += dir[1];
@@ -493,20 +493,20 @@ export default class DynamoRules extends ChessRules {
     outerLoop: for (let step of steps) {
       let i = x + step[0];
       let j = y + step[1];
-      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+      while (this.onBoard(i, j) && this.board[i][j] == "") {
         moves.push(this.getBasicMove([x, y], [i, j]));
         if (oneStep)
           continue outerLoop;
         i += step[0];
         j += step[1];
       }
-      if (V.OnBoard(i, j)) {
+      if (this.onBoard(i, j)) {
         if (this.canTake([x, y], [i, j]))
           moves.push(this.getBasicMove([x, y], [i, j]));
       }
       else {
         // Add potential board exit (suicide), except for the king
-        if (piece != V.KING) {
+        if (piece != 'k') {
           moves.push({
             start: { x: x, y: y},
             end: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
@@ -563,7 +563,8 @@ export default class DynamoRules extends ChessRules {
     }
   }
 
-  // TODO ::
+// TODO: re-write just for here getAllPotentialMoves() ?
+
   filterValid(moves) {
     const color = this.turn;
     const La = this.amoves.length;
@@ -572,7 +573,7 @@ export default class DynamoRules extends ChessRules {
         // A move is valid either if it doesn't result in a check,
         // or if a second move is possible to counter the check
         // (not undoing a potential move + action of the opponent)
-        this.play(m);
+        this.playOnBoard(m);
         let res = this.underCheck(color);
         if (this.subTurn == 2) {
           let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
@@ -580,7 +581,7 @@ export default class DynamoRules extends ChessRules {
             const moves2 = this.getAllPotentialMoves();
             for (let m2 of moves2) {
               this.play(m2);
-              const res2 = this.underCheck(color);
+              const res2 = this.underCheck(color); //TODO: + square
               const amove = this.getAmove(m, m2);
               isOpposite =
                 La > 0 && this.oppositeMoves(this.amoves[La-1], amove);
@@ -592,7 +593,7 @@ export default class DynamoRules extends ChessRules {
             }
           }
         }
-        this.undo(m);
+        this.undoOnBoard(m);
         return !res;
       });
     }
diff --git a/variants/Eightpieces/class.js b/variants/Eightpieces/class.js
new file mode 100644
index 0000000..3808a16
--- /dev/null
+++ b/variants/Eightpieces/class.js
@@ -0,0 +1,1136 @@
+import { randInt, sample } from "@/utils/alea";
+import { ChessRules, PiPo, Move } from "@/base_rules";
+
+export class EightpiecesRules extends ChessRules {
+
+  static get JAILER() {
+    return "j";
+  }
+  static get SENTRY() {
+    return "s";
+  }
+  static get LANCER() {
+    return "l";
+  }
+
+  static get IMAGE_EXTENSION() {
+    // Temporarily, for the time SVG pieces are being designed:
+    return ".png";
+  }
+
+  // Lancer directions *from white perspective*
+  static get LANCER_DIRS() {
+    return {
+      'c': [-1, 0], //north
+      'd': [-1, 1], //N-E
+      'e': [0, 1], //east
+      'f': [1, 1], //S-E
+      'g': [1, 0], //south
+      'h': [1, -1], //S-W
+      'm': [0, -1], //west
+      'o': [-1, -1] //N-W
+    };
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES
+      .concat([V.JAILER, V.SENTRY])
+      .concat(Object.keys(V.LANCER_DIRS));
+  }
+
+  getPiece(i, j) {
+    const piece = this.board[i][j].charAt(1);
+    // Special lancer case: 8 possible orientations
+    if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
+    return piece;
+  }
+
+  getPpath(b, color, score, orientation) {
+    if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b;
+    if (Object.keys(V.LANCER_DIRS).includes(b[1])) {
+      if (orientation == 'w') return "Eightpieces/tmp_png/" + b;
+      // Find opposite direction for adequate display:
+      let oppDir = '';
+      switch (b[1]) {
+        case 'c':
+          oppDir = 'g';
+          break;
+        case 'g':
+          oppDir = 'c';
+          break;
+        case 'd':
+          oppDir = 'h';
+          break;
+        case 'h':
+          oppDir = 'd';
+          break;
+        case 'e':
+          oppDir = 'm';
+          break;
+        case 'm':
+          oppDir = 'e';
+          break;
+        case 'f':
+          oppDir = 'o';
+          break;
+        case 'o':
+          oppDir = 'f';
+          break;
+      }
+      return "Eightpieces/tmp_png/" + b[0] + oppDir;
+    }
+    // TODO: after we have SVG pieces, remove the folder and next prefix:
+    return "Eightpieces/tmp_png/" + b;
+  }
+
+  getPPpath(m, orientation) {
+    return (
+      this.getPpath(
+        m.appear[0].c + m.appear[0].p,
+        null,
+        null,
+        orientation
+      )
+    );
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { sentrypush: fenParts[5] }
+    );
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 5) Check sentry push (if any)
+    if (
+      fenParsed.sentrypush != "-" &&
+      !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/)
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getSentrypushFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getSentrypushFen();
+  }
+
+  getSentrypushFen() {
+    const L = this.sentryPush.length;
+    if (!this.sentryPush[L-1]) return "-";
+    let res = "";
+    const spL = this.sentryPush[L-1].length;
+    // Condensate path: just need initial and final squares:
+    return [0, spL - 1]
+      .map(i => V.CoordsToSquare(this.sentryPush[L-1][i]))
+      .join("");
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    // subTurn == 2 only when a sentry moved, and is about to push something
+    this.subTurn = 1;
+    // Sentry position just after a "capture" (subTurn from 1 to 2)
+    this.sentryPos = null;
+    // Stack pieces' forbidden squares after a sentry move at each turn
+    const parsedFen = V.ParseFen(fen);
+    if (parsedFen.sentrypush == "-") this.sentryPush = [null];
+    else {
+      // Expand init + dest squares into a full path:
+      const init = V.SquareToCoords(parsedFen.sentrypush.substr(0, 2)),
+            dest = V.SquareToCoords(parsedFen.sentrypush.substr(2));
+      let newPath = [init];
+      const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i]));
+      // Check that it's not a knight movement:
+      if (delta[0] == 0 || delta[1] == 0 || delta[0] == delta[1]) {
+        const step = ['x', 'y'].map((i, idx) => {
+          return (dest[i] - init[i]) / delta[idx] || 0
+        });
+        let x = init.x + step[0],
+            y = init.y + step[1];
+        while (x != dest.x || y != dest.y) {
+          newPath.push({ x: x, y: y });
+          x += step[0];
+          y += step[1];
+        }
+      }
+      newPath.push(dest);
+      this.sentryPush = [newPath];
+    }
+  }
+
+  static GenRandInitFen(options) {
+    if (options.randomness == 0)
+      return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -";
+
+    const baseFen = ChessRules.GenRandInitFen(options);
+    const fenParts = baseFen.split(' ');
+    const posParts = fenParts[0].split('/');
+
+    // Replace one bishop by sentry, so that sentries on different colors
+    // Also replace one random rook by jailer,
+    // and one random knight by lancer (facing north/south)
+    let pieceLine = { b: posParts[0], w: posParts[7].toLowerCase() };
+    let posBlack = { r: -1, n: -1, b: -1 };
+    const mapP = { r: 'j', n: 'l', b: 's' };
+    ['b', 'w'].forEach(c => {
+      ['r', 'n', 'b'].forEach(p => {
+        let pl = pieceLine[c];
+        let pos = -1;
+        if (options.randomness == 2 || c == 'b')
+          pos = (randInt(2) == 0 ? pl.indexOf(p) : pl.lastIndexOf(p));
+        else pos = posBlack[p];
+        pieceLine[c] =
+          pieceLine[c].substr(0, pos) + mapP[p] + pieceLine[c].substr(pos+1);
+        if (options.randomness == 1 && c == 'b') posBlack[p] = pos;
+      });
+    });
+    // Rename 'l' into 'g' (black) or 'c' (white)
+    pieceLine['w'] = pieceLine['w'].replace('l', 'c');
+    pieceLine['b'] = pieceLine['b'].replace('l', 'g');
+    if (options.randomness == 2) {
+      const ws = pieceLine['w'].indexOf('s');
+      const bs = pieceLine['b'].indexOf('s');
+      if (ws % 2 != bs % 2) {
+        // Fix sentry: should be on different colors.
+        // => move sentry on other bishop for random color
+        const c = sample(['w', 'b'], 1);
+        pieceLine[c] = pieceLine[c]
+                       .replace('b', 't') //tmp
+                       .replace('s', 'b')
+                       .replace('t', 's');
+      }
+    }
+
+    return (
+      pieceLine['b'] + "/" +
+      posParts.slice(1, 7).join('/') + "/" +
+      pieceLine['w'].toUpperCase() + " " +
+      fenParts.slice(1, 5).join(' ') + " -"
+    );
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    if (this.subTurn == 2)
+      // Only self captures on this subturn:
+      return this.getColor(x1, y1) == this.getColor(x2, y2);
+    return super.canTake([x1, y1], [x2, y2]);
+  }
+
+  // Is piece on square (x,y) immobilized?
+  isImmobilized([x, y]) {
+    const color = this.getColor(x, y);
+    const oppCol = V.GetOppCol(color);
+    for (let step of V.steps[V.ROOK]) {
+      const [i, j] = [x + step[0], y + step[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == oppCol
+      ) {
+        if (this.getPiece(i, j) == V.JAILER) return [i, j];
+      }
+    }
+    return null;
+  }
+
+  canIplay(side, [x, y]) {
+    return (
+      (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side)
+      ||
+      (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y)
+    );
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const piece = this.getPiece(x, y);
+    const L = this.sentryPush.length;
+    // At subTurn == 2, jailers aren't effective (Jeff K)
+    if (this.subTurn == 1) {
+      const jsq = this.isImmobilized([x, y]);
+      if (!!jsq) {
+        let moves = [];
+        // Special pass move if king:
+        if (piece == V.KING) {
+          moves.push(
+            new Move({
+              appear: [],
+              vanish: [],
+              start: { x: x, y: y },
+              end: { x: jsq[0], y: jsq[1] }
+            })
+          );
+        }
+        else if (piece == V.LANCER && !!this.sentryPush[L-1]) {
+          // A pushed lancer next to the jailer: reorient
+          const color = this.getColor(x, y);
+          const curDir = this.board[x][y].charAt(1);
+          Object.keys(V.LANCER_DIRS).forEach(k => {
+            moves.push(
+              new Move({
+                appear: [{ x: x, y: y, c: color, p: k }],
+                vanish: [{ x: x, y: y, c: color, p: curDir }],
+                start: { x: x, y: y },
+                end: { x: jsq[0], y: jsq[1] }
+              })
+            );
+          });
+        }
+        return moves;
+      }
+    }
+    let moves = [];
+    switch (piece) {
+      case V.JAILER:
+        moves = this.getPotentialJailerMoves([x, y]);
+        break;
+      case V.SENTRY:
+        moves = this.getPotentialSentryMoves([x, y]);
+        break;
+      case V.LANCER:
+        moves = this.getPotentialLancerMoves([x, y]);
+        break;
+      default:
+        moves = super.getPotentialMovesFrom([x, y]);
+        break;
+    }
+    if (!!this.sentryPush[L-1]) {
+      // Delete moves walking back on sentry push path,
+      // only if not a pawn, and the piece is the pushed one.
+      const pl = this.sentryPush[L-1].length;
+      const finalPushedSq = this.sentryPush[L-1][pl-1];
+      moves = moves.filter(m => {
+        if (
+          m.vanish[0].p != V.PAWN &&
+          m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
+          this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
+        ) {
+          return false;
+        }
+        return true;
+      });
+    }
+    else if (this.subTurn == 2) {
+      // Put back the sentinel on board:
+      const color = this.turn;
+      moves.forEach(m => {
+        m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
+      });
+    }
+    return moves;
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const color = this.getColor(x, y);
+    let moves = [];
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    let shiftX = (color == "w" ? -1 : 1);
+    if (this.subTurn == 2) shiftX *= -1;
+    const firstRank = color == "w" ? sizeX - 1 : 0;
+    const startRank = color == "w" ? sizeX - 2 : 1;
+    const lastRank = color == "w" ? 0 : sizeX - 1;
+
+    // Pawns might be pushed on 1st rank and attempt to move again:
+    if (!V.OnBoard(x + shiftX, y)) return [];
+
+    // A push cannot put a pawn on last rank (it goes backward)
+    let finalPieces = [V.PAWN];
+    if (x + shiftX == lastRank) {
+      // Only allow direction facing inside board:
+      const allowedLancerDirs =
+        lastRank == 0
+          ? ['e', 'f', 'g', 'h', 'm']
+          : ['c', 'd', 'e', 'm', 'o'];
+      finalPieces =
+        allowedLancerDirs
+        .concat([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]);
+    }
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      // One square forward
+      for (let piece of finalPieces) {
+        moves.push(
+          this.getBasicMove([x, y], [x + shiftX, y], {
+            c: color,
+            p: piece
+          })
+        );
+      }
+      if (
+        // 2-squares jumps forbidden if pawn push
+        this.subTurn == 1 &&
+        [startRank, firstRank].includes(x) &&
+        this.board[x + 2 * shiftX][y] == V.EMPTY
+      ) {
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+      }
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      if (
+        y + shiftY >= 0 &&
+        y + shiftY < sizeY &&
+        this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+        this.canTake([x, y], [x + shiftX, y + shiftY])
+      ) {
+        for (let piece of finalPieces) {
+          moves.push(
+            this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
+              c: color,
+              p: piece
+            })
+          );
+        }
+      }
+    }
+
+    // En passant: only on subTurn == 1
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1];
+    if (
+      this.subTurn == 1 &&
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs(epSquare.y - y) == 1
+    ) {
+      let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+      enpassantMove.vanish.push({
+        x: x,
+        y: epSquare.y,
+        p: "p",
+        c: this.getColor(x, epSquare.y)
+      });
+      moves.push(enpassantMove);
+    }
+
+    return moves;
+  }
+
+  doClick(square) {
+    if (isNaN(square[0])) return null;
+    const L = this.sentryPush.length;
+    const [x, y] = [square[0], square[1]];
+    const color = this.turn;
+    if (
+      this.subTurn == 2 ||
+      this.board[x][y] == V.EMPTY ||
+      this.getPiece(x, y) != V.LANCER ||
+      this.getColor(x, y) != color ||
+      !!this.sentryPush[L-1]
+    ) {
+      return null;
+    }
+    // Stuck lancer?
+    const orientation = this.board[x][y][1];
+    const step = V.LANCER_DIRS[orientation];
+    if (!V.OnBoard(x + step[0], y + step[1])) {
+      let choices = [];
+      Object.keys(V.LANCER_DIRS).forEach(k => {
+        const dir = V.LANCER_DIRS[k];
+        if (
+          (dir[0] != step[0] || dir[1] != step[1]) &&
+          V.OnBoard(x + dir[0], y + dir[1])
+        ) {
+          choices.push(
+            new Move({
+              vanish: [
+                new PiPo({
+                  x: x,
+                  y: y,
+                  c: color,
+                  p: orientation
+                })
+              ],
+              appear: [
+                new PiPo({
+                  x: x,
+                  y: y,
+                  c: color,
+                  p: k
+                })
+              ],
+              start: { x: x, y : y },
+              end: { x: -1, y: -1 }
+            })
+          );
+        }
+      });
+      return choices;
+    }
+    return null;
+  }
+
+  // Obtain all lancer moves in "step" direction
+  getPotentialLancerMoves_aux([x, y], step, tr) {
+    let moves = [];
+    // Add all moves to vacant squares until opponent is met:
+    const color = this.getColor(x, y);
+    const oppCol =
+      this.subTurn == 1
+        ? V.GetOppCol(color)
+        // at subTurn == 2, consider own pieces as opponent
+        : color;
+    let sq = [x + step[0], y + step[1]];
+    while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
+      if (this.board[sq[0]][sq[1]] == V.EMPTY)
+        moves.push(this.getBasicMove([x, y], sq, tr));
+      sq[0] += step[0];
+      sq[1] += step[1];
+    }
+    if (V.OnBoard(sq[0], sq[1]))
+      // Add capturing move
+      moves.push(this.getBasicMove([x, y], sq, tr));
+    return moves;
+  }
+
+  getPotentialLancerMoves([x, y]) {
+    let moves = [];
+    // Add all lancer possible orientations, similar to pawn promotions.
+    // Except if just after a push: allow all movements from init square then
+    const L = this.sentryPush.length;
+    const color = this.getColor(x, y);
+    const dirCode = this.board[x][y][1];
+    const curDir = V.LANCER_DIRS[dirCode];
+    if (!!this.sentryPush[L-1]) {
+      // Maybe I was pushed
+      const pl = this.sentryPush[L-1].length;
+      if (
+        this.sentryPush[L-1][pl-1].x == x &&
+        this.sentryPush[L-1][pl-1].y == y
+      ) {
+        // I was pushed: allow all directions (for this move only), but
+        // do not change direction after moving, *except* if I keep the
+        // same orientation in which I was pushed.
+        // Also allow simple reorientation ("capturing king"):
+        if (!V.OnBoard(x + curDir[0], y + curDir[1])) {
+          const kp = this.kingPos[color];
+          let reorientMoves = [];
+          Object.keys(V.LANCER_DIRS).forEach(k => {
+            const dir = V.LANCER_DIRS[k];
+            if (
+              (dir[0] != curDir[0] || dir[1] != curDir[1]) &&
+              V.OnBoard(x + dir[0], y + dir[1])
+            ) {
+              reorientMoves.push(
+                new Move({
+                  vanish: [
+                    new PiPo({
+                      x: x,
+                      y: y,
+                      c: color,
+                      p: dirCode
+                    })
+                  ],
+                  appear: [
+                    new PiPo({
+                      x: x,
+                      y: y,
+                      c: color,
+                      p: k
+                    })
+                  ],
+                  start: { x: x, y : y },
+                  end: { x: kp[0], y: kp[1] }
+                })
+              );
+            }
+          });
+          Array.prototype.push.apply(moves, reorientMoves);
+        }
+        Object.values(V.LANCER_DIRS).forEach(step => {
+          const dirCode = Object.keys(V.LANCER_DIRS).find(k => {
+            return (
+              V.LANCER_DIRS[k][0] == step[0] &&
+              V.LANCER_DIRS[k][1] == step[1]
+            );
+          });
+          const dirMoves =
+            this.getPotentialLancerMoves_aux(
+              [x, y],
+              step,
+              { p: dirCode, c: color }
+            );
+          if (curDir[0] == step[0] && curDir[1] == step[1]) {
+            // Keeping same orientation: can choose after
+            let chooseMoves = [];
+            dirMoves.forEach(m => {
+              Object.keys(V.LANCER_DIRS).forEach(k => {
+                const newDir = V.LANCER_DIRS[k];
+                // Prevent orientations toward outer board:
+                if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
+                  let mk = JSON.parse(JSON.stringify(m));
+                  mk.appear[0].p = k;
+                  chooseMoves.push(mk);
+                }
+              });
+            });
+            Array.prototype.push.apply(moves, chooseMoves);
+          }
+          else Array.prototype.push.apply(moves, dirMoves);
+        });
+        return moves;
+      }
+    }
+    // I wasn't pushed: standard lancer move
+    const monodirMoves =
+      this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
+    // Add all possible orientations aftermove except if I'm being pushed
+    if (this.subTurn == 1) {
+      monodirMoves.forEach(m => {
+        Object.keys(V.LANCER_DIRS).forEach(k => {
+          const newDir = V.LANCER_DIRS[k];
+          // Prevent orientations toward outer board:
+          if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
+            let mk = JSON.parse(JSON.stringify(m));
+            mk.appear[0].p = k;
+            moves.push(mk);
+          }
+        });
+      });
+      return moves;
+    }
+    else {
+      // I'm pushed: add potential nudges, except for current orientation
+      let potentialNudges = [];
+      for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+        if (
+          (step[0] != curDir[0] || step[1] != curDir[1]) &&
+          V.OnBoard(x + step[0], y + step[1]) &&
+          this.board[x + step[0]][y + step[1]] == V.EMPTY
+        ) {
+          const newDirCode = Object.keys(V.LANCER_DIRS).find(k => {
+            const codeStep = V.LANCER_DIRS[k];
+            return (codeStep[0] == step[0] && codeStep[1] == step[1]);
+          });
+          potentialNudges.push(
+            this.getBasicMove(
+              [x, y],
+              [x + step[0], y + step[1]],
+              { c: color, p: newDirCode }
+            )
+          );
+        }
+      }
+      return monodirMoves.concat(potentialNudges);
+    }
+  }
+
+  getPotentialSentryMoves([x, y]) {
+    // The sentry moves a priori like a bishop:
+    let moves = super.getPotentialBishopMoves([x, y]);
+    // ...but captures are replaced by special move, if and only if
+    // "captured" piece can move now, considered as the capturer unit.
+    // --> except is subTurn == 2, in this case I don't push anything.
+    if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1);
+    moves.forEach(m => {
+      if (m.vanish.length == 2) {
+        // Temporarily cancel the sentry capture:
+        m.appear.pop();
+        m.vanish.pop();
+      }
+    });
+    const color = this.getColor(x, y);
+    const fMoves = moves.filter(m => {
+      // Can the pushed unit make any move? ...resulting in a non-self-check?
+      if (m.appear.length == 0) {
+        let res = false;
+        this.play(m);
+        let moves2 = this.getPotentialMovesFrom([m.end.x, m.end.y]);
+        for (let m2 of moves2) {
+          this.play(m2);
+          res = !this.underCheck(color);
+          this.undo(m2);
+          if (res) break;
+        }
+        this.undo(m);
+        return res;
+      }
+      return true;
+    });
+    return fMoves;
+  }
+
+  getPotentialJailerMoves([x, y]) {
+    return super.getPotentialRookMoves([x, y]).filter(m => {
+      // Remove jailer captures
+      return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
+    });
+  }
+
+  getPotentialKingMoves(sq) {
+    const moves = this.getSlideNJumpMoves(
+      sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
+    return (
+      this.subTurn == 1
+        ? moves.concat(this.getCastleMoves(sq))
+        : moves
+    );
+  }
+
+  atLeastOneMove() {
+    // If in second-half of a move, we already know that a move is possible
+    if (this.subTurn == 2) return true;
+    return super.atLeastOneMove();
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const basicFilter = (m, c) => {
+      this.play(m);
+      const res = !this.underCheck(c);
+      this.undo(m);
+      return res;
+    };
+    // Disable check tests for sentry pushes,
+    // because in this case the move isn't finished
+    let movesWithoutSentryPushes = [];
+    let movesWithSentryPushes = [];
+    moves.forEach(m => {
+      // Second condition below for special king "pass" moves
+      if (m.appear.length > 0 || m.vanish.length == 0)
+        movesWithoutSentryPushes.push(m);
+      else movesWithSentryPushes.push(m);
+    });
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const filteredMoves =
+      movesWithoutSentryPushes.filter(m => basicFilter(m, color));
+    // If at least one full move made, everything is allowed.
+    // Else: forbid checks and captures.
+    return (
+      this.movesCount >= 2
+        ? filteredMoves
+        : filteredMoves.filter(m => {
+          return (m.vanish.length <= 1 && basicFilter(m, oppCol));
+        })
+    ).concat(movesWithSentryPushes);
+  }
+
+  getAllValidMoves() {
+    if (this.subTurn == 1) return super.getAllValidMoves();
+    // Sentry push:
+    const sentrySq = [this.sentryPos.x, this.sentryPos.y];
+    return this.filterValid(this.getPotentialMovesFrom(sentrySq));
+  }
+
+  isAttacked(sq, color) {
+    return (
+      super.isAttacked(sq, color) ||
+      this.isAttackedByLancer(sq, color) ||
+      this.isAttackedBySentry(sq, color)
+      // The jailer doesn't capture.
+    );
+  }
+
+  isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = y + step[1];
+      while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
+        rx += step[0];
+        ry += step[1];
+      }
+      if (
+        V.OnBoard(rx, ry) &&
+        this.getPiece(rx, ry) == piece &&
+        this.getColor(rx, ry) == color &&
+        !this.isImmobilized([rx, ry])
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByPawn([x, y], color) {
+    const pawnShift = (color == "w" ? 1 : -1);
+    if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
+      for (let i of [-1, 1]) {
+        if (
+          y + i >= 0 &&
+          y + i < V.size.y &&
+          this.getPiece(x + pawnShift, y + i) == V.PAWN &&
+          this.getColor(x + pawnShift, y + i) == color &&
+          !this.isImmobilized([x + pawnShift, y + i])
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  isAttackedByLancer([x, y], color) {
+    for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+      // If in this direction there are only enemy pieces and empty squares,
+      // and we meet a lancer: can he reach us?
+      // NOTE: do not stop at first lancer, there might be several!
+      let coord = { x: x + step[0], y: y + step[1] };
+      let lancerPos = [];
+      while (
+        V.OnBoard(coord.x, coord.y) &&
+        (
+          this.board[coord.x][coord.y] == V.EMPTY ||
+          this.getColor(coord.x, coord.y) == color
+        )
+      ) {
+        if (
+          this.getPiece(coord.x, coord.y) == V.LANCER &&
+          !this.isImmobilized([coord.x, coord.y])
+        ) {
+          lancerPos.push({x: coord.x, y: coord.y});
+        }
+        coord.x += step[0];
+        coord.y += step[1];
+      }
+      const L = this.sentryPush.length;
+      const pl = (!!this.sentryPush[L-1] ? this.sentryPush[L-1].length : 0);
+      for (let xy of lancerPos) {
+        const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
+        if (
+          (dir[0] == -step[0] && dir[1] == -step[1]) ||
+          // If the lancer was just pushed, this is an attack too:
+          (
+            !!this.sentryPush[L-1] &&
+            this.sentryPush[L-1][pl-1].x == xy.x &&
+            this.sentryPush[L-1][pl-1].y == xy.y
+          )
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  // Helper to check sentries attacks:
+  selfAttack([x1, y1], [x2, y2]) {
+    const color = this.getColor(x1, y1);
+    const oppCol = V.GetOppCol(color);
+    const sliderAttack = (allowedSteps, lancer) => {
+      const deltaX = x2 - x1,
+            deltaY = y2 - y1;
+      const absDeltaX = Math.abs(deltaX),
+            absDeltaY = Math.abs(deltaY);
+      const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ];
+      if (
+        // Check that the step is a priori valid:
+        (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) ||
+        allowedSteps.every(st => st[0] != step[0] || st[1] != step[1])
+      ) {
+        return false;
+      }
+      let sq = [ x1 + step[0], y1 + step[1] ];
+      while (sq[0] != x2 || sq[1] != y2) {
+        // NOTE: no need to check OnBoard in this special case
+        if (this.board[sq[0]][sq[1]] != V.EMPTY) {
+          const p = this.getPiece(sq[0], sq[1]);
+          const pc = this.getColor(sq[0], sq[1]);
+          if (
+            // Enemy sentry on the way will be gone:
+            (p != V.SENTRY || pc != oppCol) &&
+            // Lancer temporarily "changed color":
+            (!lancer || pc == color)
+          ) {
+            return false;
+          }
+        }
+        sq[0] += step[0];
+        sq[1] += step[1];
+      }
+      return true;
+    };
+    switch (this.getPiece(x1, y1)) {
+      case V.PAWN: {
+        // Pushed pawns move as enemy pawns
+        const shift = (color == 'w' ? 1 : -1);
+        return (x1 + shift == x2 && Math.abs(y1 - y2) == 1);
+      }
+      case V.KNIGHT: {
+        const deltaX = Math.abs(x1 - x2);
+        const deltaY = Math.abs(y1 - y2);
+        return (
+          deltaX + deltaY == 3 &&
+          [1, 2].includes(deltaX) &&
+          [1, 2].includes(deltaY)
+        );
+      }
+      case V.ROOK:
+        return sliderAttack(V.steps[V.ROOK]);
+      case V.BISHOP:
+        return sliderAttack(V.steps[V.BISHOP]);
+      case V.QUEEN:
+        return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
+      case V.LANCER: {
+        // Special case: as long as no enemy units stands in-between,
+        // it attacks (if it points toward the king).
+        const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)];
+        return sliderAttack([allowedStep], "lancer");
+      }
+      // No sentries or jailer tests: they cannot self-capture
+    }
+    return false;
+  }
+
+  isAttackedBySentry([x, y], color) {
+    // Attacked by sentry means it can self-take our king.
+    // Just check diagonals of enemy sentry(ies), and if it reaches
+    // one of our pieces: can I self-take?
+    const myColor = V.GetOppCol(color);
+    let candidates = [];
+    for (let i=0; i<V.size.x; i++) {
+      for (let j=0; j<V.size.y; j++) {
+        if (
+          this.getPiece(i,j) == V.SENTRY &&
+          this.getColor(i,j) == color &&
+          !this.isImmobilized([i, j])
+        ) {
+          for (let step of V.steps[V.BISHOP]) {
+            let sq = [ i + step[0], j + step[1] ];
+            while (
+              V.OnBoard(sq[0], sq[1]) &&
+              this.board[sq[0]][sq[1]] == V.EMPTY
+            ) {
+              sq[0] += step[0];
+              sq[1] += step[1];
+            }
+            if (
+              V.OnBoard(sq[0], sq[1]) &&
+              this.getColor(sq[0], sq[1]) == myColor
+            ) {
+              candidates.push([ sq[0], sq[1] ]);
+            }
+          }
+        }
+      }
+    }
+    for (let c of candidates)
+      if (this.selfAttack(c, [x, y])) return true;
+    return false;
+  }
+
+  // Jailer doesn't capture or give check
+
+  prePlay(move) {
+    if (move.appear.length == 0 && move.vanish.length == 1)
+      // The sentry is about to push a piece: subTurn goes from 1 to 2
+      this.sentryPos = { x: move.end.x, y: move.end.y };
+    if (this.subTurn == 2 && move.vanish[0].p != V.PAWN) {
+      // A piece is pushed: forbid array of squares between start and end
+      // of move, included (except if it's a pawn)
+      let squares = [];
+      if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
+        // short-range pieces: just forbid initial square
+        squares.push({ x: move.start.x, y: move.start.y });
+      else {
+        const deltaX = move.end.x - move.start.x;
+        const deltaY = move.end.y - move.start.y;
+        const step = [
+          deltaX / Math.abs(deltaX) || 0,
+          deltaY / Math.abs(deltaY) || 0
+        ];
+        for (
+          let sq = {x: move.start.x, y: move.start.y};
+          sq.x != move.end.x || sq.y != move.end.y;
+          sq.x += step[0], sq.y += step[1]
+        ) {
+          squares.push({ x: sq.x, y: sq.y });
+        }
+      }
+      // Add end square as well, to know if I was pushed (useful for lancers)
+      squares.push({ x: move.end.x, y: move.end.y });
+      this.sentryPush.push(squares);
+    } else this.sentryPush.push(null);
+  }
+
+  play(move) {
+    this.prePlay(move);
+    move.flags = JSON.stringify(this.aggregateFlags());
+    this.epSquares.push(this.getEpSquare(move));
+    V.PlayOnBoard(this.board, move);
+    // Is it a sentry push? (useful for undo)
+    move.sentryPush = (this.subTurn == 2);
+    if (this.subTurn == 1) this.movesCount++;
+    if (move.appear.length == 0 && move.vanish.length == 1) this.subTurn = 2;
+    else {
+      // Turn changes only if not a sentry "pre-push"
+      this.turn = V.GetOppCol(this.turn);
+      this.subTurn = 1;
+    }
+    this.postPlay(move);
+  }
+
+  postPlay(move) {
+    if (move.vanish.length == 0 || this.subTurn == 2)
+      // Special pass move of the king, or sentry pre-push: nothing to update
+      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][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+      this.castleFlags[c] = [V.size.y, V.size.y];
+      return;
+    }
+    // Update castling flags if rooks are moved
+    const oppCol = V.GetOppCol(c);
+    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;
+    } else 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;
+    }
+  }
+
+  undo(move) {
+    this.epSquares.pop();
+    this.disaggregateFlags(JSON.parse(move.flags));
+    V.UndoOnBoard(this.board, move);
+    // Decrement movesCount except if the move is a sentry push
+    if (!move.sentryPush) this.movesCount--;
+    if (this.subTurn == 2) this.subTurn = 1;
+    else {
+      this.turn = V.GetOppCol(this.turn);
+      if (move.sentryPush) this.subTurn = 2;
+    }
+    this.postUndo(move);
+  }
+
+  postUndo(move) {
+    super.postUndo(move);
+    this.sentryPush.pop();
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
+      ChessRules.VALUES
+    );
+  }
+
+  getComputerMove() {
+    const maxeval = V.INFINITY;
+    const color = this.turn;
+    let moves1 = this.getAllValidMoves();
+
+    if (moves1.length == 0)
+      // TODO: this situation should not happen
+      return null;
+
+    const setEval = (move, next) => {
+      const score = this.getCurrentScore();
+      const curEval = move.eval;
+      if (score != "*") {
+        move.eval =
+          score == "1/2"
+            ? 0
+            : (score == "1-0" ? 1 : -1) * maxeval;
+      } else move.eval = this.evalPosition();
+      if (
+        // "next" is defined after sentry pushes
+        !!next && (
+          !curEval ||
+          color == 'w' && move.eval > curEval ||
+          color == 'b' && move.eval < curEval
+        )
+      ) {
+        move.second = next;
+      }
+    };
+
+    // Just search_depth == 1 (because of sentries. TODO: can do better...)
+    moves1.forEach(m1 => {
+      this.play(m1);
+      if (this.subTurn == 1) setEval(m1);
+      else {
+        // Need to play every pushes and count:
+        const moves2 = this.getAllValidMoves();
+        moves2.forEach(m2 => {
+          this.play(m2);
+          setEval(m1, m2);
+          this.undo(m2);
+        });
+      }
+      this.undo(m1);
+    });
+
+    moves1.sort((a, b) => {
+      return (color == "w" ? 1 : -1) * (b.eval - a.eval);
+    });
+    let candidates = [0];
+    for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
+      candidates.push(j);
+    const choice = moves1[candidates[randInt(candidates.length)]];
+    return (!choice.second ? choice : [choice, choice.second]);
+  }
+
+  // For moves notation:
+  static get LANCER_DIRNAMES() {
+    return {
+      'c': "N",
+      'd': "NE",
+      'e': "E",
+      'f': "SE",
+      'g': "S",
+      'h': "SW",
+      'm': "W",
+      'o': "NW"
+    };
+  }
+
+  getNotation(move) {
+    // Special case "king takes jailer" is a pass move
+    if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
+    let notation = undefined;
+    if (this.subTurn == 2) {
+      // Do not consider appear[1] (sentry) for sentry pushes
+      const simpleMove = {
+        appear: [move.appear[0]],
+        vanish: move.vanish,
+        start: move.start,
+        end: move.end
+      };
+      notation = super.getNotation(simpleMove);
+    }
+    else if (
+      move.appear.length > 0 &&
+      move.vanish[0].x == move.appear[0].x &&
+      move.vanish[0].y == move.appear[0].y
+    ) {
+      // Lancer in-place reorientation:
+      notation = "L" + V.CoordsToSquare(move.start) + ":R";
+    }
+    else notation = super.getNotation(move);
+    if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
+      // Lancer: add direction info
+      notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
+    else if (
+      move.vanish[0].p == V.PAWN &&
+      Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
+    ) {
+      // Fix promotions in lancer:
+      notation = notation.slice(0, -1) +
+        "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
+    }
+    return notation;
+  }
+
+};
diff --git a/variants/Eightpieces/complete_rules.html b/variants/Eightpieces/complete_rules.html
new file mode 100644
index 0000000..c1d12ab
--- /dev/null
+++ b/variants/Eightpieces/complete_rules.html
@@ -0,0 +1,48 @@
+p.boxed
+  | Three new pieces appear. All pieces are unique.
+
+p.
+  There are only one rook, one bishop and one knight per side in this variant.
+  That explains the name. The king and queen are still there,
+  and the three remaining slots are taken by new pieces:
+
+ul
+  li.
+    The lancer 'L' is oriented and can only move in the direction it points,
+    by any number of squares as long as an enemy isn't met
+    (it can jump over friendly pieces). If an opponent' piece is found,
+    it can be captured. After moving you can reorient the lancer.
+  li.
+    The sentry 'S' moves like a bishop but doesn't capture directly.
+    It "pushes" enemy pieces instead, either on an empty square or on other
+    enemy pieces which are thus (self-)captured.
+  li.
+    The jailer 'J' moves like a rook but also doesn't capture.
+    It immobilizes enemy pieces which are vertically or horizontally adjacent.
+
+p.
+  On the following diagram the white sentry can push the black lancer to
+  capture the black pawn on b4. The lancer is then immobilized
+  by the white jailer at a4.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:7k/8/8/8/Jp3m2/8/3S4/K7:
+  .diagram.diag22
+    | fen:7k/8/8/8/Jm3S2/8/8/K7:
+  figcaption Left: before white move S"push"f4. Right: after this move.
+
+p To reorient a stuck lancer,
+ul
+  li Just after being pushed: play a move which 'capture your king".
+  li Later in the game: click on the lancer.
+
+h3 Complete rules
+
+p
+  | The rules were invented by Jeff Kubach (2020), who described them much
+  | more precisely on the 
+  a(href="https://www.chessvariants.com/rules/8-piece-chess")
+    | chessvariants page
+  | . While the summary given above may suffice to start playing,
+  | you should read the complete rules to fully understand this variant.
diff --git a/variants/Eightpieces/rules.html b/variants/Eightpieces/rules.html
new file mode 100644
index 0000000..41e9d42
--- /dev/null
+++ b/variants/Eightpieces/rules.html
@@ -0,0 +1,3 @@
+Three new pieces appear: the lancer is a rook with a constrained direction, the sentry moves like a bishop and pushes pieces, and the jailer immobilizes pieces orthogonally adjacent.
+
+The goal is still to checkmate.
diff --git a/variants/Eightpieces/style.css b/variants/Eightpieces/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Eightpieces/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
diff --git a/variants/Emergo/class.js b/variants/Emergo/class.js
new file mode 100644
index 0000000..13069de
--- /dev/null
+++ b/variants/Emergo/class.js
@@ -0,0 +1,576 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+import { ArrayFun } from "@/utils/array";
+
+export class EmergoRules extends ChessRules {
+
+  // Simple encoding: A to L = 1 to 12, from left to right, if white controls.
+  // Lowercase if black controls.
+  // Single piece (no prisoners): A@ to L@ (+ lowercase)
+
+  static get Options() {
+    return null;
+  }
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get DarkBottomRight() {
+    return true;
+  }
+
+  // board element == file name:
+  static board2fen(b) {
+    return b;
+  }
+  static fen2board(f) {
+    return f;
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        // Add only 0.5 per symbol because 2 per piece
+        if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num) || num <= 0) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    return true;
+  }
+
+  static GetBoard(position) {
+    const rows = position.split("/");
+    let board = ArrayFun.init(V.size.x, V.size.y, "");
+    for (let i = 0; i < rows.length; i++) {
+      let j = 0;
+      for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
+        const character = rows[i][indexInRow];
+        const num = parseInt(character, 10);
+        // If num is a number, just shift j:
+        if (!isNaN(num)) j += num;
+        else
+          // Something at position i,j
+          board[i][j++] = V.fen2board(character + rows[i][++indexInRow]);
+      }
+    }
+    return board;
+  }
+
+  getPpath(b) {
+    return "Emergo/" + b;
+  }
+
+  getColor(x, y) {
+    if (x >= V.size.x) return x == V.size.x ? "w" : "b";
+    if (this.board[x][y].charCodeAt(0) < 97) return 'w';
+    return 'b';
+  }
+
+  getPiece() {
+    return V.PAWN; //unused
+  }
+
+  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;
+    }
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { reserve: fenParts[3] }
+    );
+  }
+
+  static get size() {
+    return { x: 9, y: 9 };
+  }
+
+  static GenRandInitFen() {
+    return "9/9/9/9/9/9/9/9/9 w 0 12,12";
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getReserveFen();
+  }
+
+  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])
+    );
+  }
+
+  getReservePpath(index, color) {
+    return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@';
+  }
+
+  static get RESERVE_PIECES() {
+    return [V.PAWN]; //only array length matters
+  }
+
+  setOtherVariables(fen) {
+    const reserve =
+      V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10));
+    this.reserve = { w: null, b: null };
+    if (reserve[0] > 0) this.reserve['w'] = { [V.PAWN]: reserve[0] };
+    if (reserve[1] > 0) this.reserve['b'] = { [V.PAWN]: reserve[1] };
+    // Local stack of captures during a turn (squares + directions)
+    this.captures = [ [] ];
+  }
+
+  atLeastOneCaptureFrom([x, y], color, forbiddenStep) {
+    for (let s of V.steps[V.BISHOP]) {
+      if (
+        !forbiddenStep ||
+        (s[0] != -forbiddenStep[0] || s[1] != -forbiddenStep[1])
+      ) {
+        const [i, j] = [x + s[0], y + s[1]];
+        if (
+          V.OnBoard(i + s[0], j + s[1]) &&
+          this.board[i][j] != V.EMPTY &&
+          this.getColor(i, j) != color &&
+          this.board[i + s[0]][j + s[1]] == V.EMPTY
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  atLeastOneCapture(color) {
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      return (
+        this.atLeastOneCaptureFrom(
+          captures[L-1].square, color, captures[L-1].step)
+      );
+    }
+    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 &&
+          this.getColor(i, j) == color &&
+          this.atLeastOneCaptureFrom([i, j], color)
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  maxLengthIndices(caps) {
+    let maxLength = 0;
+    let res = [];
+    for (let i = 0; i < caps.length; i++) {
+      if (caps[i].length > maxLength) {
+        res = [i];
+        maxLength = caps[i].length;
+      }
+      else if (caps[i].length == maxLength) res.push(i);
+    }
+    return res;
+  };
+
+  getLongestCaptures_aux([x, y], color, locSteps) {
+    let res = [];
+    const L = locSteps.length;
+    const lastStep = (L > 0 ? locSteps[L-1] : null);
+    for (let s of V.steps[V.BISHOP]) {
+      if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i + s[0], j + s[1]) &&
+        this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) != color
+      ) {
+        const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+        locSteps.push(s);
+        V.PlayOnBoard(this.board, move);
+        const nextRes =
+          this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
+        res.push(1 + nextRes);
+        locSteps.pop();
+        V.UndoOnBoard(this.board, move);
+      }
+    }
+    if (res.length == 0) return 0;
+    return Math.max(...res);
+  }
+
+  getLongestCapturesFrom([x, y], color, locSteps) {
+    let res = [];
+    const L = locSteps.length;
+    const lastStep = (L > 0 ? locSteps[L-1] : null);
+    for (let s of V.steps[V.BISHOP]) {
+      if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue;
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i + s[0], j + s[1]) &&
+        this.board[i + s[0]][j + s[1]] == V.EMPTY &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) != color
+      ) {
+        const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]);
+        locSteps.push(s);
+        V.PlayOnBoard(this.board, move);
+        const stepRes =
+          this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps);
+        res.push({ step: s, length: 1 + stepRes });
+        locSteps.pop();
+        V.UndoOnBoard(this.board, move);
+      }
+    }
+    return this.maxLengthIndices(res).map(i => res[i]);;
+  }
+
+  getAllLongestCaptures(color) {
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    let caps = [];
+    if (L > 0) {
+      let locSteps = [ captures[L-1].step ];
+      let res =
+        this.getLongestCapturesFrom(captures[L-1].square, color, locSteps);
+      Array.prototype.push.apply(
+        caps,
+        res.map(r => Object.assign({ square: captures[L-1].square }, r))
+      );
+    }
+    else {
+      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 &&
+            this.getColor(i, j) == color
+          ) {
+            let locSteps = [];
+            let res = this.getLongestCapturesFrom([i, j], color, locSteps);
+            Array.prototype.push.apply(
+              caps,
+              res.map(r => Object.assign({ square: [i, j] }, r))
+            );
+          }
+        }
+      }
+    }
+    return this.maxLengthIndices(caps).map(i => caps[i]);
+  }
+
+  getBasicMove([x1, y1], [x2, y2], capt) {
+    const cp1 = this.board[x1][y1];
+    if (!capt) {
+      return new Move({
+        appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ],
+        vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ]
+      });
+    }
+    // Compute resulting types based on jumped + jumping pieces
+    const color = this.getColor(x1, y1);
+    const firstCodes = (color == 'w' ? [65, 97] : [97, 65]);
+    const cpCapt = this.board[capt[0]][capt[1]];
+    let count1 = [cp1.charCodeAt(0) - firstCodes[0], -1];
+    if (cp1[1] != '@') count1[1] = cp1.charCodeAt(1) - firstCodes[0];
+    let countC = [cpCapt.charCodeAt(0) - firstCodes[1], -1];
+    if (cpCapt[1] != '@') countC[1] = cpCapt.charCodeAt(1) - firstCodes[1];
+    count1[1]++;
+    countC[0]--;
+    let colorChange = false,
+        captVanish = false;
+    if (countC[0] < 0) {
+      if (countC[1] >= 0) {
+        colorChange = true;
+        countC = [countC[1], -1];
+      }
+      else captVanish = true;
+    }
+    const incPrisoners = String.fromCharCode(firstCodes[0] + count1[1]);
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: x2,
+          y: y2,
+          c: cp1[0],
+          p: incPrisoners
+        })
+      ],
+      vanish: [
+        new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }),
+        new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] })
+      ]
+    });
+    if (!captVanish) {
+      mv.appear.push(
+        new PiPo({
+          x: capt[0],
+          y: capt[1],
+          c: String.fromCharCode(
+               firstCodes[(colorChange ? 0 : 1)] + countC[0]),
+          p: (colorChange ? '@' : cpCapt[1]),
+        })
+      );
+    }
+    return mv;
+  }
+
+  getReserveMoves(x) {
+    const color = this.turn;
+    if (!this.reserve[color] || this.atLeastOneCapture(color)) return [];
+    let moves = [];
+    const shadowPiece =
+      this.reserve[V.GetOppCol(color)] == null
+        ? this.reserve[color][V.PAWN] - 1
+        : 0;
+    const appearColor = String.fromCharCode(
+      (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece);
+    const addMove = ([i, j]) => {
+      moves.push(
+        new Move({
+          appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ],
+          vanish: [],
+          start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 }
+        })
+      );
+    };
+    const oppCol = V.GetOppCol(color);
+    const opponentCanCapture = this.atLeastOneCapture(oppCol);
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j = i % 2; j < V.size.y; j += 2) {
+        if (
+          this.board[i][j] == V.EMPTY &&
+          // prevent playing on central square at move 1:
+          (this.movesCount >= 1 || i != 4 || j != 4)
+        ) {
+          if (opponentCanCapture) addMove([i, j]);
+          else {
+            let canAddMove = true;
+            for (let s of V.steps[V.BISHOP]) {
+              if (
+                V.OnBoard(i + s[0], j + s[1]) &&
+                V.OnBoard(i - s[0], j - s[1]) &&
+                this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+                this.board[i - s[0]][j - s[1]] == V.EMPTY &&
+                this.getColor(i + s[0], j + s[1]) == oppCol
+              ) {
+                canAddMove = false;
+                break;
+              }
+            }
+            if (canAddMove) addMove([i, j]);
+          }
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialMovesFrom([x, y], longestCaptures) {
+    if (x >= V.size.x) {
+      if (longestCaptures.length == 0) return this.getReserveMoves(x);
+      return [];
+    }
+    const color = this.turn;
+    if (!!this.reserve[color] && !this.atLeastOneCapture(color)) return [];
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    let moves = [];
+    if (longestCaptures.length > 0) {
+      if (
+        L > 0 &&
+        (x != captures[L-1].square[0] || y != captures[L-1].square[1])
+      ) {
+        return [];
+      }
+      longestCaptures.forEach(lc => {
+        if (lc.square[0] == x && lc.square[1] == y) {
+          const s = lc.step;
+          const [i, j] = [x + s[0], y + s[1]];
+          moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]));
+        }
+      });
+      return moves;
+    }
+    // Just search simple moves:
+    for (let s of V.steps[V.BISHOP]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  getAllValidMoves() {
+    const color = this.turn;
+    const longestCaptures = this.getAllLongestCaptures(color);
+    let potentialMoves = [];
+    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 && this.getColor(i, j) == color) {
+          Array.prototype.push.apply(
+            potentialMoves,
+            this.getPotentialMovesFrom([i, j], longestCaptures)
+          );
+        }
+      }
+    }
+    // Add reserve moves
+    potentialMoves = potentialMoves.concat(
+      this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1))
+    );
+    return potentialMoves;
+  }
+
+  getPossibleMovesFrom([x, y]) {
+    const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y));
+    return this.getPotentialMovesFrom([x, y], longestCaptures);
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  play(move) {
+    const color = this.turn;
+    move.turn = color; //for undo
+    V.PlayOnBoard(this.board, move);
+    if (move.vanish.length == 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.push({
+        square: [move.end.x, move.end.y],
+        step: [(move.end.x - move.start.x)/2, (move.end.y - move.start.y)/2]
+      });
+      if (this.atLeastOneCapture(color))
+        // There could be other captures (mandatory)
+        move.notTheEnd = true;
+    }
+    else if (move.vanish == 0) {
+      const firstCode = (color == 'w' ? 65 : 97);
+      // Generally, reserveCount == 1 (except for shadow piece)
+      const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
+      this.reserve[color][V.PAWN] -= reserveCount;
+      if (this.reserve[color][V.PAWN] == 0) this.reserve[color] = null;
+    }
+    if (!move.notTheEnd) {
+      this.turn = V.GetOppCol(color);
+      this.movesCount++;
+      this.captures.push([]);
+    }
+  }
+
+  undo(move) {
+    V.UndoOnBoard(this.board, move);
+    if (!move.notTheEnd) {
+      this.turn = move.turn;
+      this.movesCount--;
+      this.captures.pop();
+    }
+    if (move.vanish.length == 0) {
+      const color = (move.appear[0].c == 'A' ? 'w' : 'b');
+      const firstCode = (color == 'w' ? 65 : 97);
+      const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1;
+      if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 };
+      this.reserve[color][V.PAWN] += reserveCount;
+    }
+    else if (move.vanish.length == 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.pop();
+    }
+  }
+
+  atLeastOneMove() {
+    const color = this.turn;
+    if (this.atLeastOneCapture(color)) return true;
+    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 && this.getColor(i, j) == color) {
+          const moves = this.getPotentialMovesFrom([i, j], []);
+          if (moves.length > 0) return true;
+        }
+      }
+    }
+    const reserveMoves =
+      this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
+    return (reserveMoves.length > 0);
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    // If no pieces on board + reserve, I lose
+    if (!!this.reserve[color]) return "*";
+    let atLeastOnePiece = false;
+    outerLoop: 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 && this.getColor(i, j) == color) {
+          atLeastOnePiece = true;
+          break outerLoop;
+        }
+      }
+    }
+    if (!atLeastOnePiece) return (color == 'w' ? "0-1" : "1-0");
+    if (!this.atLeastOneMove()) return "1/2";
+    return "*";
+  }
+
+  getComputerMove() {
+    // Random mover for now (TODO)
+    const color = this.turn;
+    let mvArray = [];
+    let mv = null;
+    while (this.turn == color) {
+      const moves = this.getAllValidMoves();
+      mv = moves[randInt(moves.length)];
+      mvArray.push(mv);
+      this.play(mv);
+    }
+    for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+    return (mvArray.length > 1 ? mvArray : mvArray[0]);
+  }
+
+  getNotation(move) {
+    if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end);
+    const L0 = this.captures.length;
+    if (this.captures[L0 - 1].length > 0) return V.CoordsToSquare(move.end);
+    return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
+  }
+
+};
diff --git a/variants/Empire/class.js b/variants/Empire/class.js
new file mode 100644
index 0000000..d04e4a8
--- /dev/null
+++ b/variants/Empire/class.js
@@ -0,0 +1,432 @@
+import { ChessRules } from "@/base_rules";
+
+export class EmpireRules extends ChessRules {
+
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: [V.QUEEN] }
+    );
+  }
+
+  static get LoseOnRepetition() {
+    return true;
+  }
+
+  static IsGoodFlags(flags) {
+    // Only black can castle
+    return !!flags.match(/^[a-z]{2,2}$/);
+  }
+
+  getPpath(b) {
+    return (b[0] == 'w' ? "Empire/" : "") + b;
+  }
+
+  static GenRandInitFen(options) {
+    if (options.randomness == 0)
+      return "rnbqkbnr/pppppppp/8/8/8/PPPSSPPP/8/TECDKCET w 0 ah -";
+
+    // Mapping kingdom --> empire:
+    const piecesMap = {
+      'R': 'T',
+      'N': 'E',
+      'B': 'C',
+      'Q': 'D',
+      'K': 'K'
+    };
+
+    const baseFen = ChessRules.GenRandInitFen(options);
+    return (
+      baseFen.substr(0, 24) + "PPPSSPPP/8/" +
+      baseFen.substr(35, 8).split('').map(p => piecesMap[p]).join('') +
+      baseFen.substr(43, 5) + baseFen.substr(50)
+    );
+  }
+
+  getFlagsFen() {
+    return this.castleFlags['b'].map(V.CoordToColumn).join("");
+  }
+
+  setFlags(fenflags) {
+    this.castleFlags = { 'b': [-1, -1] };
+    for (let i = 0; i < 2; i++)
+      this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i));
+  }
+
+  static get TOWER() {
+    return 't';
+  }
+  static get EAGLE() {
+    return 'e';
+  }
+  static get CARDINAL() {
+    return 'c';
+  }
+  static get DUKE() {
+    return 'd';
+  }
+  static get SOLDIER() {
+    return 's';
+  }
+  // Kaiser is technically a King, so let's keep things simple.
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat(
+      [V.TOWER, V.EAGLE, V.CARDINAL, V.DUKE, V.SOLDIER]);
+  }
+
+  getPotentialMovesFrom(sq) {
+    let moves = [];
+    const piece = this.getPiece(sq[0], sq[1]);
+    switch (piece) {
+      case V.TOWER:
+        moves = this.getPotentialTowerMoves(sq);
+        break;
+      case V.EAGLE:
+        moves = this.getPotentialEagleMoves(sq);
+        break;
+      case V.CARDINAL:
+        moves = this.getPotentialCardinalMoves(sq);
+        break;
+      case V.DUKE:
+        moves = this.getPotentialDukeMoves(sq);
+        break;
+      case V.SOLDIER:
+        moves = this.getPotentialSoldierMoves(sq);
+        break;
+      default:
+        moves = super.getPotentialMovesFrom(sq);
+    }
+    if (
+      piece != V.KING &&
+      this.kingPos['w'][0] != this.kingPos['b'][0] &&
+      this.kingPos['w'][1] != this.kingPos['b'][1]
+    ) {
+      return moves;
+    }
+    // TODO: factor two next "if" into one (rank/column...)
+    if (this.kingPos['w'][1] == this.kingPos['b'][1]) {
+      const colKing = this.kingPos['w'][1];
+      let intercept = 0; //count intercepting pieces
+      let [kingPos1, kingPos2] = [this.kingPos['w'][0], this.kingPos['b'][0]];
+      if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+      for (let i = kingPos1 + 1; i < kingPos2; i++) {
+        if (this.board[i][colKing] != V.EMPTY) intercept++;
+      }
+      if (intercept >= 2) return moves;
+      // intercept == 1 (0 is impossible):
+      // Any move not removing intercept is OK
+      return moves.filter(m => {
+        return (
+          // From another column?
+          m.start.y != colKing ||
+          // From behind a king? (including kings themselves!)
+          m.start.x <= kingPos1 ||
+          m.start.x >= kingPos2 ||
+          // Intercept piece moving: must remain in-between
+          (
+            m.end.y == colKing &&
+            m.end.x > kingPos1 &&
+            m.end.x < kingPos2
+          )
+        );
+      });
+    }
+    if (this.kingPos['w'][0] == this.kingPos['b'][0]) {
+      const rowKing = this.kingPos['w'][0];
+      let intercept = 0; //count intercepting pieces
+      let [kingPos1, kingPos2] = [this.kingPos['w'][1], this.kingPos['b'][1]];
+      if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+      for (let i = kingPos1 + 1; i < kingPos2; i++) {
+        if (this.board[rowKing][i] != V.EMPTY) intercept++;
+      }
+      if (intercept >= 2) return moves;
+      // intercept == 1 (0 is impossible):
+      // Any move not removing intercept is OK
+      return moves.filter(m => {
+        return (
+          // From another row?
+          m.start.x != rowKing ||
+          // From "behind" a king? (including kings themselves!)
+          m.start.y <= kingPos1 ||
+          m.start.y >= kingPos2 ||
+          // Intercept piece moving: must remain in-between
+          (
+            m.end.x == rowKing &&
+            m.end.y > kingPos1 &&
+            m.end.y < kingPos2
+          )
+        );
+      });
+    }
+    // piece == king: check only if move.end.y == enemy king column,
+    // or if move.end.x == enemy king rank.
+    const color = this.getColor(sq[0], sq[1]);
+    const oppCol = V.GetOppCol(color);
+    return moves.filter(m => {
+      if (
+        m.end.y != this.kingPos[oppCol][1] &&
+        m.end.x != this.kingPos[oppCol][0]
+      ) {
+        return true;
+      }
+      // check == -1 if (row, or col) unchecked, 1 if checked and occupied,
+      //          0 if checked and clear
+      let check = [-1, -1];
+      // TODO: factor two next "if"...
+      if (m.end.x == this.kingPos[oppCol][0]) {
+        if (check[0] < 0) {
+          // Do the check:
+          check[0] = 0;
+          let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]];
+          if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+          for (let i = kingPos1 + 1; i < kingPos2; i++) {
+            if (this.board[m.end.x][i] != V.EMPTY) {
+              check[0]++;
+              break;
+            }
+          }
+          return check[0] == 1;
+        }
+        // Check already done:
+        return check[0] == 1;
+      }
+      //if (m.end.y == this.kingPos[oppCol][1]) //true...
+      if (check[1] < 0) {
+        // Do the check:
+        check[1] = 0;
+        let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]];
+        if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1];
+        for (let i = kingPos1 + 1; i < kingPos2; i++) {
+          if (this.board[i][m.end.y] != V.EMPTY) {
+            check[1]++;
+            break;
+          }
+        }
+        return check[1] == 1;
+      }
+      // Check already done:
+      return check[1] == 1;
+    });
+  }
+
+  // TODO: some merging to do with Orda method (and into base_rules.js)
+  getSlideNJumpMoves_([x, y], steps, oneStep) {
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      const s = step.s;
+      let i = x + s[0];
+      let j = y + s[1];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        if (!step.onlyTake) moves.push(this.getBasicMove([x, y], [i, j]));
+        // NOTE: (bad) HACK here, since onlyTake is true only for Eagle
+        // capturing moves, which are oneStep...
+        if (oneStep || step.onlyTake) continue outerLoop;
+        i += s[0];
+        j += s[1];
+      }
+      if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]) && !step.onlyMove)
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  static get steps() {
+    return (
+      Object.assign(
+        {
+          t: [
+            { s: [-1, 0] },
+            { s: [1, 0] },
+            { s: [0, -1] },
+            { s: [0, 1] },
+            { s: [-1, -1], onlyMove: true },
+            { s: [-1, 1], onlyMove: true },
+            { s: [1, -1], onlyMove: true },
+            { s: [1, 1], onlyMove: true }
+          ],
+          c: [
+            { s: [-1, 0], onlyMove: true },
+            { s: [1, 0], onlyMove: true },
+            { s: [0, -1], onlyMove: true },
+            { s: [0, 1], onlyMove: true },
+            { s: [-1, -1] },
+            { s: [-1, 1] },
+            { s: [1, -1] },
+            { s: [1, 1] }
+          ],
+          e: [
+            { s: [-1, 0], onlyMove: true },
+            { s: [1, 0], onlyMove: true },
+            { s: [0, -1], onlyMove: true },
+            { s: [0, 1], onlyMove: true },
+            { s: [-1, -1], onlyMove: true },
+            { s: [-1, 1], onlyMove: true },
+            { s: [1, -1], onlyMove: true },
+            { s: [1, 1], onlyMove: true },
+            { s: [-2, -1], onlyTake: true },
+            { s: [-2, 1], onlyTake: true },
+            { s: [-1, -2], onlyTake: true },
+            { s: [-1, 2], onlyTake: true },
+            { s: [1, -2], onlyTake: true },
+            { s: [1, 2], onlyTake: true },
+            { s: [2, -1], onlyTake: true },
+            { s: [2, 1], onlyTake: true }
+          ]
+        },
+        ChessRules.steps
+      )
+    );
+  }
+
+  getPotentialTowerMoves(sq) {
+    return this.getSlideNJumpMoves_(sq, V.steps[V.TOWER]);
+  }
+
+  getPotentialCardinalMoves(sq) {
+    return this.getSlideNJumpMoves_(sq, V.steps[V.CARDINAL]);
+  }
+
+  getPotentialEagleMoves(sq) {
+    return this.getSlideNJumpMoves_(sq, V.steps[V.EAGLE]);
+  }
+
+  getPotentialDukeMoves([x, y]) {
+    // Anything to capture around? mark other steps to explore after
+    let steps = [];
+    const oppCol = V.GetOppCol(this.getColor(x, y));
+    let moves = [];
+    for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == oppCol
+      ) {
+        moves.push(super.getBasicMove([x, y], [i, j]));
+      }
+      else steps.push({ s: s, onlyMove: true });
+    }
+    if (steps.length > 0) {
+      const noncapturingMoves = this.getSlideNJumpMoves_([x, y], steps);
+      Array.prototype.push.apply(moves, noncapturingMoves);
+    }
+    return moves;
+  }
+
+  getPotentialKingMoves([x, y]) {
+    if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]);
+    // Empire doesn't castle:
+    return super.getSlideNJumpMoves(
+      [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
+  }
+
+  getPotentialSoldierMoves([x, y]) {
+    const c = this.getColor(x, y);
+    const shiftX = (c == 'w' ? -1 : 1);
+    const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9);
+    let steps = [];
+    if (!lastRank) steps.push([shiftX, 0]);
+    if (y > 0) steps.push([0, -1]);
+    if (y < 9) steps.push([0, 1]);
+    return super.getSlideNJumpMoves([x, y], steps, 1);
+  }
+
+  isAttacked(sq, color) {
+    if (color == 'b') return super.isAttacked(sq, color);
+    // Empire: only pawn and king (+ queen if promotion) in common:
+    return (
+      super.isAttackedByPawn(sq, color) ||
+      this.isAttackedByTower(sq, color) ||
+      this.isAttackedByEagle(sq, color) ||
+      this.isAttackedByCardinal(sq, color) ||
+      this.isAttackedByDuke(sq, color) ||
+      this.isAttackedBySoldier(sq, color) ||
+      super.isAttackedByKing(sq, color) ||
+      super.isAttackedByQueen(sq, color)
+    );
+  }
+
+  isAttackedByTower(sq, color) {
+    return super.isAttackedBySlideNJump(sq, color, V.TOWER, V.steps[V.ROOK]);
+  }
+
+  isAttackedByEagle(sq, color) {
+    return super.isAttackedBySlideNJump(
+      sq, color, V.EAGLE, V.steps[V.KNIGHT], 1);
+  }
+
+  isAttackedByCardinal(sq, color) {
+    return super.isAttackedBySlideNJump(
+      sq, color, V.CARDINAL, V.steps[V.BISHOP]);
+  }
+
+  isAttackedByDuke(sq, color) {
+    return (
+      super.isAttackedBySlideNJump(
+        sq, color, V.DUKE,
+        V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1
+      )
+    );
+  }
+
+  isAttackedBySoldier([x, y], color) {
+    const shiftX = (color == 'w' ? 1 : -1); //shift from king
+    return super.isAttackedBySlideNJump(
+      [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1);
+  }
+
+  updateCastleFlags(move, piece) {
+    // Only black can castle:
+    const firstRank = 0;
+    if (piece == V.KING && move.appear[0].c == 'b')
+      this.castleFlags['b'] = [8, 8];
+    else if (
+      move.start.x == firstRank &&
+      this.castleFlags['b'].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1);
+      this.castleFlags['b'][flagIdx] = 8;
+    }
+    else if (
+      move.end.x == firstRank &&
+      this.castleFlags['b'].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1);
+      this.castleFlags['b'][flagIdx] = 8;
+    }
+  }
+
+  getCurrentScore() {
+    // Turn has changed:
+    const color = V.GetOppCol(this.turn);
+    const lastRank = (color == 'w' ? 0 : 7);
+    if (this.kingPos[color][0] == lastRank)
+      // The opposing edge is reached!
+      return color == "w" ? "1-0" : "0-1";
+    if (this.atLeastOneMove()) return "*";
+    // Game over
+    const oppCol = this.turn;
+    return (oppCol == "w" ? "0-1" : "1-0");
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      {},
+      ChessRules.VALUES,
+      {
+        t: 7,
+        e: 7,
+        c: 4,
+        d: 4,
+        s: 2
+      }
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+};
diff --git a/variants/Enpassant/class.js b/variants/Enpassant/class.js
new file mode 100644
index 0000000..cb21830
--- /dev/null
+++ b/variants/Enpassant/class.js
@@ -0,0 +1,209 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+
+export class EnpassantRules extends ChessRules {
+
+  static IsGoodEnpassant(enpassant) {
+    if (enpassant != "-") {
+      const squares = enpassant.split(",");
+      if (squares.length > 2) return false;
+      for (let sq of squares) {
+        const ep = V.SquareToCoords(sq);
+        if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
+      }
+    }
+    return true;
+  }
+
+  getPpath(b) {
+    return (b[1] == V.KNIGHT ? "Enpassant/" : "") + b;
+  }
+
+  getEpSquare(moveOrSquare) {
+    if (!moveOrSquare) return undefined;
+    if (typeof moveOrSquare === "string") {
+      const square = moveOrSquare;
+      if (square == "-") return undefined;
+      // Expand init + dest squares into a full path:
+      const init = V.SquareToCoords(square.substr(0, 2));
+      let newPath = [init];
+      if (square.length == 2) return newPath;
+      const dest = V.SquareToCoords(square.substr(2));
+      const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i]));
+      // Check if it's a knight(rider) movement:
+      let step = [0, 0];
+      if (delta[0] > 0 && delta[1] > 0 && delta[0] != delta[1]) {
+        // Knightrider
+        const minShift = Math.min(delta[0], delta[1]);
+        step[0] = (dest.x - init.x) / minShift;
+        step[1] = (dest.y - init.y) / minShift;
+      } else {
+        // "Sliders"
+        step = ['x', 'y'].map((i, idx) => {
+          return (dest[i] - init[i]) / delta[idx] || 0
+        });
+      }
+      let x = init.x + step[0],
+          y = init.y + step[1];
+      while (x != dest.x || y != dest.y) {
+        newPath.push({ x: x, y: y });
+        x += step[0];
+        y += step[1];
+      }
+      newPath.push(dest);
+      return newPath;
+    }
+    // Argument is a move: all intermediate squares are en-passant candidates,
+    // except if the moving piece is a king.
+    const move = moveOrSquare;
+    const piece = move.appear[0].p;
+    if (piece == V.KING ||
+      (
+        Math.abs(move.end.x-move.start.x) <= 1 &&
+        Math.abs(move.end.y-move.start.y) <= 1
+      )
+    ) {
+      return undefined;
+    }
+    const delta = [move.end.x-move.start.x, move.end.y-move.start.y];
+    let step = undefined;
+    if (piece == V.KNIGHT) {
+      const divisor = Math.min(Math.abs(delta[0]), Math.abs(delta[1]));
+      step = [delta[0]/divisor || 0, delta[1]/divisor || 0];
+    } else {
+      step = [
+        delta[0]/Math.abs(delta[0]) || 0,
+        delta[1]/Math.abs(delta[1]) || 0
+      ];
+    }
+    let res = [];
+    for (
+      let [x,y] = [move.start.x+step[0],move.start.y+step[1]];
+      x != move.end.x || y != move.end.y;
+      x += step[0], y += step[1]
+    ) {
+      res.push({ x: x, y: y });
+    }
+    // Add final square to know which piece is taken en passant:
+    res.push(move.end);
+    return res;
+  }
+
+  getEnpassantFen() {
+    const L = this.epSquares.length;
+    if (!this.epSquares[L - 1]) return "-"; //no en-passant
+    const epsq = this.epSquares[L - 1];
+    if (epsq.length <= 2) return epsq.map(V.CoordsToSquare).join("");
+    // Condensate path: just need initial and final squares:
+    return V.CoordsToSquare(epsq[0]) + V.CoordsToSquare(epsq[epsq.length - 1]);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    let moves = super.getPotentialMovesFrom([x,y]);
+    // Add en-passant captures from this square:
+    const L = this.epSquares.length;
+    if (!this.epSquares[L - 1]) return moves;
+    const squares = this.epSquares[L - 1];
+    const S = squares.length;
+    // Object describing the removed opponent's piece:
+    const pipoV = new PiPo({
+      x: squares[S-1].x,
+      y: squares[S-1].y,
+      c: V.GetOppCol(this.turn),
+      p: this.getPiece(squares[S-1].x, squares[S-1].y)
+    });
+    // Check if existing non-capturing moves could also capture en passant
+    moves.forEach(m => {
+      if (
+        m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere
+        m.vanish.length <= 1 &&
+        [...Array(S-1).keys()].some(i => {
+          return m.end.x == squares[i].x && m.end.y == squares[i].y;
+        })
+      ) {
+        m.vanish.push(pipoV);
+      }
+    });
+    // Special case of the king knight's movement:
+    if (this.getPiece(x, y) == V.KING) {
+      V.steps[V.KNIGHT].forEach(step => {
+        const endX = x + step[0];
+        const endY = y + step[1];
+        if (
+          V.OnBoard(endX, endY) &&
+          [...Array(S-1).keys()].some(i => {
+            return endX == squares[i].x && endY == squares[i].y;
+          })
+        ) {
+          let enpassantMove = this.getBasicMove([x, y], [endX, endY]);
+          enpassantMove.vanish.push(pipoV);
+          moves.push(enpassantMove);
+        }
+      });
+    }
+    return moves;
+  }
+
+  getEnpassantCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const squares = this.epSquares[Lep - 1];
+    let moves = [];
+    if (!!squares) {
+      const S = squares.length;
+      const taken = squares[S-1];
+      const pipoV = new PiPo({
+        x: taken.x,
+        y: taken.y,
+        p: this.getPiece(taken.x, taken.y),
+        c: this.getColor(taken.x, taken.y)
+      });
+      [...Array(S-1).keys()].forEach(i => {
+        const sq = squares[i];
+        if (sq.x == x + shiftX && Math.abs(sq.y - y) == 1) {
+          let enpassantMove = this.getBasicMove([x, y], [sq.x, sq.y]);
+          enpassantMove.vanish.push(pipoV);
+          moves.push(enpassantMove);
+        }
+      });
+    }
+    return moves;
+  }
+
+  // Remove the "onestep" condition: knight promote to knightrider:
+  getPotentialKnightMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT]);
+  }
+
+  filterValid(moves) {
+    const filteredMoves = super.filterValid(moves);
+    // If at least one full move made, everything is allowed:
+    if (this.movesCount >= 2)
+      return filteredMoves;
+    // Else, forbid captures:
+    return filteredMoves.filter(m => m.vanish.length == 1);
+  }
+
+  isAttackedByKnight(sq, color) {
+    return this.isAttackedBySlideNJump(
+      sq,
+      color,
+      V.KNIGHT,
+      V.steps[V.KNIGHT]
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 4,
+      b: 3,
+      q: 9,
+      k: 1000
+    };
+  }
+
+};
diff --git a/variants/Evolution/class.js b/variants/Evolution/class.js
new file mode 100644
index 0000000..5250089
--- /dev/null
+++ b/variants/Evolution/class.js
@@ -0,0 +1,34 @@
+import { ChessRules } from "@/base_rules";
+
+export class EvolutionRules extends ChessRules {
+
+  getPotentialMovesFrom([x, y]) {
+    let moves = super.getPotentialMovesFrom([x, y]);
+    const c = this.getColor(x, y);
+    const piece = this.getPiece(x, y);
+    if (
+      [V.BISHOP, V.ROOK, V.QUEEN].includes(piece) &&
+      (c == 'w' && x == 7) || (c == 'b' && x == 0)
+    ) {
+      // Move from first rank
+      const forward = (c == 'w' ? -1 : 1);
+      for (let shift of [-2, 0, 2]) {
+        if (
+          (piece == V.ROOK && shift != 0) ||
+          (piece == V.BISHOP && shift == 0)
+        ) {
+          continue;
+        }
+        if (
+          V.OnBoard(x+2*forward, y+shift) &&
+          this.board[x+forward][y+shift/2] != V.EMPTY &&
+          this.getColor(x+2*forward, y+shift) != c
+        ) {
+          moves.push(this.getBasicMove([x,y], [x+2*forward,y+shift]));
+        }
+      }
+    }
+    return moves;
+  }
+
+};
diff --git a/variants/Extinction/class.js b/variants/Extinction/class.js
new file mode 100644
index 0000000..e42e294
--- /dev/null
+++ b/variants/Extinction/class.js
@@ -0,0 +1,118 @@
+import { ChessRules } from "@/base_rules";
+
+export class ExtinctionRules extends ChessRules {
+
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
+    );
+  }
+
+  static IsGoodPosition(position) {
+    if (!ChessRules.IsGoodPosition(position)) return false;
+    // Also check that each piece type is present
+    const rows = position.split("/");
+    let pieces = {};
+    for (let row of rows) {
+      for (let i = 0; i < row.length; i++) {
+        if (isNaN(parseInt(row[i], 10)) && !pieces[row[i]])
+          pieces[row[i]] = true;
+      }
+    }
+    if (Object.keys(pieces).length != 12) return false;
+    return true;
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const pos = V.ParseFen(fen).position;
+    // NOTE: no need for safety "|| []", because each piece type is present
+    // (otherwise game is already over!)
+    this.material = {
+      w: {
+        [V.KING]: pos.match(/K/g).length,
+        [V.QUEEN]: pos.match(/Q/g).length,
+        [V.ROOK]: pos.match(/R/g).length,
+        [V.KNIGHT]: pos.match(/N/g).length,
+        [V.BISHOP]: pos.match(/B/g).length,
+        [V.PAWN]: pos.match(/P/g).length
+      },
+      b: {
+        [V.KING]: pos.match(/k/g).length,
+        [V.QUEEN]: pos.match(/q/g).length,
+        [V.ROOK]: pos.match(/r/g).length,
+        [V.KNIGHT]: pos.match(/n/g).length,
+        [V.BISHOP]: pos.match(/b/g).length,
+        [V.PAWN]: pos.match(/p/g).length
+      }
+    };
+  }
+
+  // TODO: verify this assertion
+  atLeastOneMove() {
+    return true; //always at least one possible move
+  }
+
+  filterValid(moves) {
+    return moves; //there is no check
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    // Treat the promotion case: (not the capture part)
+    if (move.appear[0].p != move.vanish[0].p) {
+      this.material[move.appear[0].c][move.appear[0].p]++;
+      this.material[move.appear[0].c][V.PAWN]--;
+    }
+    if (move.vanish.length == 2 && move.appear.length == 1)
+      //capture
+      this.material[move.vanish[1].c][move.vanish[1].p]--;
+  }
+
+  postUndo(move) {
+    super.postUndo(move);
+    if (move.appear[0].p != move.vanish[0].p) {
+      this.material[move.appear[0].c][move.appear[0].p]--;
+      this.material[move.appear[0].c][V.PAWN]++;
+    }
+    if (move.vanish.length == 2 && move.appear.length == 1)
+      this.material[move.vanish[1].c][move.vanish[1].p]++;
+  }
+
+  getCurrentScore() {
+    if (this.atLeastOneMove()) {
+      // Game not over?
+      const color = this.turn;
+      if (
+        Object.keys(this.material[color]).some(p => {
+          return this.material[color][p] == 0;
+        })
+      ) {
+        return this.turn == "w" ? "0-1" : "1-0";
+      }
+      return "*";
+    }
+    return this.turn == "w" ? "0-1" : "1-0"; //NOTE: currently unreachable...
+  }
+
+  evalPosition() {
+    const color = this.turn;
+    if (
+      Object.keys(this.material[color]).some(p => {
+        return this.material[color][p] == 0;
+      })
+    ) {
+      // Very negative (resp. positive)
+      // if white (reps. black) pieces set is incomplete
+      return (color == "w" ? -1 : 1) * V.INFINITY;
+    }
+    return super.evalPosition();
+  }
+
+};
diff --git a/variants/Fanorona/class.js b/variants/Fanorona/class.js
new file mode 100644
index 0000000..e1e653a
--- /dev/null
+++ b/variants/Fanorona/class.js
@@ -0,0 +1,342 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class FanoronaRules extends ChessRules {
+
+  static get Options() {
+    return null;
+  }
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get Monochrome() {
+    return true;
+  }
+
+  static get Lines() {
+    let lines = [];
+    // Draw all inter-squares lines, shifted:
+    for (let i = 0; i < V.size.x; i++)
+      lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
+    for (let j = 0; j < V.size.y; j++)
+      lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
+    const columnDiags = [
+      [[0.5, 0.5], [2.5, 2.5]],
+      [[0.5, 2.5], [2.5, 0.5]],
+      [[2.5, 0.5], [4.5, 2.5]],
+      [[4.5, 0.5], [2.5, 2.5]]
+    ];
+    for (let j of [0, 2, 4, 6]) {
+      lines = lines.concat(
+        columnDiags.map(L => [[L[0][0], L[0][1] + j], [L[1][0], L[1][1] + j]])
+      );
+    }
+    return lines;
+  }
+
+  static get Notoodark() {
+    return true;
+  }
+
+  static GenRandInitFen() {
+    return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0";
+  }
+
+  setOtherVariables(fen) {
+    // Local stack of captures during a turn (squares + directions)
+    this.captures = [ [] ];
+  }
+
+  static get size() {
+    return { x: 5, y: 9 };
+  }
+
+  getPiece() {
+    return V.PAWN;
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        if (row[i].toLowerCase() == V.PAWN) sumElts++;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num) || num <= 0) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    return true;
+  }
+
+  getPpath(b) {
+    return "Fanorona/" + b;
+  }
+
+  getPPpath(m, orientation) {
+    // m.vanish.length >= 2, first capture gives direction
+    const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end);
+    const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y];
+    const multStep = (orientation == 'w' ? 1 : -1);
+    const normalizedStep = [
+      multStep * step[0] / Math.abs(step[0]),
+      multStep * step[1] / Math.abs(step[1])
+    ];
+    return (
+      "Fanorona/arrow_" +
+      (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0)
+    );
+  }
+
+  // After moving, add stones captured in "step" direction from new location
+  // [x, y] to mv.vanish (if any captured stone!)
+  addCapture([x, y], step, move) {
+    let [i, j] = [x + step[0], y + step[1]];
+    const oppCol = V.GetOppCol(move.vanish[0].c);
+    while (
+      V.OnBoard(i, j) &&
+      this.board[i][j] != V.EMPTY &&
+      this.getColor(i, j) == oppCol
+    ) {
+      move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN }));
+      i += step[0];
+      j += step[1];
+    }
+    return (move.vanish.length >= 2);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      var c = captures[L-1];
+      if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1])
+        return [];
+    }
+    const oppCol = V.GetOppCol(this.turn);
+    let steps = V.steps[V.ROOK];
+    if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+    let moves = [];
+    for (let s of steps) {
+      if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) {
+        // Add a move to say "I'm done capturing"
+        moves.push(
+          new Move({
+            appear: [],
+            vanish: [],
+            start: { x: x, y: y },
+            end: { x: x - s[0], y: y - s[1] }
+          })
+        );
+        continue;
+      }
+      let [i, j] = [x + s[0], y + s[1]];
+      if (captures.some(c => c.square.x == i && c.square.y == j)) continue;
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        // The move is potentially allowed. Might lead to 2 different captures
+        let mv = super.getBasicMove([x, y], [i, j]);
+        const capt = this.addCapture([i, j], s, mv);
+        if (capt) {
+          moves.push(mv);
+          mv = super.getBasicMove([x, y], [i, j]);
+        }
+        const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv);
+        if (capt_bw) moves.push(mv);
+        // Captures take priority (if available)
+        if (!capt && !capt_bw && L == 0) moves.push(mv);
+      }
+    }
+    return moves;
+  }
+
+  atLeastOneCapture() {
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    const L = captures.length;
+    if (L > 0) {
+      // If some adjacent enemy stone, with free space to capture it,
+      // toward a square not already visited, through a different step
+      // from last one: then yes.
+      const c = captures[L-1];
+      const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]];
+      let steps = V.steps[V.ROOK];
+      if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]);
+      // TODO: half of the steps explored are redundant
+      for (let s of steps) {
+        if (s[0] == c.step[0] && s[1] == c.step[1]) continue;
+        const [i, j] = [x + s[0], y + s[1]];
+        if (
+          !V.OnBoard(i, j) ||
+          this.board[i][j] != V.EMPTY ||
+          captures.some(c => c.square.x == i && c.square.y == j)
+        ) {
+          continue;
+        }
+        if (
+          V.OnBoard(i + s[0], j + s[1]) &&
+          this.board[i + s[0]][j + s[1]] != V.EMPTY &&
+          this.getColor(i + s[0], j + s[1]) == oppCol
+        ) {
+          return true;
+        }
+        if (
+          V.OnBoard(x - s[0], y - s[1]) &&
+          this.board[x - s[0]][y - s[1]] != V.EMPTY &&
+          this.getColor(x - s[0], y - s[1]) == oppCol
+        ) {
+          return true;
+        }
+      }
+      return false;
+    }
+    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 &&
+          this.getColor(i, j) == color &&
+          // TODO: this could be more efficient
+          this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2)
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  static KeepCaptures(moves) {
+    return moves.filter(m => m.vanish.length >= 2);
+  }
+
+  getPossibleMovesFrom(sq) {
+    let moves = this.getPotentialMovesFrom(sq);
+    const L0 = this.captures.length;
+    const captures = this.captures[L0 - 1];
+    if (captures.length > 0) return this.getPotentialMovesFrom(sq);
+    const captureMoves = V.KeepCaptures(moves);
+    if (captureMoves.length > 0) return captureMoves;
+    if (this.atLeastOneCapture()) return [];
+    return moves;
+  }
+
+  getAllValidMoves() {
+    const moves = super.getAllValidMoves();
+    if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves);
+    return moves;
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  play(move) {
+    const color = this.turn;
+    move.turn = color; //for undo
+    V.PlayOnBoard(this.board, move);
+    if (move.vanish.length >= 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.push({
+        square: move.start,
+        step: [move.end.x - move.start.x, move.end.y - move.start.y]
+      });
+      if (this.atLeastOneCapture())
+        // There could be other captures (optional)
+        move.notTheEnd = true;
+    }
+    if (!move.notTheEnd) {
+      this.turn = V.GetOppCol(color);
+      this.movesCount++;
+      this.captures.push([]);
+    }
+  }
+
+  undo(move) {
+    V.UndoOnBoard(this.board, move);
+    if (!move.notTheEnd) {
+      this.turn = move.turn;
+      this.movesCount--;
+      this.captures.pop();
+    }
+    if (move.vanish.length >= 2) {
+      const L0 = this.captures.length;
+      let captures = this.captures[L0 - 1];
+      captures.pop();
+    }
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    // If no stones on board, I lose
+    if (
+      this.board.every(b => {
+        return b.every(cell => {
+          return (cell == "" || cell[0] != color);
+        });
+      })
+    ) {
+      return (color == 'w' ? "0-1" : "1-0");
+    }
+    return "*";
+  }
+
+  getComputerMove() {
+    const moves = this.getAllValidMoves();
+    if (moves.length == 0) return null;
+    const color = this.turn;
+    // Capture available? If yes, play it
+    let captures = moves.filter(m => m.vanish.length >= 2);
+    let mvArray = [];
+    while (captures.length >= 1) {
+      // Then just pick random captures (trying to maximize)
+      let candidates = captures.filter(c => !!c.notTheEnd);
+      let mv = null;
+      if (candidates.length >= 1) mv = candidates[randInt(candidates.length)];
+      else mv = captures[randInt(captures.length)];
+      this.play(mv);
+      mvArray.push(mv);
+      captures = (this.turn == color ? this.getAllValidMoves() : []);
+    }
+    if (mvArray.length >= 1) {
+      for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+      return mvArray;
+    }
+    // Just play a random move, which if possible does not let a capture
+    let candidates = [];
+    for (let m of moves) {
+      this.play(m);
+      if (!this.atLeastOneCapture()) candidates.push(m);
+      this.undo(m);
+    }
+    if (candidates.length >= 1) return candidates[randInt(candidates.length)];
+    return moves[randInt(moves.length)];
+  }
+
+  getNotation(move) {
+    if (move.appear.length == 0) return "stop";
+    return (
+      V.CoordsToSquare(move.start) +
+      V.CoordsToSquare(move.end) +
+      (move.vanish.length >= 2 ? "X" : "")
+    );
+  }
+
+};
diff --git a/variants/Sleepy/class.js b/variants/Sleepy/class.js
new file mode 100644
index 0000000..e58ceae
--- /dev/null
+++ b/variants/Sleepy/class.js
@@ -0,0 +1,110 @@
+import ChessRules from "/base_rules.js";
+import PiPo from "/utils/PiPo.js";
+import Move from "/utils/Move.js";
+
+export default class SleepyRules extends ChessRules {
+
+  static get Options() {
+    return {
+      select: C.Options.select,
+      input: {},
+      styles: ["cylinder"] //TODO
+    };
+  }
+
+  setOtherVariables(fenParsed) {
+    super.setOtherVariables(fenParsed);
+    // Stack of "last move" only for intermediate chaining
+    this.lastMoveEnd = [];
+  }
+
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    const L = this.lastMoveEnd.length;
+    const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null);
+    if (
+      this.board[ex][ey] == "" ||
+      this.getColor(ex, ey) == C.GetOppTurn(this.turn)
+    ) {
+      if (piece && !tr)
+        tr = {c: this.turn, p: piece};
+      let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
+      if (piece)
+        mv.vanish.pop(); //end of a chain: initial piece remains
+      return mv;
+    }
+    // (Self)Capture: initial, or inside a chain
+    const initPiece = (piece || this.getPiece(sx, sy)),
+          destPiece = this.getPiece(ex, ey);
+    let mv = new Move({
+      start: {x: sx, y: sy},
+      end: {x: ex, y: ey},
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.turn,
+          p: (!!tr ? tr.p : initPiece)
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.turn,
+          p: destPiece
+        })
+      ]
+    });
+    if (!piece) {
+      // Initial capture
+      mv.vanish.unshift(
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: this.turn,
+          p: initPiece
+        })
+      );
+    }
+    mv.chained = destPiece; //easier (no need to detect it)
+//    mv.drag = {c: this.turn, p: initPiece}; //TODO: doesn't work
+    return mv;
+  }
+
+  getPiece(x, y) {
+    const L = this.lastMoveEnd.length;
+    if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y)
+      return this.lastMoveEnd[L-1].p;
+    return super.getPiece(x, y);
+  }
+
+  getPotentialMovesFrom([x, y], color) {
+    const L = this.lastMoveEnd.length;
+    if (
+      L >= 1 &&
+      (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
+    ) {
+      // A self-capture was played: wrong square
+      return [];
+    }
+    return super.getPotentialMovesFrom([x, y], color);
+  }
+
+  isLastMove(move) {
+    return !move.chained;
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    if (!!move.chained) {
+      this.lastMoveEnd.push({
+        x: move.end.x,
+        y: move.end.y,
+        p: move.chained
+      });
+    }
+    else
+      this.lastMoveEnd = [];
+  }
+
+};
diff --git a/variants/Sleepy/rules.html b/variants/Sleepy/rules.html
new file mode 100644
index 0000000..f27cd96
--- /dev/null
+++ b/variants/Sleepy/rules.html
@@ -0,0 +1,7 @@
+<p>
+  You can "capture" your own pieces, and then move them from the capturing
+  square in the same turn, with potential chaining if the captured unit
+  makes a self-capture too.
+</p>
+
+<p class="author">Benjamin Auder (2021).</p>
diff --git a/variants/Sleepy/style.css b/variants/Sleepy/style.css
new file mode 100644
index 0000000..a3550bc
--- /dev/null
+++ b/variants/Sleepy/style.css
@@ -0,0 +1 @@
+@import url("/base_pieces.css");
-- 
2.48.1


From ab2ca6784b154f0fd6183b908df124063a45f876 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 30 Jan 2025 18:26:19 +0100
Subject: [PATCH 13/16] update

---
 base_rules.js            | 3 +--
 variants/Dynamo/class.js | 8 ++++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/base_rules.js b/base_rules.js
index 3d8e463..888fdcf 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -2235,8 +2235,7 @@ export default class ChessRules {
 
   // 'color' arg because some variants (e.g. Refusal) check opponent moves
   filterValid(moves, color) {
-    if (!color)
-      color = this.turn;
+    color = color || this.turn;
     const oppCols = this.getOppCols(color);
     let kingPos = this.searchKingPos(color);
     return moves.filter(m => {
diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index 7b835e0..d726522 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -181,8 +181,8 @@ export default class DynamoRules extends ChessRules {
   // NOTE: for pushes, play the pushed piece first.
   //       for pulls: play the piece doing the action first
   // NOTE: to push a piece out of the board, make it slide until its king
-  getPotentialMovesFrom([x, y]) {
-    const color = this.turn;
+  getPotentialMovesFrom([x, y], color) {
+    const color = color || this.turn;
     const sqCol = this.getColor(x, y);
     const pawnShift = (color == 'w' ? -1 : 1);
     const pawnStartRank = (color == 'w' ? 6 : 1);
@@ -486,6 +486,10 @@ export default class DynamoRules extends ChessRules {
     return [];
   }
 
+  getAllPotentialMoves(color) {
+    
+  }
+
   getSlideNJumpMoves([x, y], steps, oneStep) {
     let moves = [];
     const c = this.getColor(x, y);
-- 
2.48.1


From 4a2da562da7627a70cfe0bc36db231adc00afe02 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 5 Feb 2025 12:02:32 +0100
Subject: [PATCH 14/16] Dynamo

---
 variants/Dynamo/class.js | 170 +++++++++++++++++++--------------------
 1 file changed, 82 insertions(+), 88 deletions(-)

diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index d726522..d9bb855 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -53,6 +53,17 @@ export default class DynamoRules extends ChessRules {
     return res;
   }
 
+  addExitMove(moves, [x, y], kp) {
+    moves.push(
+      new Move({
+        start: { x: x, y: y },
+        end: { x: kp[color][0], y: kp[color][1] },
+        appear: [],
+        vanish: [{ x: x, y: y, c: color, p: piece }]
+      })
+    );
+  }
+
   // Step is right, just add (push/pull) moves in this direction
   // Direction is assumed normalized.
   getMovesInDirection([x, y], [dx, dy], nbSteps, kp) {
@@ -78,17 +89,8 @@ export default class DynamoRules extends ChessRules {
       i += dx;
       j += dy;
     }
-    if (!this.onBoard(i, j) && piece != 'k') {
-      // Add special "exit" move, by "taking king"
-      moves.push(
-        new Move({
-          start: { x: x, y: y },
-          end: { x: kp[0], y: kp[1] },
-          appear: [],
-          vanish: [{ x: x, y: y, c: color, p: piece }]
-        })
-      );
-    }
+    if (!this.onBoard(i, j) && piece != 'k')
+      this.addExitMove(moves, [x, y], kp);
     return moves;
   }
 
@@ -178,6 +180,36 @@ export default class DynamoRules extends ChessRules {
     );
   }
 
+  // Test if a piece can suicide
+  canReachBorder(x, y) {
+    const p = this.getPiece(x, y);
+    switch (p) {
+      case 'p':
+      case 'k':
+        return false;
+      case 'n':
+        return (
+          x <= 1 || x >= this.size.x - 2 || y <= 1 || y >= this.size.y - 2
+        );
+    }
+    // Slider
+    let steps = [];
+    if (['r', 'q'].includes(p))
+      steps = steps.concat(this.pieces()['r'].both[0].steps);
+    if (['b', 'q'].includes(p))
+      steps = steps.concat(this.pieces()['b'].both[0].steps);
+    for (let s of steps) {
+      let [i, j] = [x + s[0], y + s[1]];
+      while (this.onBoard(i, j) && this.board[i][j] == "") {
+        i += s[0];
+        j += s[1];
+      }
+      if (!this.onBoard(i, j))
+        return true;
+    }
+    return false;
+  }
+
   // NOTE: for pushes, play the pushed piece first.
   //       for pulls: play the piece doing the action first
   // NOTE: to push a piece out of the board, make it slide until its king
@@ -190,7 +222,7 @@ export default class DynamoRules extends ChessRules {
       return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end);
     };
     if (this.subTurn == 1) {
-      const kp = this.searchKingPos(color);
+      const kp = [ this.searchKingPos('w')[0], this.searchKingPos('b')[0] ];
       const addMoves = (dir, nbSteps) => {
         const newMoves =
           this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp)
@@ -199,21 +231,12 @@ export default class DynamoRules extends ChessRules {
         Array.prototype.push.apply(moves, newMoves);
       };
       // Free to play any move (if piece of my color):
-      let moves =
-        sqCol == color
-          ? super.getPotentialMovesFrom([x, y])
-          : [];
-      // There may be several suicide moves: keep only one
-      let hasExit = false;
-      moves = moves.filter(m => {
-        const suicide = (m.appear.length == 0);
-        if (suicide) {
-          if (hasExit)
-            return false;
-          hasExit = true;
-        }
-        return true;
-      });
+      let moves = [];
+      if (sqCol == color) {
+        moves = super.getPotentialMovesFrom([x, y])
+        if (this.canReachBorder(x, y))
+          this.addSuicideMove(moves, [x, y], kp);
+      }
       // Structure to avoid adding moves twice (can be action & move)
       let movesHash = {};
       moves.forEach(m => { movesHash[getMoveHash(m)] = true; });
@@ -486,50 +509,6 @@ export default class DynamoRules extends ChessRules {
     return [];
   }
 
-  getAllPotentialMoves(color) {
-    
-  }
-
-  getSlideNJumpMoves([x, y], steps, oneStep) {
-    let moves = [];
-    const c = this.getColor(x, y);
-    const piece = this.getPiece(x, y);
-    outerLoop: for (let step of steps) {
-      let i = x + step[0];
-      let j = y + step[1];
-      while (this.onBoard(i, j) && this.board[i][j] == "") {
-        moves.push(this.getBasicMove([x, y], [i, j]));
-        if (oneStep)
-          continue outerLoop;
-        i += step[0];
-        j += step[1];
-      }
-      if (this.onBoard(i, j)) {
-        if (this.canTake([x, y], [i, j]))
-          moves.push(this.getBasicMove([x, y], [i, j]));
-      }
-      else {
-        // Add potential board exit (suicide), except for the king
-        if (piece != 'k') {
-          moves.push({
-            start: { x: x, y: y},
-            end: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
-            appear: [],
-            vanish: [
-              new PiPo({
-                x: x,
-                y: y,
-                c: c,
-                p: piece
-              })
-            ]
-          });
-        }
-      }
-    }
-    return moves;
-  }
-
   // Does m2 un-do m1 ? (to disallow undoing actions)
   oppositeMoves(m1, m2) {
     const isEqual = (av1, av2) => {
@@ -567,7 +546,19 @@ export default class DynamoRules extends ChessRules {
     }
   }
 
-// TODO: re-write just for here getAllPotentialMoves() ?
+  getAllPotentialMoves() {
+    const color = this.turn;
+    let moves = [];
+    for (let i=0; i<this.size.x; i++) {
+      for (let j=0; j<this.size.y; j++) {
+        if (this.board[x][y] != "") {
+          Array.prototype.push.apply(
+            moves, this.getPotentialMovesFrom([i, j], color));
+        }
+      }
+    }
+    return moves;
+  }
 
   filterValid(moves) {
     const color = this.turn;
@@ -577,7 +568,7 @@ export default class DynamoRules extends ChessRules {
         // A move is valid either if it doesn't result in a check,
         // or if a second move is possible to counter the check
         // (not undoing a potential move + action of the opponent)
-        this.playOnBoard(m);
+        this.play(m);
         let res = this.underCheck(color);
         if (this.subTurn == 2) {
           let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
@@ -597,7 +588,7 @@ export default class DynamoRules extends ChessRules {
             }
           }
         }
-        this.undoOnBoard(m);
+        this.undo(m);
         return !res;
       });
     }
@@ -659,19 +650,11 @@ export default class DynamoRules extends ChessRules {
     }
   }
 
-  play(move) {
-//    if (this.subTurn == 1 && move.vanish.length == 0) {
-//      // Patch to work with old format: (TODO: remove later)
-//      move.ignore = true;
-//      return;
-//    }
-
-    // In preplay ?
-    this.updateCastleFlags(move);
-
-    const oppCol = C.GetOppTurn(color);
-
+  play(move, filterValid) {
+    if (!filterValid)
+      this.updateCastleFlags(move);
     const color = this.turn;
+    const oppCol = C.GetOppTurn(color);
     const gotoNext = (mv) => {
       const L = this.firstMove.length;
       this.amoves.push(this.getAmove(this.firstMove[L-1], mv));
@@ -686,7 +669,7 @@ export default class DynamoRules extends ChessRules {
       this.subTurn = 2;
       this.firstMove.push(move);
       if (
-        // Condition is true on empty arrays: //TODO: getAllPotentialMoves doesn't exist
+        // Condition is true on empty arrays:
         this.getAllPotentialMoves().every(m => {
           this.playOnBoard(m);
           const res = this.underCheck([kp], oppCol); //TODO: find kp first
@@ -698,7 +681,18 @@ export default class DynamoRules extends ChessRules {
         gotoNext(this.getEmptyMove());
       }
     }
-    this.postPlay(move);
+  }
+
+  undo(move) {
+    this.undoOnBoard(this.board, move);
+    if (this.subTurn == 1) {
+      this.amoves.pop();
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+    }
+    if (move.subTurn == 1) this.firstMove.pop();
+    this.subTurn = move.subTurn;
+    this.toOldKingPos(move);
   }
 
 };
-- 
2.48.1


From 07d16218df6bba6571e9dcd002b4a785bcf2c51b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 5 Feb 2025 23:14:13 +0100
Subject: [PATCH 15/16] Debug Dynamo

---
 variants/Dynamo/class.js | 32 ++++++++++++++++++++++----------
 1 file changed, 22 insertions(+), 10 deletions(-)

diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index d9bb855..08519f5 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -1,3 +1,4 @@
+import Move from "/utils/Move.js";
 import ChessRules from "/base_rules.js";
 
 export default class DynamoRules extends ChessRules {
@@ -57,9 +58,10 @@ export default class DynamoRules extends ChessRules {
     moves.push(
       new Move({
         start: { x: x, y: y },
-        end: { x: kp[color][0], y: kp[color][1] },
+        end: { x: kp[0], y: kp[1] },
         appear: [],
-        vanish: [{ x: x, y: y, c: color, p: piece }]
+        vanish: [
+          { x: x, y: y, c: this.getColor(x, y), p: this.getPiece(x, y) }]
       })
     );
   }
@@ -214,7 +216,7 @@ export default class DynamoRules extends ChessRules {
   //       for pulls: play the piece doing the action first
   // NOTE: to push a piece out of the board, make it slide until its king
   getPotentialMovesFrom([x, y], color) {
-    const color = color || this.turn;
+    color = color || this.turn;
     const sqCol = this.getColor(x, y);
     const pawnShift = (color == 'w' ? -1 : 1);
     const pawnStartRank = (color == 'w' ? 6 : 1);
@@ -222,7 +224,7 @@ export default class DynamoRules extends ChessRules {
       return C.CoordsToSquare(m.start) + C.CoordsToSquare(m.end);
     };
     if (this.subTurn == 1) {
-      const kp = [ this.searchKingPos('w')[0], this.searchKingPos('b')[0] ];
+      const kp = this.searchKingPos(color)[0];
       const addMoves = (dir, nbSteps) => {
         const newMoves =
           this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps, kp)
@@ -235,7 +237,7 @@ export default class DynamoRules extends ChessRules {
       if (sqCol == color) {
         moves = super.getPotentialMovesFrom([x, y])
         if (this.canReachBorder(x, y))
-          this.addSuicideMove(moves, [x, y], kp);
+          this.addExitMove(moves, [x, y], kp);
       }
       // Structure to avoid adding moves twice (can be action & move)
       let movesHash = {};
@@ -562,13 +564,14 @@ export default class DynamoRules extends ChessRules {
 
   filterValid(moves) {
     const color = this.turn;
-    const La = this.amoves.length;
+    const La = this.amoves.length; //TODO: debug
     if (this.subTurn == 1) {
       return moves.filter(m => {
         // A move is valid either if it doesn't result in a check,
         // or if a second move is possible to counter the check
         // (not undoing a potential move + action of the opponent)
         this.play(m);
+        const kp = this.searchKingPos(color);
         let res = this.underCheck(color);
         if (this.subTurn == 2) {
           let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
@@ -576,7 +579,10 @@ export default class DynamoRules extends ChessRules {
             const moves2 = this.getAllPotentialMoves();
             for (let m2 of moves2) {
               this.play(m2);
-              const res2 = this.underCheck(color); //TODO: + square
+              let cur_kp = kp;
+              if (m2.appear[0].p == 'k')
+                cur_kp = [m2.appear[0].x, m2.appear[0].y];
+              const res2 = this.underCheck(cur_kp, color);
               const amove = this.getAmove(m, m2);
               isOpposite =
                 La > 0 && this.oppositeMoves(this.amoves[La-1], amove);
@@ -668,11 +674,15 @@ export default class DynamoRules extends ChessRules {
     else {
       this.subTurn = 2;
       this.firstMove.push(move);
+      const kp = this.searchKingPos(color);
       if (
         // Condition is true on empty arrays:
         this.getAllPotentialMoves().every(m => {
           this.playOnBoard(m);
-          const res = this.underCheck([kp], oppCol); //TODO: find kp first
+          let cur_kp = kp;
+          if (m.appear[0].p == 'k')
+            cur_kp = [m.appear[0].x, m.appear[0].y];
+          const res = this.underCheck(cur_kp, oppCol);
           this.undoOnBoard(m);
           return res;
         })
@@ -683,14 +693,16 @@ export default class DynamoRules extends ChessRules {
     }
   }
 
+  // For filterValid()
   undo(move) {
     this.undoOnBoard(this.board, move);
     if (this.subTurn == 1) {
       this.amoves.pop();
-      this.turn = V.GetOppCol(this.turn);
+      this.turn = C.GetOppTurn(this.turn);
       this.movesCount--;
     }
-    if (move.subTurn == 1) this.firstMove.pop();
+    if (move.subTurn == 1)
+      this.firstMove.pop();
     this.subTurn = move.subTurn;
     this.toOldKingPos(move);
   }
-- 
2.48.1


From c5fb8354ad889ef45b2483525cd79272372be8e6 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 6 Feb 2025 11:01:58 +0100
Subject: [PATCH 16/16] update

---
 base_rules.js            | 16 ------------
 variants/Dynamo/class.js | 53 +++++++++++++---------------------------
 2 files changed, 17 insertions(+), 52 deletions(-)

diff --git a/base_rules.js b/base_rules.js
index 888fdcf..81be6fd 100644
--- a/base_rules.js
+++ b/base_rules.js
@@ -175,22 +175,6 @@ export default class ChessRules {
     return Object.values(cd).map(c => c.toString(36)).join("");
   }
 
-  // c10 --> 02 (assuming 10 rows)
-  static SquareFromUsual(sq) {
-    return (
-      (this.size.x - parseInt(sq.substring(1), 10)).toString(36) +
-      (sq.charCodeAt(0) - 97).toString(36)
-    );
-  }
-
-  // 02 --> c10
-  static UsualFromSquare(sq) {
-    return (
-      String.fromCharCode(parseInt(sq.charAt(1), 36) + 97) +
-      (this.size.x - parseInt(sq.charAt(0), 36)).toString(10)
-    );
-  }
-
   coordsToId(cd) {
     if (typeof cd.x == "number") {
       return (
diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js
index 08519f5..7744293 100644
--- a/variants/Dynamo/class.js
+++ b/variants/Dynamo/class.js
@@ -30,27 +30,14 @@ export default class DynamoRules extends ChessRules {
     this.subTurn = 1;
     // Last action format: e2h5/d1g4 for queen on d1 pushing pawn to h5
     // for example, and moving herself to g4. If just move: e2h5
-    this.lastAction = [];
-    if (fenParsed.amove != '-') {
-      this.lastAction = fenParsed.amove.split('/').map(a => {
-        return {
-          c1: C.SquareToCoords(C.SquareFromUsual(a.substr(0, 2))),
-          c2: C.SquareToCoords(C.SquareFromUsual(a.substr(2, 2)))
-        };
-      });
-    }
+    this.amove = [];
+    if (fenParsed.amove != '-')
+      this.amove = JSON.parse(fenParsed.amove);
   }
 
   getPartFen(o) {
     let res = super.getPartFen(o);
-    if (o.init)
-      res["amove"] = '-';
-    else {
-      res["amove"] = this.lastAction.map(a => {
-        C.UsualFromSquare(C.CoordsToSquare(a.c1)) +
-        C.UsualFromSquare(C.CoordsToSquare(a.c2))
-      }).join('/');
-    }
+    res["amove"] = (o.init ? '-' : JSON.stringify(this.amove));
     return res;
   }
 
@@ -539,7 +526,6 @@ export default class DynamoRules extends ChessRules {
     );
   }
 
-  // TODO: just stack in this.lastAction instead
   getAmove(move1, move2) {
     // Just merge (one is action one is move, one may be empty)
     return {
@@ -562,9 +548,10 @@ export default class DynamoRules extends ChessRules {
     return moves;
   }
 
+// TODO: I over-simplified, amove need to be saved for after undos
+
   filterValid(moves) {
     const color = this.turn;
-    const La = this.amoves.length; //TODO: debug
     if (this.subTurn == 1) {
       return moves.filter(m => {
         // A move is valid either if it doesn't result in a check,
@@ -574,7 +561,7 @@ export default class DynamoRules extends ChessRules {
         const kp = this.searchKingPos(color);
         let res = this.underCheck(color);
         if (this.subTurn == 2) {
-          let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m);
+          let isOpposite = this.oppositeMoves(this.amove, m);
           if (res || isOpposite) {
             const moves2 = this.getAllPotentialMoves();
             for (let m2 of moves2) {
@@ -584,8 +571,7 @@ export default class DynamoRules extends ChessRules {
                 cur_kp = [m2.appear[0].x, m2.appear[0].y];
               const res2 = this.underCheck(cur_kp, color);
               const amove = this.getAmove(m, m2);
-              isOpposite =
-                La > 0 && this.oppositeMoves(this.amoves[La-1], amove);
+              isOpposite = this.oppositeMoves(this.amove, amove);
               this.undo(m2);
               if (!res2 && !isOpposite) {
                 res = false;
@@ -605,8 +591,8 @@ export default class DynamoRules extends ChessRules {
       super.filterValid(
         moves.filter(m => {
           // Move shouldn't undo another:
-          const amove = this.getAmove(this.firstMove[Lf-1], m);
-          return !this.oppositeMoves(this.amoves[La-1], amove);
+          const amove = this.getAmove(this.firstMove, m);
+          return !this.oppositeMoves(this.amove, amove);
         })
       )
     );
@@ -629,13 +615,12 @@ export default class DynamoRules extends ChessRules {
       return null;
     // If subTurn == 2 && square is empty && !underCheck && !isOpposite,
     // then return an empty move, allowing to "pass" subTurn2
-    const La = this.amoves.length;
-    const Lf = this.firstMove.length;
+    const kp = this.searchKingPos(this.turn);
     if (
       this.subTurn == 2 &&
-      this.board[square[0]][square[1]] == V.EMPTY &&
-      !this.underCheck(this.turn) &&
-      (La == 0 || !this.oppositeMoves(this.amoves[La-1], this.firstMove[Lf-1]))
+      this.board[square[0]][square[1]] == "" &&
+      !this.underCheck(kp, C.GetOppTurn(this.turn)) &&
+      !this.oppositeMoves(this.amove, this.firstMove))
     ) {
       return this.getEmptyMove();
     }
@@ -661,9 +646,9 @@ export default class DynamoRules extends ChessRules {
       this.updateCastleFlags(move);
     const color = this.turn;
     const oppCol = C.GetOppTurn(color);
+    move.subTurn = this.subTurn; //for undo
     const gotoNext = (mv) => {
-      const L = this.firstMove.length;
-      this.amoves.push(this.getAmove(this.firstMove[L-1], mv));
+      this.amove = this.getAmove(this.firstMove, mv);
       this.turn = oppCol;
       this.subTurn = 1;
       this.movesCount++;
@@ -673,7 +658,7 @@ export default class DynamoRules extends ChessRules {
       gotoNext(move);
     else {
       this.subTurn = 2;
-      this.firstMove.push(move);
+      this.firstMove = move;
       const kp = this.searchKingPos(color);
       if (
         // Condition is true on empty arrays:
@@ -697,14 +682,10 @@ export default class DynamoRules extends ChessRules {
   undo(move) {
     this.undoOnBoard(this.board, move);
     if (this.subTurn == 1) {
-      this.amoves.pop();
       this.turn = C.GetOppTurn(this.turn);
       this.movesCount--;
     }
-    if (move.subTurn == 1)
-      this.firstMove.pop();
     this.subTurn = move.subTurn;
-    this.toOldKingPos(move);
   }
 
 };
-- 
2.48.1