Add (real) Football chess
authorBenjamin Auder <benjamin.auder@somewhere>
Thu, 14 Jan 2021 19:10:14 +0000 (20:10 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Thu, 14 Jan 2021 19:10:14 +0000 (20:10 +0100)
23 files changed:
TODO
client/public/images/pieces/Football/SOURCE [new file with mode: 0644]
client/public/images/pieces/Football/ball.svg [new file with mode: 0644]
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/translations/rules/Football/en.pug
client/src/translations/rules/Football/es.pug
client/src/translations/rules/Football/fr.pug
client/src/translations/rules/Squatter1/en.pug [moved from client/src/translations/rules/Squatter/en.pug with 100% similarity]
client/src/translations/rules/Squatter1/es.pug [moved from client/src/translations/rules/Squatter/es.pug with 100% similarity]
client/src/translations/rules/Squatter1/fr.pug [moved from client/src/translations/rules/Squatter/fr.pug with 100% similarity]
client/src/translations/rules/Squatter2/en.pug [new file with mode: 0644]
client/src/translations/rules/Squatter2/es.pug [new file with mode: 0644]
client/src/translations/rules/Squatter2/fr.pug [new file with mode: 0644]
client/src/translations/variants/en.pug
client/src/translations/variants/es.pug
client/src/translations/variants/fr.pug
client/src/variants/Ball.js
client/src/variants/Football.js
client/src/variants/Squatter1.js [moved from client/src/variants/Squatter.js with 98% similarity]
client/src/variants/Squatter2.js [new file with mode: 0644]
server/db/populate.sql

diff --git a/TODO b/TODO
index 4bf0b48..53cace8 100644 (file)
--- a/TODO
+++ b/TODO
@@ -3,9 +3,6 @@ Embedded rules language not updated when language is set (in Analyse, Game and P
 If new live game starts in background, "new game" notify OK but not first move.
 
 NEW VARIANTS:
-https://www.chessvariants.com/crossover.dir/football.html
-  ballon vers n'importe laquelle de nos pièces = 'j'ai terminé mes passes, à toi'
-  Also: rename current Football into Squatter2 (and Squatter into Squatter1)
 https://www.pychess.org/variant/shogun
 https://www.chessvariants.com/incinf.dir/bario.html
   https://www.chessvariants.com/index/listcomments.php?order=DESC&itemid=Bario
@@ -13,6 +10,6 @@ https://www.chessvariants.com/incinf.dir/bario.html
   https://le-cdn.website-editor.net/20ef5f800ea646c29f6852cfc5ceda07/dms3rep/multi/opt/BAR028-e15a849c-960w.jpg
 
 Non-chess: ( won't add draughts: https://lidraughts.org/ )
-Avalam, Fanorona https://fr.wikipedia.org/wiki/Fanorona
+Gomoku, Konane
+Fanorona https://fr.wikipedia.org/wiki/Fanorona
 Yoté https://fr.wikipedia.org/wiki/Yot%C3%A9 http://www.zillionsofgames.com/cgi-bin/zilligames/submissions.cgi/92187?do=show;id=960
-Qoridor: reserve moves to place the walls, convention from left / from top
diff --git a/client/public/images/pieces/Football/SOURCE b/client/public/images/pieces/Football/SOURCE
new file mode 100644 (file)
index 0000000..3c339cb
--- /dev/null
@@ -0,0 +1 @@
+https://commons.wikimedia.org/wiki/File:Soccer_ball.svg
diff --git a/client/public/images/pieces/Football/ball.svg b/client/public/images/pieces/Football/ball.svg
new file mode 100644 (file)
index 0000000..7b57918
--- /dev/null
@@ -0,0 +1,215 @@
+<?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:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="800"
+   height="800"
+   viewBox="-105 -105 210 210"
+   version="1.1"
+   id="svg58"
+   sodipodi:docname="ball.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)">
+  <metadata
+     id="metadata62">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1060"
+     id="namedview60"
+     showgrid="false"
+     inkscape:zoom="1.10375"
+     inkscape:cx="400"
+     inkscape:cy="400"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg58" />
+  <defs
+     id="defs42">
+    <clipPath
+       id="ball">
+      <circle
+         r="100"
+         stroke-width="0"
+         id="circle2" />
+    </clipPath>
+    <radialGradient
+       id="shadow1"
+       cx="-19.999999"
+       cy="-39.999998"
+       r="160"
+       fx="-19.999999"
+       fy="-39.999998"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="scale(0.9)">
+      <stop
+         offset="0"
+         stop-color="white"
+         stop-opacity="1"
+         id="stop5" />
+      <stop
+         offset=".4"
+         stop-color="white"
+         stop-opacity="1"
+         id="stop7" />
+      <stop
+         offset=".8"
+         stop-color="#EEEEEE"
+         stop-opacity="1"
+         id="stop9" />
+    </radialGradient>
+    <radialGradient
+       id="shadow2"
+       cx="0"
+       cy="0"
+       r="100"
+       fx="0"
+       fy="0"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="scale(0.9)">
+      <stop
+         offset="0"
+         stop-color="white"
+         stop-opacity="0"
+         id="stop12" />
+      <stop
+         offset=".8"
+         stop-color="white"
+         stop-opacity="0"
+         id="stop14" />
+      <stop
+         offset=".99"
+         stop-color="black"
+         stop-opacity=".3"
+         id="stop16" />
+      <stop
+         offset="1"
+         stop-color="black"
+         stop-opacity="1"
+         id="stop18" />
+    </radialGradient>
+    <g
+       id="black_stuff"
+       stroke-linejoin="round"
+       clip-path="url(#ball)">
+      <g
+         fill="black"
+         id="g35">
+        <path
+           d="M 6,-32 Q 26,-28 46,-19 Q 57,-35 64,-47 Q 50,-68 37,-76 Q 17,-75 1,-68 Q 4,-51 6,-32"
+           id="path21" />
+        <path
+           d="M -26,-2 Q -45,-8 -62,-11 Q -74,5 -76,22 Q -69,40 -50,54 Q -32,47 -17,39 Q -23,15 -26,-2"
+           id="path23" />
+        <path
+           d="M -95,22 Q -102,12 -102,-8 V 80 H -85 Q -95,45 -95,22"
+           id="path25" />
+        <path
+           d="M 55,24 Q 41,41 24,52 Q 28,65 31,79 Q 55,78 68,67 Q 78,50 80,35 Q 65,28 55,24"
+           id="path27" />
+        <path
+           d="M 0,120 L -3,95 Q -25,93 -42,82 Q -50,84 -60,81"
+           id="path29" />
+        <path
+           d="M -90,-48 Q -80,-52 -68,-49 Q -52,-71 -35,-77 Q -35,-100 -40,-100 H -100"
+           id="path31" />
+        <path
+           d="M 100,-55 L 87,-37 Q 98,-10 97,5 L 100,6"
+           id="path33" />
+      </g>
+      <g
+         fill="none"
+         id="g39">
+        <path
+           d="M 6,-32 Q -18,-12 -26,-2                      M 46,-19 Q 54,5 55,24                      M 64,-47 Q 77,-44 87,-37                      M 37,-76 Q 39,-90 36,-100                      M 1,-68 Q -13,-77 -35,-77                      M -62,-11 Q -67,-25 -68,-49                      M -76,22 Q -85,24 -95,22                      M -50,54 Q -49,70 -42,82                      M -17,39 Q 0,48 24,52                      M 31,79 Q 20,92 -3,95                      M 68,67 L 80,80                      M 80,35 Q 90,25 97,5             "
+           id="path37" />
+      </g>
+    </g>
+  </defs>
+  <circle
+     r="90"
+     fill="#ffffff"
+     stroke="none"
+     id="circle44"
+     cx="-1.4210855e-14"
+     cy="-1.4210855e-14"
+     style="stroke-width:0.9" />
+  <circle
+     r="90"
+     fill="url(#shadow1)"
+     stroke="none"
+     id="circle46"
+     style="fill:url(#shadow1);stroke-width:0.9"
+     cx="-1.4210855e-14"
+     cy="-1.4210855e-14" />
+  <use
+     xlink:href="#black_stuff"
+     stroke="#eeeeee"
+     stroke-width="7"
+     id="use48"
+     transform="scale(0.9)"
+     x="0"
+     y="0"
+     width="100%"
+     height="100%" />
+  <use
+     xlink:href="#black_stuff"
+     stroke="#dddddd"
+     stroke-width="4"
+     id="use50"
+     transform="scale(0.9)"
+     x="0"
+     y="0"
+     width="100%"
+     height="100%" />
+  <use
+     xlink:href="#black_stuff"
+     stroke="#999999"
+     stroke-width="2"
+     id="use52"
+     transform="scale(0.9)"
+     x="0"
+     y="0"
+     width="100%"
+     height="100%" />
+  <use
+     xlink:href="#black_stuff"
+     stroke="#000000"
+     stroke-width="1"
+     id="use54"
+     transform="scale(0.9)"
+     x="0"
+     y="0"
+     width="100%"
+     height="100%" />
+  <circle
+     r="90"
+     fill="url(#shadow2)"
+     stroke="none"
+     id="circle56"
+     style="fill:url(#shadow2);stroke-width:0.9"
+     cx="-1.4210855e-14"
+     cy="-1.4210855e-14" />
+</svg>
index 6d79e55..bf58f25 100644 (file)
@@ -293,7 +293,8 @@ export const translations = {
   "Shoot pieces": "Shoot pieces",
   "Spartan versus Persians": "Spartan versus Persians",
   "Squares disappear": "Squares disappear",
-  "Squat last rank": "Squat last rank",
+  "Squat last rank (v1)": "Squat last rank (v1)",
+  "Squat last rank (v2)": "Squat last rank (v2)",
   "Standard rules": "Standard rules",
   "Stun & kick pieces": "Stun & kick pieces",
   "Thai Chess (v1)": "Thai Chess (v1)",
index a50a979..f9a9d99 100644 (file)
@@ -293,7 +293,8 @@ export const translations = {
   "Shoot pieces": "Tirar de las piezas",
   "Spartan versus Persians": "Espartanos contra Persas",
   "Squares disappear": "Las casillas desaparecen",
-  "Squat last rank": "Ocupa la última fila",
+  "Squat last rank (v1)": "Ocupa la última fila (v1)",
+  "Squat last rank (v2)": "Ocupa la última fila (v2)",
   "Standard rules": "Reglas estandar",
   "Stun & kick pieces": "Aturdir & patear piezas",
   "Thai Chess (v1)": "Ajedrez tailandés (v1)",
index 925f141..101436d 100644 (file)
@@ -293,7 +293,8 @@ export const translations = {
   "Shoot pieces": "Tirez sur les pièces",
   "Spartan versus Persians": "Spartiates contre Perses",
   "Squares disappear": "Les cases disparaissent",
-  "Squat last rank": "Occupez la dernière rangée",
+  "Squat last rank (v1)": "Occupez la dernière rangée (v1)",
+  "Squat last rank (v2)": "Occupez la dernière rangée (v2)",
   "Standard rules": "Règles usuelles",
   "Stun & kick pieces": "Étourdissez & frappez les pièces",
   "Thai Chess": "Échecs thai",
index 34fb52b..4f20b61 100644 (file)
@@ -1,21 +1,54 @@
-p.boxed.
-  Win by playing a move on the middle of last rank.
-  The king has no royal status.
+p.boxed
+  | Pieces don't capture but can kick a ball.
+  | The object of the game is to send the ball into the enemy goal.
 
 p.
-  The goal for White (resp. Black) are the squares d8 and e8 (resp. d1 and
-  e1) in the middle of the opposing rank. The first player to settle a
-  piece there wins, even if it is attacked.
+  At each turn, a player can first make a normal (non capturing) move.
+  If none of his pieces are adjacent to the ball, such move is mandatory.
+  But, if any of his pieces stand next to the ball he may then or right
+  away kick it, if the step defined by the arrow piece --> ball is
+  compatible with the piece's movement.
+
+p.
+  To play a kick, click on the ball and then on the desired square.
+  The knight send the ball at any knight-step away from its initial position,
+  except on squares adjacent to the knight.
+  However, when the ball is in a corner an exception to this rule is allowed:
+  on the following diagram "kick from g1 to h3" is allowed".
 
 figure.diagram-container
-  .diagram
-    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
-  figcaption 1...Nc3 and 2...N(x)d1# wins for Black.
+  .diagram.diag12
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba:
+  .diagram.diag22
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1:
+  figcaption Before and after (kick) g1-h3
+
+h4 Some restrictions.
+
+p.
+  To avoid infinite cycles, each piece can kick the ball at most
+  once during a turn.
+
+p Pieces cannot stand in any goal square.
 
-h3 Source
+p The ball may never reach or cross the goal horizontally.
+
+h4 Complete a move on the interface.
+
+p.
+  If at the end of an initial or intermediate move one of your pieces is
+  adjacent to the ball, you'll need to "capture" any of your pieces
+  with the ball to indicate that the move is over.
+
+h3 More information
 
 p
-  | This variant is mentioned 
-  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
-    | on this page
-  | . It isn't specified if the winning piece needs to be safe from attacks.
+  | See the 
+  a(href="https://www.chessvariants.com/crossover.dir/football.html")
+    | chessvariants page
+  | , for example, and also the 
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | Classified Encyclopedia of Chess Variants
+  | .
+
+p Inventor: Joseph Boyer (1951)
index 2ca6e47..1f346b6 100644 (file)
@@ -1,21 +1,58 @@
-p.boxed.
-  Gana haciendo un movimiento en el centro de la última fila.
-  El rey no tiene estatus real.
+p.boxed
+  | Las piezas no capturan pero pueden golpear una bola.
+  | El objetivo del juego es enviar el balón a la portería contraria.
 
 p.
-  El objetivo de las blancas (resp. negras) son las casillas d8 y e8
-  (resp. d1 y e1) en el medio de la fila opuesta.
-  El primer jugador que instala una pieza ahí gana, incluso si es atacada.
+  En cada ronda, un jugador puede realizar primero un movimiento normal
+  (sin captura). Si ninguna de sus piezas está al lado de la bola, tal
+  movimiento es obligatorio.
+  Pero, si una de sus piezas toque la pelota, entonces puede golpearla
+  después de un desplazamiento inicial o inmediatamente, siempre que el paso
+  definido por la flecha pieza --> globo es compatible con los
+  movimientos de este último.
+
+p.
+  Para mover la bola, haga clic en ella y luego en la casilla de llegada.
+  El caballo puede enviar la pelota a cualquier casilla dentro
+  distancia del saltador desde la posición inicial del balón, excepto en las
+  casillas adyacentes al caballo.
+  Sin embargo, cuando la pelota está en una esquina permitimos una excepción
+  a esto regla: en el siguiente diagrama, el caballo puede enviar la pelota
+  de g1 a h3 por ejemplo.
 
 figure.diagram-container
-  .diagram
-    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
-  figcaption 1...Nc3 y 2...N(x)d1# gana para las negras.
+  .diagram.diag12
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba:
+  .diagram.diag22
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1:
+  figcaption Antes y después del golpe g1-h3
+
+h4 Algunas restricciones
+
+p.
+  Para evitar ciclos interminables, cada pieza puede golpear la pelota
+  como máximo una vez durante un turno.
+
+p Ninguna pieza puede estar en una casilla portería.
 
-h3 Fuente
+p El balón nunca puede alcanzar o cruzar la portería horizontalmente.
+
+h4 Completa una jugada en la interfaz.
+
+p.
+  Si al final de un movimiento inicial o intermedio una de tus piezas es al
+  lado de la bola, entonces tienes que "capturar" una de tus piezas con la
+  bola para indicar el final de la ronda.
+
+h3 Más información
 
 p
-  | Esta variante se menciona 
-  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
-    | en esta pagina
-  | . No está claro si la pieza debe estar a salvo de los ataques.
+  | Ver la 
+  a(href="https://www.chessvariants.com/crossover.dir/football.html")
+    | página chessvariants.com
+  | , por ejemplo, y también la 
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | Classified Encyclopedia of Chess Variants
+  | .
+
+p Inventor: Joseph Boyer (1951)
index 3af7b57..f06bc90 100644 (file)
@@ -1,21 +1,57 @@
-p.boxed.
-  Gagnez en jouant un coup au centre de la dernière rangée.
-  Le roi n'a pas de statut royal.
+p.boxed
+  | Les pièces ne capturent pas mais peuvent frapper un ballon.
+  | L'objectif du jeu est d'envoyer le ballon dans le but adverse.
 
 p.
-  Le but des blancs (resp. des noirs) sont les cases d8 et e8 (resp. d1 et e1)
-  au milieu de la rangée opposée. Le premier joueur à y installer une pièce
-  gagne, même si celle-ci est attaquée.
+  À chaque tour, un jour peut d'abord effectuer un coup normal (non capturant).
+  Si aucune de ses pièces n'est à côté du ballon, un tel coup est obligatoire.
+  Mais, si une de ses pièces touche le ballon, alors il peut le frapper soit
+  après un déplacement initial soit tout de suite, à condition que le pas
+  défini par la flèche pièce --> ballon soit compatible avec les
+  mouvements de cette dernière.
+
+p.
+  Pour déplacer la balle, cliquez dessus puis sur la case d'arrivée.
+  Le cavalier peut envoyer le ballon vers n'importe quelle case située à
+  distance de cavalier de la position initiale du ballon, sauf sur les
+  cases adjacentes au cavalier.
+  Cependant, quand la balle est dans un coin on autorise une exception à cette
+  règle : sur le diagramme suivant, le cavalier peut envoyer le ballon
+  de g1 à h3 par exemple.
 
 figure.diagram-container
-  .diagram
-    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
-  figcaption 1...Nc3 et 2...N(x)d1# gagne pour les noirs.
+  .diagram.diag12
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/9/7N1/RNB2K1Ba:
+  .diagram.diag22
+    | fen:3q1k1br/r7R/2n2n3/3Q5/6b2/9/7a1/7N1/RNB2K1B1:
+  figcaption Avant et après la frappe g1-h3
+
+h4 Quelques restrictions
+
+p.
+  Afin d'éviter des cycles infinis, chaque pièce peut frapper le ballon
+  au plus une fois pendant un tour.
+
+p Aucune pièce ne peut se trouver sur une case but.
 
-h3 Source
+p La balle ne peut jamais atteindre ou traverser le but horizontalement.
+
+h4 Compléter un coup sur l'interface.
+
+p.
+  Si à la fin d'un coup initial ou intermédiaire une de vos pièces est à côté
+  du ballon, alors vous devez "capturer" une de vos pièces avec la balle
+  pour indiquer la fin du tour.
+
+h3 Plus d'information
 
 p
-  | Cette variante est mentionnée 
-  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
-    | sur cette page
-  | . Il n'est pas précisé si la pièce doit être à l'abri des attaques.
+  | Voir la 
+  a(href="https://www.chessvariants.com/crossover.dir/football.html")
+    | page chessvariants.com
+  | , par exemple, et également la 
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | Classified Encyclopedia of Chess Variants
+  | .
+
+p Inventeur : Joseph Boyer (1951)
diff --git a/client/src/translations/rules/Squatter2/en.pug b/client/src/translations/rules/Squatter2/en.pug
new file mode 100644 (file)
index 0000000..34fb52b
--- /dev/null
@@ -0,0 +1,21 @@
+p.boxed.
+  Win by playing a move on the middle of last rank.
+  The king has no royal status.
+
+p.
+  The goal for White (resp. Black) are the squares d8 and e8 (resp. d1 and
+  e1) in the middle of the opposing rank. The first player to settle a
+  piece there wins, even if it is attacked.
+
+figure.diagram-container
+  .diagram
+    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
+  figcaption 1...Nc3 and 2...N(x)d1# wins for Black.
+
+h3 Source
+
+p
+  | This variant is mentioned 
+  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
+    | on this page
+  | . It isn't specified if the winning piece needs to be safe from attacks.
diff --git a/client/src/translations/rules/Squatter2/es.pug b/client/src/translations/rules/Squatter2/es.pug
new file mode 100644 (file)
index 0000000..2ca6e47
--- /dev/null
@@ -0,0 +1,21 @@
+p.boxed.
+  Gana haciendo un movimiento en el centro de la última fila.
+  El rey no tiene estatus real.
+
+p.
+  El objetivo de las blancas (resp. negras) son las casillas d8 y e8
+  (resp. d1 y e1) en el medio de la fila opuesta.
+  El primer jugador que instala una pieza ahí gana, incluso si es atacada.
+
+figure.diagram-container
+  .diagram
+    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
+  figcaption 1...Nc3 y 2...N(x)d1# gana para las negras.
+
+h3 Fuente
+
+p
+  | Esta variante se menciona 
+  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
+    | en esta pagina
+  | . No está claro si la pieza debe estar a salvo de los ataques.
diff --git a/client/src/translations/rules/Squatter2/fr.pug b/client/src/translations/rules/Squatter2/fr.pug
new file mode 100644 (file)
index 0000000..3af7b57
--- /dev/null
@@ -0,0 +1,21 @@
+p.boxed.
+  Gagnez en jouant un coup au centre de la dernière rangée.
+  Le roi n'a pas de statut royal.
+
+p.
+  Le but des blancs (resp. des noirs) sont les cases d8 et e8 (resp. d1 et e1)
+  au milieu de la rangée opposée. Le premier joueur à y installer une pièce
+  gagne, même si celle-ci est attaquée.
+
+figure.diagram-container
+  .diagram
+    | fen:2r5/p1p1b2p/1pbp4/5K2/P1PP4/1P6/4nPPP/3R1NR1 c3:
+  figcaption 1...Nc3 et 2...N(x)d1# gagne pour les noirs.
+
+h3 Source
+
+p
+  | Cette variante est mentionnée 
+  a(href="http://abrobecker.free.fr/chess/fairyblitz.htm#football")
+    | sur cette page
+  | . Il n'est pas précisé si la pièce doit être à l'abri des attaques.
index 66c1493..55c778d 100644 (file)
@@ -425,7 +425,8 @@ p Orthodox rules, but the goal is not checkmate (or not only).
     "Threechecks",
     "Kinglet",
     "Koth",
-    "Squatter"
+    "Squatter1",
+    "Squatter2"
   ]
 ul
   for v in varlist
index 39eda80..7a3dea3 100644 (file)
@@ -435,7 +435,8 @@ p Reglas ortodoxas, pero el objetivo no es jaque mate (o no solo).
     "Threechecks",
     "Kinglet",
     "Koth",
-    "Squatter"
+    "Squatter1",
+    "Squatter2"
   ]
 ul
   for v in varlist
index 9cf10ed..9673a53 100644 (file)
@@ -433,7 +433,8 @@ p Règles orthodoxes, mais le but n'est pas de mater (ou pas seulement).
     "Threechecks",
     "Kinglet",
     "Koth",
-    "Squatter"
+    "Squatter1",
+    "Squatter2"
   ]
 ul
   for v in varlist
index 04ad1fe..5de871d 100644 (file)
@@ -36,7 +36,7 @@ export class BallRules extends ChessRules {
   }
 
   static get BALL() {
-    // Ball is already taken:
+    // 'b' is already taken:
     return "aa";
   }
 
@@ -95,17 +95,18 @@ export class BallRules extends ChessRules {
     const rows = position.split("/");
     if (rows.length != V.size.x) return false;
     let pieces = { "w": 0, "b": 0 };
-    const withBall = Object.keys(V.HAS_BALL_DECODE).concat([V.BALL]);
+    const withBall = Object.keys(V.HAS_BALL_DECODE).concat(['a']);
     let ballCount = 0;
     for (let row of rows) {
       let sumElts = 0;
       for (let i = 0; i < row.length; i++) {
         const lowerRi = row[i].toLowerCase();
         if (V.PIECES.includes(lowerRi)) {
-          if (lowerRi != V.BALL) pieces[row[i] == lowerRi ? "b" : "w"]++;
+          if (lowerRi != 'a') pieces[row[i] == lowerRi ? "b" : "w"]++;
           if (withBall.includes(lowerRi)) ballCount++;
           sumElts++;
-        } else {
+        }
+        else {
           const num = parseInt(row[i], 10);
           if (isNaN(num)) return false;
           sumElts += num;
@@ -337,7 +338,8 @@ export class BallRules extends ChessRules {
             })
           );
         }
-      } else if (mv.vanish[1].c == mv.vanish[0].c) {
+      }
+      else if (mv.vanish[1].c == mv.vanish[0].c) {
         // Pass the ball: the passing unit does not disappear
         mv.appear.push(JSON.parse(JSON.stringify(mv.vanish[0])));
         mv.appear[0].p = V.HAS_BALL_CODE[mv.vanish[1].p];
index 430e31a..b1630ea 100644 (file)
@@ -1,47 +1,55 @@
 import { ChessRules } from "@/base_rules";
-import { SuicideRules } from "@/variants/Suicide";
+import { randInt } from "@/utils/alea";
 
 export class FootballRules extends ChessRules {
 
+  static get HasEnpassant() {
+    return false;
+  }
+
   static get HasFlags() {
     return false;
   }
 
-  static get PawnSpecs() {
-    return Object.assign(
-      {},
-      ChessRules.PawnSpecs,
-      { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
-    );
+  static get size() {
+    return { x: 9, y: 9 };
   }
 
   static get Lines() {
     return [
       // White goal:
-      [[0, 3], [0, 5]],
+      [[0, 4], [0, 5]],
       [[0, 5], [1, 5]],
-      [[1, 5], [1, 3]],
-      [[1, 3], [0, 3]],
+      [[1, 4], [0, 4]],
       // Black goal:
-      [[8, 3], [8, 5]],
-      [[8, 5], [7, 5]],
-      [[7, 5], [7, 3]],
-      [[7, 3], [8, 3]]
+      [[9, 4], [9, 5]],
+      [[9, 5], [8, 5]],
+      [[8, 4], [9, 4]]
     ];
   }
 
+  static get BALL() {
+    // 'b' is already taken:
+    return "aa";
+  }
+
+  // Check that exactly one ball is on the board
+  // + at least one piece per color.
   static IsGoodPosition(position) {
     if (position.length == 0) return false;
     const rows = position.split("/");
     if (rows.length != V.size.x) return false;
-    // Just check that at least one piece of each color is there:
     let pieces = { "w": 0, "b": 0 };
+    let ballCount = 0;
     for (let row of rows) {
       let sumElts = 0;
       for (let i = 0; i < row.length; i++) {
         const lowerRi = row[i].toLowerCase();
-        if (V.PIECES.includes(lowerRi)) {
-          pieces[row[i] == lowerRi ? "b" : "w"]++;
+        if (!!lowerRi.match(/^[a-z]$/)) {
+          if (V.PIECES.includes(lowerRi))
+            pieces[row[i] == lowerRi ? "b" : "w"]++;
+          else if (lowerRi == 'a') ballCount++;
+          else return false;
           sumElts++;
         }
         else {
@@ -52,37 +60,340 @@ export class FootballRules extends ChessRules {
       }
       if (sumElts != V.size.y) return false;
     }
-    if (Object.values(pieces).some(v => v == 0)) return false;
+    if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
+      return false;
     return true;
   }
 
-  scanKings() {}
+  static board2fen(b) {
+    if (b == V.BALL) return 'a';
+    return ChessRules.board2fen(b);
+  }
 
-  filterValid(moves) {
+  static fen2board(f) {
+    if (f == 'a') return V.BALL;
+    return ChessRules.fen2board(f);
+  }
+
+  getPpath(b) {
+    if (b == V.BALL) return "Football/ball";
+    return b;
+  }
+
+  canIplay(side, [x, y]) {
+    return (
+      side == this.turn &&
+      (this.board[x][y] == V.BALL || this.getColor(x, y) == side)
+    );
+  }
+
+  // No checks or king tracking etc. But, track ball
+  setOtherVariables() {
+    // Stack of "kicked by" coordinates, to avoid infinite loops
+    this.kickedBy = [ {} ];
+    this.subTurn = 1;
+    this.ballPos = [-1, -1];
+    for (let i=0; i < V.size.x; i++) {
+      for (let j=0; j< V.size.y; j++) {
+        if (this.board[i][j] == V.BALL) {
+          this.ballPos = [i, j];
+          return;
+        }
+      }
+    }
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0)
+      return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0";
+
+    // TODO: following is mostly copy-paste from Suicide variant
+    let pieces = { w: new Array(8), b: new Array(8) };
+    for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
+      // Get random squares for every piece, totally freely
+      let positions = shuffle(ArrayFun.range(8));
+      const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
+      const rem2 = positions[0] % 2;
+      if (rem2 == positions[1] % 2) {
+        // Fix bishops (on different colors)
+        for (let i=2; i<8; i++) {
+          if (positions[i] % 2 != rem2)
+            [positions[1], positions[i]] = [positions[i], positions[1]];
+        }
+      }
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
+    }
+    const piecesB = pieces["b"].join("") ;
+    const piecesW = pieces["w"].join("").toUpperCase();
+    return (
+      piecesB.substr(0, 4) + "1" + piecesB.substr(4) +
+      "/9/9/9/4a4/9/9/9/" +
+      piecesW.substr(0, 4) + "1" + piecesW.substr(4) +
+      " w 0"
+    );
+  }
+
+  tryKickFrom([x, y]) {
+    const bp = this.ballPos;
+    const emptySquare = (i, j) => {
+      return V.OnBoard(i, j) && this.board[i][j] == V.EMPTY;
+    };
+    // Kick the (adjacent) ball from x, y with current turn:
+    const step = [bp[0] - x, bp[1] - y];
+    const piece = this.getPiece(x, y);
+    let moves = [];
+    if (piece == V.KNIGHT) {
+      // The knight case is particular
+      V.steps[V.KNIGHT].forEach(s => {
+        const [i, j] = [bp[0] + s[0], bp[1] + s[1]];
+        if (
+          V.OnBoard(i, j) &&
+          this.board[i][j] == V.EMPTY &&
+          (
+            // In a corner? The, allow all ball moves
+            ([0, 8].includes(bp[0]) && [0, 8].includes(bp[1])) ||
+            // Do not end near the knight
+            (Math.abs(i - x) >= 2 || Math.abs(j - y) >= 2)
+          )
+        ) {
+          moves.push(super.getBasicMove(bp, [i, j]));
+        }
+      });
+    }
+    else {
+      let compatible = false,
+          oneStep = false;
+      switch (piece) {
+        case V.ROOK:
+          compatible = (step[0] == 0 || step[1] == 0);
+          break;
+        case V.BISHOP:
+          compatible = (step[0] != 0 && step[1] != 0);
+          break;
+        case V.QUEEN:
+          compatible = true;
+          break;
+        case V.KING:
+          compatible = true;
+          oneStep = true;
+          break;
+      }
+      if (!compatible) return [];
+      let [i, j] = [bp[0] + step[0], bp[1] + step[1]];
+      const horizontalStepOnGoalRow =
+        ([0, 8].includes(bp[0]) && step.some(s => s == 0));
+      if (emptySquare(i, j) && (!horizontalStepOnGoalRow || j != 4)) {
+        moves.push(super.getBasicMove(bp, [i, j]));
+        if (!oneStep) {
+          do {
+            i += step[0];
+            j += step[1];
+            if (!emptySquare(i, j)) break;
+            if (!horizontalStepOnGoalRow || j != 4)
+              moves.push(super.getBasicMove(bp, [i, j]));
+          } while (true);
+        }
+      }
+    }
+    const kickedFrom = x + "-" + y;
+    moves.forEach(m => m.by = kickedFrom)
     return moves;
   }
 
+  getPotentialMovesFrom([x, y], computer) {
+    if (V.PIECES.includes(this.getPiece(x, y))) {
+      if (this.subTurn > 1) return [];
+      return (
+        super.getPotentialMovesFrom([x, y])
+        .filter(m => m.end.y != 4 || ![0, 8].includes(m.end.x))
+      );
+    }
+    // Kicking the ball: look for adjacent pieces.
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    const c = this.turn;
+    let moves = [];
+    for (let s of steps) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == c
+      ) {
+        Array.prototype.push.apply(moves, this.tryKickFrom([i, j]));
+      }
+    }
+    // And, always add the "end" move. For computer, keep only one
+    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) == c) {
+          moves.push(super.getBasicMove([x, y], [i, j]));
+          if (!!computer) break outerLoop;
+        }
+      }
+    }
+    return moves;
+  }
+
+  // No captures:
+  getSlideNJumpMoves([x, y], steps, oneStep) {
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      let i = x + step[0];
+      let j = y + step[1];
+      let stepCount = 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];
+        stepCount++;
+      }
+    }
+    return moves;
+  }
+
+  // Extra arg "computer" to avoid trimming all redundant pass moves:
+  getAllPotentialMoves(computer) {
+    const color = this.turn;
+    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], computer)
+          );
+        }
+      }
+    }
+    return potentialMoves;
+  }
+
+  getAllValidMoves() {
+    return this.filterValid(this.getAllPotentialMoves("computer"));
+  }
+
+  filterValid(moves) {
+    const L = this.kickedBy.length;
+    const kb = this.kickedBy[L-1];
+    return moves.filter(m => !m.by || !kb[m.by]);
+  }
+
   getCheckSquares() {
     return [];
   }
 
-  // No variables update because no royal king + no castling
-  prePlay() {}
-  postPlay() {}
-  preUndo() {}
-  postUndo() {}
+  allowAnotherPass(color) {
+    // Two cases: a piece moved, or the ball moved.
+    // In both cases, check our pieces and ball proximity,
+    // so the move played doesn't matter (if ball position updated)
+    const bp = this.ballPos;
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let s of steps) {
+      const [i, j] = [this.ballPos[0] + s[0], this.ballPos[1] + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == color
+      ) {
+        return true; //potentially...
+      }
+    }
+    return false;
+  }
+
+  prePlay(move) {
+    if (move.appear[0].p == 'a')
+      this.ballPos = [move.appear[0].x, move.appear[0].y];
+  }
+
+  play(move) {
+    // Special message saying "passes are over"
+    const passesOver = (move.vanish.length == 2);
+    if (!passesOver) {
+      this.prePlay(move);
+      V.PlayOnBoard(this.board, move);
+    }
+    move.turn = [this.turn, this.subTurn]; //easier undo
+    if (passesOver || !this.allowAnotherPass(this.turn)) {
+      this.turn = V.GetOppCol(this.turn);
+      this.subTurn = 1;
+      this.movesCount++;
+      this.kickedBy.push( {} );
+    }
+    else {
+      this.subTurn++;
+      if (!!move.by) {
+        const L = this.kickedBy.length;
+        this.kickedBy[L-1][move.by] = true;
+      }
+    }
+  }
+
+  undo(move) {
+    const passesOver = (move.vanish.length == 2);
+    if (move.turn[0] != this.turn) {
+      [this.turn, this.subTurn] = move.turn;
+      this.movesCount--;
+      this.kickedBy.pop();
+    }
+    else {
+      this.subTurn--;
+      if (!!move.by) {
+        const L = this.kickedBy.length;
+        delete this.kickedBy[L-1][move.by];
+      }
+    }
+    if (!passesOver) {
+      V.UndoOnBoard(this.board, move);
+      this.postUndo(move);
+    }
+  }
+
+  postUndo(move) {
+    if (move.vanish[0].p == 'a')
+      this.ballPos = [move.vanish[0].x, move.vanish[0].y];
+  }
 
   getCurrentScore() {
-    const oppCol = V.GetOppCol(this.turn);
-    const goal = (oppCol == 'w' ? 0 : 7);
-    if (this.board[goal].slice(3, 5).some(b => b[0] == oppCol))
-      return oppCol == 'w' ? "1-0" : "0-1";
-    if (this.atLeastOneMove()) return "*";
-    return "1/2";
+    if (this.board[0][4] == V.BALL) return "1-0";
+    if (this.board[8][4] == V.BALL) return "0-1";
+    return "*";
   }
 
-  static GenRandInitFen(randomness) {
-    return SuicideRules.GenRandInitFen(randomness);
+  getComputerMove() {
+    let initMoves = this.getAllValidMoves();
+    if (initMoves.length == 0) return null;
+    let moves = JSON.parse(JSON.stringify(initMoves));
+    let mvArray = [];
+    let mv = null;
+    // Just play random moves (for now at least. TODO?)
+    const c = this.turn;
+    while (moves.length > 0) {
+      mv = moves[randInt(moves.length)];
+      mvArray.push(mv);
+      this.play(mv);
+      if (mv.vanish.length == 1 && this.allowAnotherPass(c))
+        // Potential kick
+        moves = this.getPotentialMovesFrom(this.ballPos);
+      else break;
+    }
+    for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+    return (mvArray.length > 1 ? mvArray : mvArray[0]);
+  }
+
+  // NOTE: evalPosition() is wrong, but unused since bot plays at random
+
+  getNotation(move) {
+    if (move.vanish.length == 2) return "pass";
+    if (move.vanish[0].p != 'a') return super.getNotation(move);
+    // Kick: simple notation (TODO?)
+    return V.CoordsToSquare(move.end);
   }
 
 };
similarity index 98%
rename from client/src/variants/Squatter.js
rename to client/src/variants/Squatter1.js
index f997679..a4aafd3 100644 (file)
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export class SquatterRules extends ChessRules {
+export class Squatter1Rules extends ChessRules {
 
   static get Lines() {
     return [
diff --git a/client/src/variants/Squatter2.js b/client/src/variants/Squatter2.js
new file mode 100644 (file)
index 0000000..15caaf5
--- /dev/null
@@ -0,0 +1,88 @@
+import { ChessRules } from "@/base_rules";
+import { SuicideRules } from "@/variants/Suicide";
+
+export class Squatter2Rules extends ChessRules {
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
+    );
+  }
+
+  static get Lines() {
+    return [
+      // White goal:
+      [[0, 3], [0, 5]],
+      [[0, 5], [1, 5]],
+      [[1, 5], [1, 3]],
+      [[1, 3], [0, 3]],
+      // Black goal:
+      [[8, 3], [8, 5]],
+      [[8, 5], [7, 5]],
+      [[7, 5], [7, 3]],
+      [[7, 3], [8, 3]]
+    ];
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    // Just check that at least one piece of each color is there:
+    let pieces = { "w": 0, "b": 0 };
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        const lowerRi = row[i].toLowerCase();
+        if (V.PIECES.includes(lowerRi)) {
+          pieces[row[i] == lowerRi ? "b" : "w"]++;
+          sumElts++;
+        }
+        else {
+          const num = parseInt(row[i], 10);
+          if (isNaN(num)) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    if (Object.values(pieces).some(v => v == 0)) return false;
+    return true;
+  }
+
+  scanKings() {}
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  // No variables update because no royal king + no castling
+  prePlay() {}
+  postPlay() {}
+  preUndo() {}
+  postUndo() {}
+
+  getCurrentScore() {
+    const oppCol = V.GetOppCol(this.turn);
+    const goal = (oppCol == 'w' ? 0 : 7);
+    if (this.board[goal].slice(3, 5).some(b => b[0] == oppCol))
+      return oppCol == 'w' ? "1-0" : "0-1";
+    if (this.atLeastOneMove()) return "*";
+    return "1/2";
+  }
+
+  static GenRandInitFen(randomness) {
+    return SuicideRules.GenRandInitFen(randomness);
+  }
+
+};
index 13f2e8a..3a5e517 100644 (file)
@@ -131,7 +131,8 @@ insert or ignore into Variants (name, description) values
   ('Shogi', 'Japanese Chess'),
   ('Sittuyin', 'Burmese Chess'),
   ('Spartan', 'Spartan versus Persians'),
-  ('Squatter', 'Squat last rank'),
+  ('Squatter1', 'Squat last rank (v1)'),
+  ('Squatter2', 'Squat last rank (v2)'),
   ('Suicide', 'Lose all pieces'),
   ('Suction', 'Attract opposite king'),
   ('Swap', 'Dangerous captures'),