From e023d74715d327ebd7623079cb778b0def0a1464 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 17 Jan 2021 01:10:26 +0100
Subject: [PATCH] Add Fusion and Selfabsorption

---
 client/public/images/pieces/Fusion/bc.svg     |   1 +
 client/public/images/pieces/Fusion/bd.svg     |   1 +
 client/public/images/pieces/Fusion/bf.svg     |   1 +
 client/public/images/pieces/Fusion/bg.svg     |   1 +
 client/public/images/pieces/Fusion/bm.svg     |   1 +
 client/public/images/pieces/Fusion/wc.svg     |   1 +
 client/public/images/pieces/Fusion/wd.svg     |   1 +
 client/public/images/pieces/Fusion/wf.svg     |   1 +
 client/public/images/pieces/Fusion/wg.svg     |   1 +
 client/public/images/pieces/Fusion/wm.svg     |   1 +
 client/src/translations/en.js                 |   2 +
 client/src/translations/es.js                 |   2 +
 client/src/translations/fr.js                 |   2 +
 client/src/translations/rules/Fusion/en.pug   |  84 ++++
 client/src/translations/rules/Fusion/es.pug   |  98 ++++
 client/src/translations/rules/Fusion/fr.pug   |  90 ++++
 .../translations/rules/Selfabsorption/en.pug  |  11 +
 .../translations/rules/Selfabsorption/es.pug  |  11 +
 .../translations/rules/Selfabsorption/fr.pug  |  11 +
 client/src/translations/variants/en.pug       |   2 +
 client/src/translations/variants/es.pug       |   2 +
 client/src/translations/variants/fr.pug       |   2 +
 client/src/variants/Absorption.js             |   9 +-
 client/src/variants/Fusion.js                 | 437 ++++++++++++++++++
 client/src/variants/Selfabsorption.js         |  67 +++
 server/db/populate.sql                        |   2 +
 26 files changed, 839 insertions(+), 3 deletions(-)
 create mode 120000 client/public/images/pieces/Fusion/bc.svg
 create mode 120000 client/public/images/pieces/Fusion/bd.svg
 create mode 120000 client/public/images/pieces/Fusion/bf.svg
 create mode 120000 client/public/images/pieces/Fusion/bg.svg
 create mode 120000 client/public/images/pieces/Fusion/bm.svg
 create mode 120000 client/public/images/pieces/Fusion/wc.svg
 create mode 120000 client/public/images/pieces/Fusion/wd.svg
 create mode 120000 client/public/images/pieces/Fusion/wf.svg
 create mode 120000 client/public/images/pieces/Fusion/wg.svg
 create mode 120000 client/public/images/pieces/Fusion/wm.svg
 create mode 100644 client/src/translations/rules/Fusion/en.pug
 create mode 100644 client/src/translations/rules/Fusion/es.pug
 create mode 100644 client/src/translations/rules/Fusion/fr.pug
 create mode 100644 client/src/translations/rules/Selfabsorption/en.pug
 create mode 100644 client/src/translations/rules/Selfabsorption/es.pug
 create mode 100644 client/src/translations/rules/Selfabsorption/fr.pug
 create mode 100644 client/src/variants/Fusion.js
 create mode 100644 client/src/variants/Selfabsorption.js

diff --git a/client/public/images/pieces/Fusion/bc.svg b/client/public/images/pieces/Fusion/bc.svg
new file mode 120000
index 00000000..1200186b
--- /dev/null
+++ b/client/public/images/pieces/Fusion/bc.svg
@@ -0,0 +1 @@
+../Alice/bo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/bd.svg b/client/public/images/pieces/Fusion/bd.svg
new file mode 120000
index 00000000..78e54f8d
--- /dev/null
+++ b/client/public/images/pieces/Fusion/bd.svg
@@ -0,0 +1 @@
+../Perfect/bs.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/bf.svg b/client/public/images/pieces/Fusion/bf.svg
new file mode 120000
index 00000000..b30a26ad
--- /dev/null
+++ b/client/public/images/pieces/Fusion/bf.svg
@@ -0,0 +1 @@
+../Alice/bc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/bg.svg b/client/public/images/pieces/Fusion/bg.svg
new file mode 120000
index 00000000..09e6ea3e
--- /dev/null
+++ b/client/public/images/pieces/Fusion/bg.svg
@@ -0,0 +1 @@
+../Alice/bu.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/bm.svg b/client/public/images/pieces/Fusion/bm.svg
new file mode 120000
index 00000000..d3aaacf7
--- /dev/null
+++ b/client/public/images/pieces/Fusion/bm.svg
@@ -0,0 +1 @@
+../Perfect/be.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/wc.svg b/client/public/images/pieces/Fusion/wc.svg
new file mode 120000
index 00000000..4a85712d
--- /dev/null
+++ b/client/public/images/pieces/Fusion/wc.svg
@@ -0,0 +1 @@
+../Alice/wo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/wd.svg b/client/public/images/pieces/Fusion/wd.svg
new file mode 120000
index 00000000..b0eb8b97
--- /dev/null
+++ b/client/public/images/pieces/Fusion/wd.svg
@@ -0,0 +1 @@
+../Perfect/ws.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/wf.svg b/client/public/images/pieces/Fusion/wf.svg
new file mode 120000
index 00000000..d23af91d
--- /dev/null
+++ b/client/public/images/pieces/Fusion/wf.svg
@@ -0,0 +1 @@
+../Alice/wc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/wg.svg b/client/public/images/pieces/Fusion/wg.svg
new file mode 120000
index 00000000..c1403b33
--- /dev/null
+++ b/client/public/images/pieces/Fusion/wg.svg
@@ -0,0 +1 @@
+../Alice/wu.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Fusion/wm.svg b/client/public/images/pieces/Fusion/wm.svg
new file mode 120000
index 00000000..f47feb0e
--- /dev/null
+++ b/client/public/images/pieces/Fusion/wm.svg
@@ -0,0 +1 @@
+../Perfect/we.svg
\ No newline at end of file
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 9501efe2..90a4fa15 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -215,6 +215,8 @@ export const translations = {
   "Four new pieces": "Four new pieces",
   "Free initial setup": "Free initial setup",
   "Friendly pieces": "Friendly pieces",
+  "Fusion pieces (v1)": "Fusion pieces (v1)",
+  "Fusion pieces (v2)": "Fusion pieces (v2)",
   "In the shadow": "In the shadow",
   "Interweaved colorbound teams": "Interweaved colorbound teams",
   "General's Chess": "General's Chess",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 152ee64c..650aea92 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -215,6 +215,8 @@ export const translations = {
   "Four new pieces": "Quatro nuevas piezas",
   "Free initial setup": "Posición inicial libre",
   "Friendly pieces": "Piezas amistosas",
+  "Fusion pieces (v1)": "Fusionar piezas (v1)",
+  "Fusion pieces (v2)": "Fusionar piezas (v2)",
   "In the shadow": "En la sombra",
   "Interweaved colorbound teams": "Equipos unicolores entrelazados",
   "General's Chess": "Ajedrez de los Generales",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 2c6809a4..5a04fab5 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -215,6 +215,8 @@ export const translations = {
   "Four new pieces": "Quatre nouvelles pièces",
   "Free initial setup": "Position initiale libre",
   "Friendly pieces": "Pièces amies",
+  "Fusion pieces (v1)": "Fusionner les pièces (v1)",
+  "Fusion pieces (v2)": "Fusionner les pièces (v2)",
   "In the shadow": "Dans l'ombre",
   "Interweaved colorbound teams": "Équipes unicolores entremêlées",
   "General's Chess": "Échecs des Généraux",
diff --git a/client/src/translations/rules/Fusion/en.pug b/client/src/translations/rules/Fusion/en.pug
new file mode 100644
index 00000000..f2a25724
--- /dev/null
+++ b/client/src/translations/rules/Fusion/en.pug
@@ -0,0 +1,84 @@
+p.boxed
+  | Pieces can fusion and also be split under certain circumstances.
+
+p Fusion Chess is played like FIDE Chess with the following exceptions:
+
+h3 Fusion
+
+p.
+  A non-royal simple piece (Knight, Bishop, or Rook) may combine with
+  another simple piece on the same side (including a King) by moving
+  onto its square.
+
+p The combined piece moves as either of the two pieces just combined:
+ul
+  li King + Bishop = Pontiff
+  li King + Rook = Dragon King
+  li King + Knight = Cavalier King
+  li Bishop + Rook = Queen
+  li Bishop + Knight = Paladin
+  li Rook + Knight = Marshall
+
+p A piece may not combine with
+ul
+  li another piece of the same type,
+  li a piece belonging to the opponent,
+  li a compound piece.
+
+p.
+  A King may not move to combine any piece, though other another
+  piece may move to combine with its King.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/4P3/5N2/PPPPBPPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbm1/pp1ppppp/2n5/2p5/4P3/5D2/PPPP1PPP/RNBQK2R:
+  figcaption Before and after moves BxNf3 and RxNg8.
+
+h3 Fission
+
+p.
+  An unattacked compound piece may split by moving one
+  of its components, under its own powers of movement, to an empty square.
+ul
+  li A Rook which separates from a piece must move away as a Rook moves.
+  li A Bishop which separates from a piece must move away as a Bishop moves.
+  li A Knight which separates from a piece must move away as a Knight moves.
+  li A King which separates from a piece must move away as a King moves.
+p The compound piece is replaced by the component which doesn't move away.
+
+h3 Other details
+
+p Any piece that has participated in fission or fusion may no longer castle.
+
+p Pawns may promote to Rook, Bishop, or Knight only.
+
+p.
+  The object is to checkmate your opponent's current royal piece, which may
+  be a King, Pontiff, Dragon King, or Cavalier King.
+
+p.
+  No royal piece may move through check when moving more than one space.
+  For the Cavalier King, the Knight move is understood as beginning
+  orthogonally and turning diagonally or vice versa. If both spaces
+  it might pass over to reach a square are checked, it cannot move there.
+  However, this does not limit its ability to check or attack a piece.
+
+figure.diagram-container
+  .diagram
+    | fen:r3br2/c2p4/1pd2pqp/p1B5/1D1P1QP1/8/PPP1P2P/2RG2N1 c8:
+
+p.
+  On the diagram above, the Cavalier King only non-splitting move as a Knight
+  is to c8. Indeed, a6 and b6 are under attack. b8 as well, but not b7.
+
+h3 More information
+
+p
+  | See the 
+  a(href="https://www.chessvariants.com/other.dir/fusion.html")
+    | chessvariants page
+  | .
+
+p Inventor: Fergus Duniho (1999)
diff --git a/client/src/translations/rules/Fusion/es.pug b/client/src/translations/rules/Fusion/es.pug
new file mode 100644
index 00000000..de85107b
--- /dev/null
+++ b/client/src/translations/rules/Fusion/es.pug
@@ -0,0 +1,98 @@
+p.boxed
+  | Las piezas pueden fusionarse y también dividirse en
+  | ciertas circunstancias.
+
+p.
+  Ajedrez Fusión se juega como el juego habitual,
+  con las siguientes excepciones:
+
+h3 Fusión
+
+p.
+  Una sola pieza no real (Alfil, Caballo o Torre) se puede combinar con
+  otra pieza del mismo lado (incluido el Rey) moviéndose a su casilla.
+
+p La pieza combinada evoluciona como una u otra de las piezas unidas:
+ul
+  li Rey + Alfil = Pontífice
+  li Rey + Torre = Rey Dragón
+  li Rey + Caballo = Rey Caballero
+  li Alfil + Torre = Dama
+  li Alfil + Caballo = Paladín
+  li Torre + Caballo = Mariscal
+
+p Una pieza no se puede combinar con
+ul
+  li otra pieza del mismo tipo,
+  li una pieza que pertenece al oponente,
+  li una pieza compuesta.
+
+p.
+  Un Rey no puede moverse para combinar con una pieza, pero una
+  pieza puede realizar el desplazamiento que lleva a la fusión.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/4P3/5N2/PPPPBPPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbm1/pp1ppppp/2n5/2p5/4P3/5D2/PPPP1PPP/RNBQK2R:
+  figcaption Antes y después de los movimientos BxNf3 y RxNg8.
+
+h3 Fisión
+
+p.
+  Una pieza compuesta no atacada puede partirse moviendo uno de sus
+  componentes, según su tipo de movimiento, a un cuadrado vacío.
+ul
+  li.
+    Una Torre que se separa de una pieza debe alejarse mediante
+    un movimiento de Torre.
+  li.
+    Un Alfil que se separa de una pieza debe alejarse después de un
+    movimiento de Alfil.
+  li.
+    Un Caballo que se separa de una pieza debe
+    alejarse después de un movimiento de Caballo.
+  li.
+    Un Rey que se separa de una pieza debe alejarse mediante
+    un movimiento de Rey.
+p La pieza compuesta se reemplaza por el componente no inicial.
+
+h3 Otros detalles
+
+p.
+  Cualquier pieza que haya participado en una fisión o fusión
+  ya no puede enrocarse.
+
+p Los peones solo se promueven a Torre, Alfil o Caballo.
+
+p.
+  El objetivo es matar a la actual pieza real opuesta, esta
+  puede ser un Rey, Pontífice, Rey Dragón o Rey Caballo.
+
+p.
+  Una pieza real que se mueva más allá de un cuadrado no debe pasar
+  plazas atacadas. Respecto al Rey Caballero, un movimiento de Caballo
+  se entiende como un primer paso ortogonal antes de un giro diagonal,
+  o viceversa. Si los dos espacios que se pueden cruzar para alcanzar
+  una casilla está bajo control, no puede ir a esa casilla.
+  Sin embargo, eso no le impide dar jaque o atacar una pieza.
+
+figure.diagram-container
+  .diagram
+    | fen:r3br2/c2p4/1pd2pqp/p1B5/1D1P1QP1/8/PPP1P2P/2RG2N1 c8:
+
+p.
+  En el diagrama anterior, el único movimiento que no se separa del Rey
+  Caballero es hacia c8. De hecho, a6 y b6 son atacados.
+  b8 también, pero no b7.
+
+h3 Más información
+
+p
+  | Ver la 
+  a(href="https://www.chessvariants.com/other.dir/fusion.html")
+    | página chessvariants
+  | .
+
+p Inventor: Fergus Duniho (1999)
diff --git a/client/src/translations/rules/Fusion/fr.pug b/client/src/translations/rules/Fusion/fr.pug
new file mode 100644
index 00000000..3339f77f
--- /dev/null
+++ b/client/src/translations/rules/Fusion/fr.pug
@@ -0,0 +1,90 @@
+p.boxed
+  | Les pièces peuvent fusionner et également être divisées dans
+  | certaines circonstances.
+
+p.
+  Les Échecs Fusion se jouent comme le jeu habituel,
+  aux exceptions suivantes près :
+
+h3 Fusion
+
+p.
+  Une pièce non royale simple (Fou, Cavalier, ou Tour) peut se combiner avec
+  une autre pièce simple du même camp (incluant le Roi) en se déplaçant vers
+  sa case.
+
+p La pièce combinée évolue comme l'une ou l'autre des pièces réunies :
+ul
+  li Roi + Fou = Pontife
+  li Roi + Tour = Roi Dragon
+  li Roi + Cavalier = Roi Cavalier
+  li Fou + Tour = Dame
+  li Fou + Cavalier = Paladin
+  li Tour + Cavalier = Maréchal
+
+p Une pièce ne peut pas être combinée avec
+ul
+  li une autre pièce du même type,
+  li une pièce appartenant à l'adversaire,
+  li une pièce composée.
+
+p.
+  Un Roi ne peut pas se déplacer pour se combiner avec une pièce, mais une
+  pièce peut effectuer le déplacement menant à la fusion.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/4P3/5N2/PPPPBPPP/RNBQK2R:
+  .diagram.diag22
+    | fen:r1bqkbm1/pp1ppppp/2n5/2p5/4P3/5D2/PPPP1PPP/RNBQK2R:
+  figcaption Avant et après les coup BxNf3 et RxNg8.
+
+h3 Fission
+
+p.
+  Une pièce composée non attaquée peut se diviser en déplaçant une de ses
+  composantes, selon son type de mouvement, vers une case vide.
+ul
+  li Une Tour se séparant d'une pièce doit s'éloigner via un coup de Tour.
+  li Un Fou se séparant d'une pièce doit s'éloigner suivant un coup de Fou.
+  li.
+    Un Cavalier se séparant d'une pièce doit
+    s'éloigner suivant un coup de Cavalier.
+  li Un Roi se séparant d'une pièce doit s'éloigner via un coup de Roi.
+p La pièce composée est remplacée par la composante ne partant pas.
+
+h3 Autres détails
+
+p Toute pièce ayant participé à une fission ou fusion ne peut plus roquer.
+
+p Les pions sont promus en Tour, Fou ou Cavalier seulement.
+
+p.
+  L'objectif est de mater la pièce adverse actuellement royale, celle-ci
+  pouvant être un Roi, un Pontife, Un Roi Dragon ou Roi Cavalier.
+
+p.
+  Une pièce royale se déplaçant au-delà d'une case ne doit pas passer par
+  des cases attaquées. Concernant le Roi Cavalier, un coup de Cavalier
+  s'entend comme un premier pas orthogonal avant un virage diagonal,
+  ou vice-versa. Si les deux espaces pouvant être traversés afin d'atteindre
+  une case sont en échec, il ne peut pas se rendre sur cette case.
+  Cependant, cela ne l'empêche pas de donner échec ou d'attaquer une pièce.
+
+figure.diagram-container
+  .diagram
+    | fen:r3br2/c2p4/1pd2pqp/p1B5/1D1P1QP1/8/PPP1P2P/2RG2N1 c8:
+
+p.
+  Sur le diagramme ci-dessus, le seul coup non séparant du Roi Cavalier est
+  vers c8. En effet, a6 et b6 sont attaquées. b8 aussi, mais pas b7.
+
+h3 Plus d'information
+
+p
+  | Voir la 
+  a(href="https://www.chessvariants.com/other.dir/fusion.html")
+    | page chessvariants
+  | .
+
+p Inventeur : Fergus Duniho (1999)
diff --git a/client/src/translations/rules/Selfabsorption/en.pug b/client/src/translations/rules/Selfabsorption/en.pug
new file mode 100644
index 00000000..943773c1
--- /dev/null
+++ b/client/src/translations/rules/Selfabsorption/en.pug
@@ -0,0 +1,11 @@
+p.boxed
+  | Capture your own pieces to fusion their power.
+
+p
+  | Capturing a rook, knight, bishop or queen of your color with a piece
+  | from this list but of a different nature is allowed.
+  | The result adds up the pieces' abilities, as in 
+  a(href="/#/variants/Absorption") Absorption
+  | &nbsp;variant.
+
+p This was suggested by Vincent Rothuis in 2020.
diff --git a/client/src/translations/rules/Selfabsorption/es.pug b/client/src/translations/rules/Selfabsorption/es.pug
new file mode 100644
index 00000000..e2f91754
--- /dev/null
+++ b/client/src/translations/rules/Selfabsorption/es.pug
@@ -0,0 +1,11 @@
+p.boxed
+ | Captura tus propias piezas para fusionar sus poderes.
+
+p
+  | Puede capturar una torre, un caballo, un alfil o una dama de su
+  | color con una pieza de este listado pero de una naturaleza diferente.
+  | El resultado suma los movimientos de las piezas, como en la variante 
+  a(href="/#/variants/Absorption") Absorption
+  | .
+
+p Esto fue sugerido por Vincent Rothuis en 2020.
diff --git a/client/src/translations/rules/Selfabsorption/fr.pug b/client/src/translations/rules/Selfabsorption/fr.pug
new file mode 100644
index 00000000..a03d3da2
--- /dev/null
+++ b/client/src/translations/rules/Selfabsorption/fr.pug
@@ -0,0 +1,11 @@
+p.boxed
+  | Capturez vos propres pièces pour fusionner leurs pouvoirs.
+
+p
+  | Vous pouvez capturer une tour, un cavalier, un fou ou une dame de votre
+  | couleur avec une pièce de cette liste mais de nature différente.
+  | Le résultat somme les déplacements des pièces, comme dans la variante 
+  a(href="/#/variants/Absorption") Absorption
+  | .
+
+p Ceci a été suggéré par Vincent Rothuis en 2020.
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index aea01ff7..542c2d48 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -446,6 +446,7 @@ p.
     "Bicolour",
     "Evolution",
     "Forward",
+    "Fusion",
     "Gridolina",
     "Hamilton",
     "Kingsmaker",
@@ -453,6 +454,7 @@ p.
     "Refusal",
     "Relayup",
     "Rollerball",
+    "Selfabsorption",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index e087a42e..09fd1169 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -456,6 +456,7 @@ p.
     "Bicolour",
     "Evolution",
     "Forward",
+    "Fusion",
     "Gridolina",
     "Hamilton",
     "Kingsmaker",
@@ -463,6 +464,7 @@ p.
     "Refusal",
     "Relayup",
     "Rollerball",
+    "Selfabsorption",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index bb640db0..144ad2df 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -454,6 +454,7 @@ p.
     "Bicolour",
     "Evolution",
     "Forward",
+    "Fusion",
     "Gridolina",
     "Hamilton",
     "Kingsmaker",
@@ -461,6 +462,7 @@ p.
     "Refusal",
     "Relayup",
     "Rollerball",
+    "Selfabsorption",
     "Takenmake",
     "Wormhole"
   ]
diff --git a/client/src/variants/Absorption.js b/client/src/variants/Absorption.js
index c6e1de74..33c83517 100644
--- a/client/src/variants/Absorption.js
+++ b/client/src/variants/Absorption.js
@@ -91,10 +91,13 @@ export class AbsorptionRules extends ChessRules {
       );
     });
     moves.forEach(m => {
-      if (m.vanish.length == 2) {
+      if (
+        m.vanish.length == 2 &&
+        m.appear.length == 1 &&
+        piece != m.vanish[1].p
+      ) {
         // Augment pieces abilities in case of captures
-        const piece2 = m.vanish[1].p;
-        if (piece != piece2) m.appear[0].p = V.Fusion(piece, piece2);
+        m.appear[0].p = V.Fusion(piece, m.vanish[1].p);
       }
     });
     return moves;
diff --git a/client/src/variants/Fusion.js b/client/src/variants/Fusion.js
new file mode 100644
index 00000000..2ead9ad7
--- /dev/null
+++ b/client/src/variants/Fusion.js
@@ -0,0 +1,437 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+
+export class FusionRules extends ChessRules {
+
+  static get PawnSpecs() {
+    return (
+      Object.assign(
+        { promotions: [V.ROOK, V.KNIGHT, V.BISHOP] },
+        ChessRules.PawnSpecs
+      )
+    );
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    let kings = { "k": 0, "K": 0 };
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        if (['K', 'F', 'G', 'C'].includes(row[i])) kings['K']++;
+        else if (['k', 'f', 'g', 'c'].includes(row[i])) kings['k']++;
+        if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num) || num <= 0) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    if (Object.values(kings).some(v => v != 1)) return false;
+    return true;
+  }
+
+  scanKings(fen) {
+    this.kingPos = { w: [-1, -1], b: [-1, -1] };
+    const fenRows = V.ParseFen(fen).position.split("/");
+    for (let i = 0; i < fenRows.length; i++) {
+      let k = 0;
+      for (let j = 0; j < fenRows[i].length; j++) {
+        const ch_ij = fenRows[i].charAt(j);
+        if (['k', 'f', 'g', 'c'].includes(ch_ij))
+          this.kingPos["b"] = [i, k];
+        else if (['K', 'F', 'G', 'C'].includes(ch_ij))
+          this.kingPos["w"] = [i, k];
+        else {
+          const num = parseInt(fenRows[i].charAt(j), 10);
+          if (!isNaN(num)) k += num - 1;
+        }
+        k++;
+      }
+    }
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    if (this.getColor(x1, y1) !== this.getColor(x2, y2)) return true;
+    const p1 = this.getPiece(x1, y1);
+    const p2 = this.getPiece(x2, y2);
+    return (
+      p1 != p2 &&
+      [V.ROOK, V.KNIGHT, V.BISHOP].includes(p1) &&
+      [V.KING, V.ROOK, V.KNIGHT, V.BISHOP].includes(p2)
+    );
+  }
+
+  getPpath(b) {
+    if ([V.BN, V.RN, V.KB, V.KR, V.KN].includes(b[1])) return "Fusion/" + b;
+    return b;
+  }
+
+  // Three new pieces: rook+knight, bishop+knight and queen+knight
+  static get RN() {
+    // Marshall
+    return 'm';
+  }
+  static get BN() {
+    // Paladin
+    return 'd';
+  }
+  static get KB() {
+    // Pontiff
+    return 'f';
+  }
+  static get KR() {
+    // Dragon King
+    return 'g';
+  }
+  static get KN() {
+    // Cavalier King
+    return 'c';
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.RN, V.BN, V.KB, V.KR, V.KN]);
+  }
+
+  static Fusion(p1, p2) {
+    if (p2 == V.KING) {
+      switch (p1) {
+        case V.ROOK: return V.KR;
+        case V.BISHOP: return V.KB;
+        case V.KNIGHT: return V.KN;
+      }
+    }
+    if ([p1, p2].includes(V.KNIGHT)) {
+      if ([p1, p2].includes(V.ROOK)) return V.RN;
+      return V.BN;
+    }
+    // Only remaining combination is rook + bishop = queen
+    return V.QUEEN;
+  }
+
+  getPotentialMovesFrom(sq) {
+    let moves = [];
+    const piece = this.getPiece(sq[0], sq[1]);
+    switch (piece) {
+      case V.RN:
+        moves =
+          super.getPotentialRookMoves(sq).concat(
+          super.getPotentialKnightMoves(sq)).concat(
+          this.getFissionMoves(sq));
+        break;
+      case V.BN:
+        moves =
+          super.getPotentialBishopMoves(sq).concat(
+          super.getPotentialKnightMoves(sq)).concat(
+          this.getFissionMoves(sq));
+        break;
+      case V.KN:
+        moves =
+          super.getPotentialKingMoves(sq).concat(
+          this.getPotentialKingAsKnightMoves(sq)).concat(
+          this.getFissionMoves(sq));
+        break;
+      case V.KB:
+        moves =
+          super.getPotentialKingMoves(sq).concat(
+          this.getPotentialKingAsBishopMoves(sq)).concat(
+          this.getFissionMoves(sq));
+        break;
+      case V.KR:
+        moves =
+          super.getPotentialKingMoves(sq).concat(
+          this.getPotentialKingAsRookMoves(sq)).concat(
+          this.getFissionMoves(sq));
+        break;
+      case V.QUEEN:
+        moves =
+          super.getPotentialQueenMoves(sq).concat(this.getFissionMoves(sq));
+        break;
+      default:
+        moves = super.getPotentialMovesFrom(sq);
+        break;
+    }
+    moves.forEach(m => {
+      if (
+        m.vanish.length == 2 &&
+        m.appear.length == 1 &&
+        m.vanish[0].c == m.vanish[1].c
+      ) {
+        // Augment pieces abilities in case of self-captures
+        m.appear[0].p = V.Fusion(piece, m.vanish[1].p);
+      }
+    });
+    return moves;
+  }
+
+  getSlideNJumpMoves_fission([x, y], moving, staying, steps, oneStep) {
+    let moves = [];
+    const c = this.getColor(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(
+          new Move({
+            appear: [
+              new PiPo({ x: i, y: j, c: c, p: moving }),
+              new PiPo({ x: x, y: y, c: c, p: staying }),
+            ],
+            vanish: [
+              new PiPo({ x: x, y: y, c: c, p: this.getPiece(x, y) })
+            ]
+          })
+        );
+        if (!!oneStep) continue outerLoop;
+        i += step[0];
+        j += step[1];
+      }
+    }
+    return moves;
+  }
+
+  getFissionMoves(sq) {
+    // Square attacked by opponent?
+    const color = this.getColor(sq[0], sq[1]);
+    const oppCol = V.GetOppCol(color);
+    if (this.isAttacked(sq, oppCol)) return [];
+    // Ok, fission a priori valid
+    const kSteps = V.steps[V.BISHOP].concat(V.steps[V.BISHOP]);
+    switch (this.getPiece(sq[0], sq[1])) {
+      case V.BN:
+        return (
+          this.getSlideNJumpMoves_fission(
+            sq, V.BISHOP, V.KNIGHT, V.steps[V.BISHOP])
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.KNIGHT, V.BISHOP, V.steps[V.KNIGHT], "oneStep"))
+        );
+      case V.RN:
+        return (
+          this.getSlideNJumpMoves_fission(
+            sq, V.ROOK, V.KNIGHT, V.steps[V.ROOK])
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.KNIGHT, V.ROOK, V.steps[V.KNIGHT], "oneStep"))
+        );
+      case V.KN:
+        return (
+          this.getSlideNJumpMoves_fission(sq, V.KING, V.KNIGHT, kSteps)
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.KNIGHT, V.KING, V.steps[V.KNIGHT], "oneStep"))
+        );
+      case V.KB:
+        return (
+          this.getSlideNJumpMoves_fission(sq, V.KING, V.BISHOP, kSteps)
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.BISHOP, V.KING, V.steps[V.BISHOP]))
+        );
+      case V.KR:
+        return (
+          this.getSlideNJumpMoves_fission(sq, V.KING, V.ROOK, kSteps)
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.ROOK, V.KING, V.steps[V.ROOK]))
+        );
+      case V.QUEEN:
+        return (
+          this.getSlideNJumpMoves_fission(
+            sq, V.BISHOP, V.ROOK, V.steps[V.BISHOP])
+          .concat(this.getSlideNJumpMoves_fission(
+            sq, V.ROOK, V.BISHOP, V.steps[V.ROOK]))
+        );
+    }
+  }
+
+  intermediateSquaresFromKnightStep(step) {
+    if (step[0] == 2) return [ [1, 0], [1, step[1]] ];
+    if (step[0] == -2) return [ [-1, 0], [-1, step[1]] ];
+    if (step[1] == 2) return [ [0, 1], [step[1], 1] ];
+    // step[1] == -2:
+    return [ [0, -1], [step[1], -1] ];
+  }
+
+  getPotentialKingAsKnightMoves([x, y]) {
+    const oppCol = V.GetOppCol(this.turn);
+    let moves = [];
+    let intermediateOk = {};
+    for (let s of V.steps[V.KNIGHT]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (!V.OnBoard(i, j) || this.board[i][j] != V.EMPTY) continue;
+      const iSq = this.intermediateSquaresFromKnightStep(s);
+      let moveOk = false;
+      for (let sq of iSq) {
+        const key = sq[0] + "_" + sq[1];
+        if (Object.keys(intermediateOk).includes(key)) {
+          if (intermediateOk[key]) moveOk = true;
+        }
+        else {
+          moveOk = !this.isAttacked([x + sq[0], y + sq[1]], oppCol);
+          intermediateOk[key] = moveOk;
+        }
+        if (moveOk) break;
+      }
+      if (moveOk) moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  getPotentialKingMovesAsSlider([x, y], slider) {
+    const oppCol = V.GetOppCol(this.turn);
+    let moves = [];
+    for (let s of V.steps[slider]) {
+      let [i, j] = [x + s[0], y + s[1]];
+      if (
+        !V.OnBoard(i, j) ||
+        this.board[i][j] != V.EMPTY ||
+        this.isAttacked([i, j], oppCol)
+      ) {
+        continue;
+      }
+      i += s[0];
+      j += s[1];
+      while (
+        V.OnBoard(i, j) &&
+        this.board[i][j] == V.EMPTY &&
+        // TODO: this test will be done twice (also in filterValid())
+        !this.isAttacked([i, j], oppCol)
+      ) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        i += s[0];
+        j += s[1];
+      }
+    }
+    return moves;
+  }
+
+  getPotentialKingAsBishopMoves(sq) {
+    return this.getPotentialKingMovesAsSlider(sq, V.BISHOP);
+  }
+
+  getPotentialKingAsRookMoves(sq) {
+    return this.getPotentialKingMovesAsSlider(sq, V.ROOK);
+  }
+
+  isAttacked(sq, color) {
+    return (
+      super.isAttacked(sq, color) ||
+      this.isAttackedByBN(sq, color) ||
+      this.isAttackedByRN(sq, color) ||
+      this.isAttackedByKN(sq, color) ||
+      this.isAttackedByKB(sq, color) ||
+      this.isAttackedByKR(sq, color)
+    );
+  }
+
+  isAttackedByBN(sq, color) {
+    return (
+      this.isAttackedBySlideNJump(sq, color, V.BN, V.steps[V.BISHOP]) ||
+      this.isAttackedBySlideNJump(
+        sq, color, V.BN, V.steps[V.KNIGHT], "oneStep")
+    );
+  }
+
+  isAttackedByRN(sq, color) {
+    return (
+      this.isAttackedBySlideNJump(sq, color, V.RN, V.steps[V.ROOK]) ||
+      this.isAttackedBySlideNJump(
+        sq, color, V.RN, V.steps[V.KNIGHT], "oneStep")
+    );
+  }
+
+  isAttackedByKN(sq, color) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return (
+      this.isAttackedBySlideNJump(sq, color, V.KN, steps, "oneStep") ||
+      this.isAttackedBySlideNJump(
+        sq, color, V.KN, V.steps[V.KNIGHT], "oneStep")
+    );
+  }
+
+  isAttackedByKB(sq, color) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return (
+      this.isAttackedBySlideNJump(sq, color, V.KB, steps, "oneStep") ||
+      this.isAttackedBySlideNJump(sq, color, V.KB, V.steps[V.BISHOP])
+    );
+  }
+
+  isAttackedByKR(sq, color) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return (
+      this.isAttackedBySlideNJump(sq, color, V.KR, steps, "oneStep") ||
+      this.isAttackedBySlideNJump(sq, color, V.KR, V.steps[V.ROOK])
+    );
+  }
+
+  updateCastleFlags(move, piece) {
+    const c = V.GetOppCol(this.turn);
+    const firstRank = (c == "w" ? V.size.x - 1 : 0);
+    const oppCol = this.turn;
+    const oppFirstRank = V.size.x - 1 - firstRank;
+    if (piece == V.KING)
+      this.castleFlags[c] = [V.size.y, V.size.y];
+    else 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;
+    }
+    // Check move endpoint: if my king or any rook position, flags off
+    if (move.end.x == this.kingPos[c][0] && move.end.y == this.kingPos[c][1])
+      this.castleFlags[c] = [V.size.y, V.size.y];
+    else if (
+      move.end.x == firstRank &&
+      this.castleFlags[c].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags[c][0] ? 0 : 1);
+      this.castleFlags[c][flagIdx] = V.size.y;
+    }
+    else if (
+      move.end.x == oppFirstRank &&
+      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;
+    }
+  }
+
+  postPlay(move) {
+    const c = V.GetOppCol(this.turn);
+    const piece = move.appear[0].p;
+    if ([V.KING, V.KN, V.KB, V.KR].includes(piece))
+      this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
+    this.updateCastleFlags(move, piece);
+  }
+
+  postUndo(move) {
+    const c = this.getColor(move.start.x, move.start.y);
+    if ([V.KING, V.KN, V.KB, V.KR].includes(move.appear[0].p))
+      this.kingPos[c] = [move.start.x, move.start.y];
+  }
+
+  static get VALUES() {
+    // Values such that sum of values = value of sum
+    return Object.assign(
+      { m: 8, d: 6, f: 1003, g: 1005, c: 1003 },
+      ChessRules.VALUES
+    );
+  }
+
+  getNotation(move) {
+    if (move.appear.length == 2 && move.vanish.length == 1) {
+      // Fission (because no capture in this case)
+      return (
+        move.appear[0].p.toUpperCase() + V.CoordsToSquare(move.end) +
+        "/f:" + move.appear[1].p.toUpperCase() + V.CoordsToSquare(move.start)
+      );
+    }
+    let notation = super.getNotation(move);
+    if (move.vanish[0].p != V.PAWN && move.appear[0].p != move.vanish[0].p)
+      // Fusion (not from a pawn: handled in ChessRules)
+      notation += "=" + move.appear[0].p.toUpperCase();
+    return notation;
+  }
+
+};
diff --git a/client/src/variants/Selfabsorption.js b/client/src/variants/Selfabsorption.js
new file mode 100644
index 00000000..edfa09f5
--- /dev/null
+++ b/client/src/variants/Selfabsorption.js
@@ -0,0 +1,67 @@
+import { ChessRules } from "@/base_rules";
+import { AbsorptionRules } from "@/variants/Absorption";
+
+export class SelfabsorptionRules extends AbsorptionRules {
+
+  canTake([x1, y1], [x2, y2]) {
+    if (this.getColor(x1, y1) !== this.getColor(x2, y2)) return true;
+    const p1 = this.getPiece(x1, y1);
+    const p2 = this.getPiece(x2, y2);
+    return (
+      p1 != p2 &&
+      [V.QUEEN, V.ROOK, V.KNIGHT, V.BISHOP].includes(p1) &&
+      [V.QUEEN, V.ROOK, V.KNIGHT, V.BISHOP].includes(p2)
+    );
+  }
+
+  getPotentialMovesFrom(sq) {
+    let moves = [];
+    const piece = this.getPiece(sq[0], sq[1]);
+    switch (piece) {
+      case V.RN:
+        moves =
+          super.getPotentialRookMoves(sq).concat(
+          super.getPotentialKnightMoves(sq));
+        break;
+      case V.BN:
+        moves =
+          super.getPotentialBishopMoves(sq).concat(
+          super.getPotentialKnightMoves(sq));
+        break;
+      case V.QN:
+        moves =
+          super.getPotentialQueenMoves(sq).concat(
+          super.getPotentialKnightMoves(sq));
+        break;
+      case V.PAWN:
+        moves = super.getPotentialPawnMoves(sq);
+        break;
+      case V.ROOK:
+        moves = super.getPotentialRookMoves(sq);
+        break;
+      case V.KNIGHT:
+        moves = super.getPotentialKnightMoves(sq);
+        break;
+      case V.BISHOP:
+        moves = super.getPotentialBishopMoves(sq);
+        break;
+      case V.QUEEN:
+        moves = super.getPotentialQueenMoves(sq);
+        break;
+      case V.KING:
+        moves = super.getPotentialKingMoves(sq);
+        break;
+    }
+    moves.forEach(m => {
+      if (
+        m.vanish.length == 2 && m.appear.length == 1 &&
+        piece != m.vanish[1].p && m.vanish[0].c == m.vanish[1].c
+      ) {
+        // Augment pieces abilities in case of self-captures
+        m.appear[0].p = V.Fusion(piece, m.vanish[1].p);
+      }
+    });
+    return moves;
+  }
+
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 122224d1..5322ce99 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -66,6 +66,7 @@ insert or ignore into Variants (name, description) values
   ('Freecapture', 'Capture both colors'),
   ('Fugue', 'Baroque Music'),
   ('Fullcavalry', 'Lancers everywhere'),
+  ('Fusion', 'Fusion pieces (v1)'),
   ('Grand', 'Big board'),
   ('Grasshopper', 'Long jumps over pieces'),
   ('Gridolina', 'Jump the borders'),
@@ -128,6 +129,7 @@ insert or ignore into Variants (name, description) values
   ('Rugby', 'Transform an essay'),
   ('Schess', 'Seirawan-Harper Chess'),
   ('Screen', 'Free initial setup'),
+  ('Selfabsorption', 'Fusion pieces (v2)'),
   ('Shako', 'Non-conformism and utopia'),
   ('Shatranj', 'Ancient rules'),
   ('Shogi', 'Japanese Chess'),
-- 
2.44.0