From 7ba4a5bc5b64e19a1e7f26aa232d5c50770d07ad Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 4 Mar 2020 16:12:30 +0100
Subject: [PATCH] Experimental symmetric randomness + deterministic option

---
 TODO                                    |  8 ----
 client/src/base_rules.js                | 16 ++++++--
 client/src/components/BaseGame.vue      |  6 ++-
 client/src/components/ChallengeList.vue | 15 +++++++
 client/src/translations/about/en.pug    | 12 +++---
 client/src/translations/about/es.pug    | 12 +++---
 client/src/translations/about/fr.pug    | 12 +++---
 client/src/translations/en.js           |  5 +++
 client/src/translations/es.js           |  5 +++
 client/src/translations/fr.js           |  5 +++
 client/src/utils/timeControl.js         |  2 +
 client/src/variants/Allmate1.js         |  4 +-
 client/src/variants/Allmate2.js         |  4 +-
 client/src/variants/Antiking.js         | 11 ++++-
 client/src/variants/Arena.js            |  6 ++-
 client/src/variants/Baroque.js          | 12 +++++-
 client/src/variants/Checkered.js        |  8 ++--
 client/src/variants/Circular.js         | 11 ++++-
 client/src/variants/Crazyhouse.js       |  4 +-
 client/src/variants/Grand.js            | 13 +++++-
 client/src/variants/Grasshopper.js      |  4 +-
 client/src/variants/Hidden.js           |  1 +
 client/src/variants/Hiddenqueen.js      |  6 +--
 client/src/variants/Knightmate.js       | 53 ++-----------------------
 client/src/variants/Losers.js           | 14 ++++++-
 client/src/variants/Recycle.js          |  4 +-
 client/src/variants/Royalrace.js        | 14 ++++++-
 client/src/variants/Shatranj.js         |  4 +-
 client/src/variants/Suction.js          |  4 +-
 client/src/variants/Threechecks.js      |  8 ++--
 client/src/variants/Upsidedown.js       | 11 ++++-
 client/src/variants/Wildebeest.js       | 13 +++++-
 client/src/views/Analyse.vue            | 12 +++---
 client/src/views/Hall.vue               | 14 ++++++-
 server/db/create.sql                    |  1 +
 server/models/Challenge.js              | 13 +++---
 server/routes/challenges.js             |  1 +
 37 files changed, 222 insertions(+), 126 deletions(-)

diff --git a/TODO b/TODO
index ab06e20a..7e9c4834 100644
--- a/TODO
+++ b/TODO
@@ -7,14 +7,6 @@ From MyGames page: send "mconnect" to all online players (me included: potential
   When quit, send mdisconnect (relayed by server if no other MyGames tab).
 And remove current "notify through newmove" on server in sockets.js
 
-Analyse mode when launched from a position: should keep orientation
---> $route query param, "side=w or b"
-
-Subcursor (intra move) for multi-move variants for better navigation (in BaseGame)
-
-Allow symmetric mode in all variants (vertical symmetry for racing kings)
-flag in challenge, "Symmetric: true / false", option of GenRandInitFen()
-
 # Misc:
 Saw once a "double challenge" bug, one anonymous and a second one logged
 Both were asked a challenge probably, and both challenges added as different ones.
diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 5b335b0f..93772b9b 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -238,11 +238,21 @@ export const ChessRules = class ChessRules {
   /////////////
   // FEN UTILS
 
-  // Setup the initial random (assymetric) position
-  static GenRandInitFen() {
+  // Setup the initial random (asymmetric) position
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      // Deterministic:
+      return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 1111 -";
+
     let pieces = { w: new Array(8), b: new Array(8) };
-    // Shuffle pieces on first and last rank
+    // Shuffle pieces on first (and last rank if randomness == 2)
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(8);
 
       // Get random squares for bishops
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 2a1a4474..4af4e153 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -227,11 +227,13 @@ export default {
         this.lastMove = null;
     },
     analyzePosition: function() {
-      const newUrl =
+      let newUrl =
         "/analyse/" +
         this.game.vname +
         "/?fen=" +
         this.vr.getFen().replace(/ /g, "_");
+      if (this.game.mycolor)
+        newUrl += "&side=" + this.game.mycolor;
       // Open in same tab in live games (against cheating)
       if (this.game.type == "live") this.$router.push(newUrl);
       else window.open("#" + newUrl);
@@ -337,7 +339,7 @@ export default {
         }
       };
       const playMove = () => {
-        const animate = V.ShowMoves == "all" && received;
+        const animate = V.ShowMoves == "all" && (received || navigate);
         if (!Array.isArray(move)) move = [move];
         let moveIdx = 0;
         let self = this;
diff --git a/client/src/components/ChallengeList.vue b/client/src/components/ChallengeList.vue
index a350c705..7572ede1 100644
--- a/client/src/components/ChallengeList.vue
+++ b/client/src/components/ChallengeList.vue
@@ -6,6 +6,7 @@ div
         th {{ st.tr["Variant"] }}
         th {{ st.tr["With"] }}
         th {{ st.tr["Cadence"] }}
+        th {{ st.tr["Random?"] }}
     tbody
       tr(
         v-for="c in sortedChallenges"
@@ -15,6 +16,7 @@ div
         td {{ c.vname }}
         td {{ withWho(c) }}
         td {{ c.cadence }}
+        td(:class="getRandomnessClass(c)")
 </template>
 
 <script>
@@ -52,6 +54,11 @@ export default {
       if (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id)
         return c.to || this.st.tr["Any player"];
       return c.from.name || "@nonymous";
+    },
+    getRandomnessClass: function(c) {
+      return {
+        ["random-" + c.randomness]: true
+      };
     }
   }
 };
@@ -63,4 +70,12 @@ tr.fromyou > td
   font-style: italic
 tr.toyou > td
   background-color: #fcd785
+
+tr > td:last-child
+  &.random-0
+    background-color: #FF5733
+  &.random-1
+    background-color: #2B63B4
+  &.random-2
+    background-color: #33B42B
 </style>
diff --git a/client/src/translations/about/en.pug b/client/src/translations/about/en.pug
index a47463ce..3968dac4 100644
--- a/client/src/translations/about/en.pug
+++ b/client/src/translations/about/en.pug
@@ -9,12 +9,14 @@ p.
   A few years later I had a prototype to play in live, then other variants
   were added and the website was made more attractive.
 
-h3 Philosophy
+h3 Notes
 
-p Have fun and challenge your mind :-)
-ul
-  li ELO rating is purposely absent from this website.
-  li Games start with a random assymetric position.
+p ELO rating is purposely absent from this website.
+
+p.
+  Games start by default with a random asymmetric position.
+  Random symmetric or even deterministic positions are available too,
+  if you prefer more fairness (but less fun? :-P ).
 
 h3 Contribute
 
diff --git a/client/src/translations/about/es.pug b/client/src/translations/about/es.pug
index 4566df84..f240ef89 100644
--- a/client/src/translations/about/es.pug
+++ b/client/src/translations/about/es.pug
@@ -9,12 +9,14 @@ p.
   para jugar en vivo, y luego otras variantes se agregaron
   y el sitio se hizo más atractivo.
 
-h3 Filosofía
+h3 Notas
 
-p Diviértete y desafía tu mente:-)
-ul
-  li El ranking ELO está ausente a propósito de este sitio.
-  li Las partidas comienzan con una posición aleatoria no simétrica.
+p El ranking ELO está ausente a propósito de este sitio.
+
+p.
+  Las partidas comienzan por defecto con una posición aleatoria no simétrica.
+  Es posible una posición aleatoria simétrica o incluso determinista,
+  si prefieres más justicia (¿pero más aburrimiento? :-P).
 
 h3 Contribuir
 
diff --git a/client/src/translations/about/fr.pug b/client/src/translations/about/fr.pug
index 2860445d..b994c792 100644
--- a/client/src/translations/about/fr.pug
+++ b/client/src/translations/about/fr.pug
@@ -9,12 +9,14 @@ p.
   tard je disposais d'un prototype pour jouer en direct, puis d'autres
   variantes se sont ajoutées et le site a été rendu plus attractif.
 
-h3 Philosophie
+h3 Notes
 
-p Exercez vos neurones en vous amusant :-)
-ul
-  li Le classement ELO est à dessein absent de ce site.
-  li Les parties démarrent avec une position aléatoire non symétrique.
+p Le classement ELO est à dessein absent de ce site.
+
+p.
+  Les parties démarrent par défaut avec une position aléatoire non symétrique.
+  Une position aléatoire symétrique ou même déterministe est possible,
+  si vous préférez plus de justice (mais plus d'ennui ? :-P ).
 
 h3 Contribuer
 
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index e7fcb24d..856034cc 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -9,6 +9,7 @@ export const translations = {
   "Any player": "Any player",
   Apply: "Apply",
   "Are you sure?": "Are you sure?",
+  "Asymmetric random": "Asymmetric random",
   "Authentication successful!": "Authentication successful!",
   "Back to Hall in 3 seconds...": "Back to Hall in 3 seconds...",
   "Back to list": "Back to list",
@@ -30,6 +31,7 @@ export const translations = {
   "Correspondance games": "Correspondance games",
   "Database error: stop private browsing, or update your browser": "Database error: stop private browsing, or update your browser",
   Delete: "Delete",
+  Deterministic: "Deterministic",
   Download: "Download",
   Draw: "Draw",
   "Draw offer only in your turn": "Draw offer only in your turn",
@@ -92,6 +94,8 @@ export const translations = {
   "Processing... Please wait": "Processing... Please wait",
   Problems: "Problems",
   "participant(s):": "participant(s):",
+  "Random?": "Random?",
+  "Randomness": "Randomness",
   Refuse: "Refuse",
   Register: "Register",
   "Registration complete! Please check your emails now": "Registration complete! Please check your emails now",
@@ -112,6 +116,7 @@ export const translations = {
   Stop: "Stop",
   "Stop game": "Stop game",
   Subject: "Subject",
+  "Symmetric random": "Symmetric random",
   "Terminate game?": "Terminate game?",
   "Three repetitions": "Three repetitions",
   Time: "Time",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 70e00e70..15926180 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -9,6 +9,7 @@ export const translations = {
   "Any player": "Cualquier jugador",
   Apply: "Aplicar",
   "Are you sure?": "¿Está usted seguro?",
+  "Asymmetric random": "Aleatorio asimétrico",
   "Authentication successful!": "¡Autenticación exitosa!",
   "Back to Hall in 3 seconds...": "Regreso al salón en 3 segundos...",
   "Back to list": "Volver a la lista",
@@ -30,6 +31,7 @@ export const translations = {
   "Correspondance games": "Partidas por correspondencia",
   "Database error: stop private browsing, or update your browser": "Error de la base de datos: detener la navegación privada, o actualizar su navegador",
   Delete: "Borrar",
+  Deterministic: "Determinista",
   Download: "Descargar",
   Draw: "Tablas",
   "Draw offer only in your turn": "Oferta de tablas solo en tu turno",
@@ -92,6 +94,8 @@ export const translations = {
   "Processing... Please wait": "Procesando... por favor espere",
   Problems: "Problemas",
   "participant(s):": "participante(s):",
+  "Random?": "Aleatorio?",
+  "Randomness": "Grado de azar",
   Refuse: "Rechazar",
   Register: "Registrarse",
   "Registration complete! Please check your emails now": "¡Registro completo! Revise sus correos electrónicos ahora",
@@ -112,6 +116,7 @@ export const translations = {
   Stop: "Interrupción",
   "Stop game": "Terminar la partida",
   Subject: "Asunto",
+  "Symmetric random": "Aleatorio simétrico",
   "Terminate game?": "¿Terminar la partida?",
   "Three repetitions": "Tres repeticiones",
   Time: "Tiempo",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 3fa73d08..848241a3 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -10,6 +10,7 @@ export const translations = {
   Apply: "Appliquer",
   "Authentication successful!": "Authentification réussie !",
   "Are you sure?": "Étes vous sûr?",
+  "Asymmetric random": "Aléatoire asymétrique",
   "Back to Hall in 3 seconds...": "Retour au Hall dans 3 secondes...",
   "Back to list": "Retour à la liste",
   "Black to move": "Trait aux noirs",
@@ -30,6 +31,7 @@ export const translations = {
   "Correspondance games": "Parties par correspondance",
   "Database error: stop private browsing, or update your browser": "Erreur de base de données : arrêtez la navigation privée, ou mettez à jour votre navigateur",
   Delete: "Supprimer",
+  Deterministic: "Déterministe",
   Download: "Télécharger",
   Draw: "Nulle",
   "Draw offer only in your turn": "Proposition de nulle seulement sur votre temps",
@@ -92,6 +94,8 @@ export const translations = {
   "Processing... Please wait": "Traitement en cours... Attendez SVP",
   Problems: "Problèmes",
   "participant(s):": "participant(s) :",
+  "Random?": "Aléatoire?",
+  "Randomness": "Degré d'aléa",
   Refuse: "Refuser",
   Register: "S'enregistrer",
   "Registration complete! Please check your emails now": "Enregistrement terminé ! Allez voir vos emails maintenant",
@@ -112,6 +116,7 @@ export const translations = {
   Stop: "Arrêt",
   "Stop game": "Arrêter la partie",
   Subject: "Sujet",
+  "Symmetric random": "Aléatoire symétrique",
   "Terminate game?": "Stopper la partie ?",
   "Three repetitions": "Triple répétition",
   Time: "Temps",
diff --git a/client/src/utils/timeControl.js b/client/src/utils/timeControl.js
index b4bac053..c38d08b1 100644
--- a/client/src/utils/timeControl.js
+++ b/client/src/utils/timeControl.js
@@ -33,6 +33,8 @@ export function extractTime(cadence) {
   const mainTime = timeUnitToSeconds(mainTimeValue, mainTimeUnit);
   let increment = 0;
   if (tcParts.length >= 2) {
+    // Correspondance games don't use an increment:
+    if (mainTimeUnit == 'd') return null;
     tcParts[1] += "s";
     const incrementArray = tcParts[1].match(/^([0-9]+)([smhd]+)$/);
     if (!incrementArray) return null;
diff --git a/client/src/variants/Allmate1.js b/client/src/variants/Allmate1.js
index bf8f1be9..5511dd54 100644
--- a/client/src/variants/Allmate1.js
+++ b/client/src/variants/Allmate1.js
@@ -10,8 +10,8 @@ export const VariantRules = class Allmate1Rules extends ChessRules {
     return [];
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen().replace(/ -$/, "");
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness).replace(/ -$/, "");
   }
 
   getPotentialMovesFrom([x, y]) {
diff --git a/client/src/variants/Allmate2.js b/client/src/variants/Allmate2.js
index 34a0f8b0..0e515b2d 100644
--- a/client/src/variants/Allmate2.js
+++ b/client/src/variants/Allmate2.js
@@ -10,8 +10,8 @@ export const VariantRules = class Allmate2Rules extends ChessRules {
     return [];
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen().replace(/ -$/, "");
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness).replace(/ -$/, "");
   }
 
   getPotentialMovesFrom([x, y]) {
diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js
index 21e37f4b..1c062bbf 100644
--- a/client/src/variants/Antiking.js
+++ b/client/src/variants/Antiking.js
@@ -151,10 +151,19 @@ export const VariantRules = class AntikingRules extends ChessRules {
     );
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "rnbqkbnr/pppppppp/3A4/8/8/3a4/PPPPPPPP/RNBQKBNR w 0 1111 -";
+
     let pieces = { w: new Array(8), b: new Array(8) };
     let antikingPos = { w: -1, b: -1 };
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(8);
 
       // Get random squares for bishops, but avoid corners; because,
diff --git a/client/src/variants/Arena.js b/client/src/variants/Arena.js
index 42b1f4af..f3713e27 100644
--- a/client/src/variants/Arena.js
+++ b/client/src/variants/Arena.js
@@ -5,8 +5,10 @@ export const VariantRules = class ArenaRules extends ChessRules {
     return false;
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen().replace("w 1111 -", "w -");
+  static GenRandInitFen(randomness) {
+    return ChessRules
+      .GenRandInitFen(randomness)
+      .replace("w 0 1111 -", "w 0 -");
   }
 
   static InArena(x) {
diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js
index ae4b511f..a860fe79 100644
--- a/client/src/variants/Baroque.js
+++ b/client/src/variants/Baroque.js
@@ -545,10 +545,20 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     return 2;
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      // Deterministic:
+      return "rnbqkbnrm/pppppppp/8/8/8/8/PPPPPPPP/MNBKQBNR w 0";
+
     let pieces = { w: new Array(8), b: new Array(8) };
     // Shuffle pieces on first and last rank
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(8);
       // Get random squares for every piece, totally freely
 
diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js
index ac99602b..5f9bf8f6 100644
--- a/client/src/variants/Checkered.js
+++ b/client/src/variants/Checkered.js
@@ -295,10 +295,10 @@ export const VariantRules = class CheckeredRules extends ChessRules {
     return evaluation;
   }
 
-  static GenRandInitFen() {
-    const randFen = ChessRules.GenRandInitFen();
-    // Add 16 pawns flags + empty cmove:
-    return randFen.replace(" w 0 1111", " w 0 11111111111111111111 -");
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness)
+      // Add 16 pawns flags + empty cmove:
+      .replace(" w 0 1111", " w 0 11111111111111111111 -");
   }
 
   static ParseFen(fen) {
diff --git a/client/src/variants/Circular.js b/client/src/variants/Circular.js
index 9ec597bd..93df8efc 100644
--- a/client/src/variants/Circular.js
+++ b/client/src/variants/Circular.js
@@ -30,10 +30,19 @@ export const VariantRules = class CircularRules extends ChessRules {
     this.pawnFlags = flags;
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "8/8/pppppppp/rnbqkbnr/8/8/PPPPPPPP/RNBQKBNR w 0 1111111111111111";
+
     let pieces = { w: new Array(8), b: new Array(8) };
     // Shuffle pieces on first and fifth rank
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(8);
 
       // Get random squares for bishops
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index 24018d14..7dc53466 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -28,8 +28,8 @@ export const VariantRules = class CrazyhouseRules extends ChessRules {
     });
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen() + " 0000000000 -";
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness) + " 0000000000 -";
   }
 
   getFen() {
diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js
index d3659af8..c910dd8e 100644
--- a/client/src/variants/Grand.js
+++ b/client/src/variants/Grand.js
@@ -316,10 +316,21 @@ export const VariantRules = class GrandRules extends ChessRules {
     return 2;
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0) {
+      return "rnbqkmcbnr/pppppppppp/10/10/10/10/10/10/PPPPPPPPPP/RNBQKMCBNR " +
+        "w 0 1111 - 00000000000000";
+    }
+
     let pieces = { w: new Array(10), b: new Array(10) };
     // Shuffle pieces on first and last rank
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(10);
 
       // Get random squares for bishops
diff --git a/client/src/variants/Grasshopper.js b/client/src/variants/Grasshopper.js
index 8f24340a..a991035b 100644
--- a/client/src/variants/Grasshopper.js
+++ b/client/src/variants/Grasshopper.js
@@ -133,8 +133,8 @@ export const VariantRules = class GrasshopperRules extends ChessRules {
     );
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen()
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness)
       .replace("w 0 1111 -", "w 0 1111")
       .replace(
         "/pppppppp/8/8/8/8/PPPPPPPP/",
diff --git a/client/src/variants/Hidden.js b/client/src/variants/Hidden.js
index c79267a5..5ccc3d7e 100644
--- a/client/src/variants/Hidden.js
+++ b/client/src/variants/Hidden.js
@@ -157,6 +157,7 @@ export const VariantRules = class HiddenRules extends ChessRules {
     return moves;
   }
 
+  // Ignore randomness here: placement is always random asymmetric
   static GenRandInitFen() {
     let pieces = { w: new Array(8), b: new Array(8) };
     // Shuffle pieces + pawns on two first ranks
diff --git a/client/src/variants/Hiddenqueen.js b/client/src/variants/Hiddenqueen.js
index 4f66894b..7048d41c 100644
--- a/client/src/variants/Hiddenqueen.js
+++ b/client/src/variants/Hiddenqueen.js
@@ -175,9 +175,9 @@ export const VariantRules = class HiddenqueenRules extends ChessRules {
     return this.filterValid(this.getPotentialMovesFrom(sq));
   }
 
-  static GenRandInitFen() {
-    let fen = ChessRules.GenRandInitFen();
-    // Place hidden queens at random:
+  static GenRandInitFen(randomness) {
+    let fen = ChessRules.GenRandInitFen(randomness);
+    // Place hidden queens at random (always):
     let hiddenQueenPos = randInt(8);
     let pawnRank = "PPPPPPPP".split("");
     pawnRank[hiddenQueenPos] = "T";
diff --git a/client/src/variants/Knightmate.js b/client/src/variants/Knightmate.js
index c0e84c63..0ac7ebed 100644
--- a/client/src/variants/Knightmate.js
+++ b/client/src/variants/Knightmate.js
@@ -15,56 +15,9 @@ export const VariantRules = class KnightmateRules extends ChessRules {
     return ([V.KING, V.COMMONER].includes(b[1]) ? "Knightmate/" : "") + b;
   }
 
-  static GenRandInitFen() {
-    let pieces = { w: new Array(8), b: new Array(8) };
-    // Shuffle pieces on first and last rank
-    for (let c of ["w", "b"]) {
-      let positions = ArrayFun.range(8);
-
-      // Get random squares for bishops
-      let randIndex = 2 * randInt(4);
-      const bishop1Pos = positions[randIndex];
-      let randIndex_tmp = 2 * randInt(4) + 1;
-      const bishop2Pos = positions[randIndex_tmp];
-      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
-      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
-
-      // Get random squares for commoners
-      randIndex = randInt(6);
-      const commoner1Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-      randIndex = randInt(5);
-      const commoner2Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Get random square for queen
-      randIndex = randInt(4);
-      const queenPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Rooks and king positions are now fixed,
-      // because of the ordering rook-king-rook
-      const rook1Pos = positions[0];
-      const kingPos = positions[1];
-      const rook2Pos = positions[2];
-
-      // Finally put the shuffled pieces in the board array
-      pieces[c][rook1Pos] = "r";
-      pieces[c][commoner1Pos] = "c";
-      pieces[c][bishop1Pos] = "b";
-      pieces[c][queenPos] = "q";
-      pieces[c][kingPos] = "k";
-      pieces[c][bishop2Pos] = "b";
-      pieces[c][commoner2Pos] = "c";
-      pieces[c][rook2Pos] = "r";
-    }
-    // Add turn + flags + enpassant
-    return (
-      pieces["b"].join("") +
-      "/pppppppp/8/8/8/8/PPPPPPPP/" +
-      pieces["w"].join("").toUpperCase() +
-      " w 0 1111 -"
-    );
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness)
+      .replace(/n/g, 'c').replace(/N/g, 'C');
   }
 
   getPotentialMovesFrom([x, y]) {
diff --git a/client/src/variants/Losers.js b/client/src/variants/Losers.js
index 81d2730a..b4385bab 100644
--- a/client/src/variants/Losers.js
+++ b/client/src/variants/Losers.js
@@ -142,10 +142,19 @@ export const VariantRules = class LosersRules extends ChessRules {
     return -super.evalPosition(); //better with less material
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 -";
+
     let pieces = { w: new Array(8), b: new Array(8) };
     // Shuffle pieces on first and last rank
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(8);
 
       // Get random squares for bishops
@@ -194,7 +203,8 @@ export const VariantRules = class LosersRules extends ChessRules {
       pieces["b"].join("") +
       "/pppppppp/8/8/8/8/PPPPPPPP/" +
       pieces["w"].join("").toUpperCase() +
+      // En-passant allowed, but no flags
       " w 0 -"
-    ); //en-passant allowed, but no flags
+    );
   }
 };
diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js
index 50ccd74e..a234350b 100644
--- a/client/src/variants/Recycle.js
+++ b/client/src/variants/Recycle.js
@@ -18,8 +18,8 @@ export const VariantRules = class RecycleRules extends ChessRules {
     });
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen() + " 0000000000";
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness) + " 0000000000";
   }
 
   getFen() {
diff --git a/client/src/variants/Royalrace.js b/client/src/variants/Royalrace.js
index 6f64d24f..ff6e1e1d 100644
--- a/client/src/variants/Royalrace.js
+++ b/client/src/variants/Royalrace.js
@@ -19,10 +19,22 @@ export const VariantRules = class RoyalraceRules extends ChessRules {
     return { x: 11, y: 11 };
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "11/11/11/11/11/11/11/11/11/QRBNP1pnbrq/KRBNP1pnbrk w 0";
+
     let pieces = { w: new Array(10), b: new Array(10) };
     // Shuffle pieces on first and second rank
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = JSON.parse(JSON.stringify(pieces['w'])).reverse();
+        pieces['b'] =
+          pieces['b'].splice(5,10).reverse().concat(
+          pieces['b'].splice(0,5).reverse());
+        break;
+      }
+
       // Reserve 4 and 5 which are pawns positions
       let positions = ArrayFun.range(10).filter(i => i != 4 && i != 5);
 
diff --git a/client/src/variants/Shatranj.js b/client/src/variants/Shatranj.js
index bb00101e..ef6f65b5 100644
--- a/client/src/variants/Shatranj.js
+++ b/client/src/variants/Shatranj.js
@@ -21,8 +21,8 @@ export const VariantRules = class ShatranjRules extends ChessRules {
     ];
   }
 
-  static GenRandInitFen() {
-    return ChessRules.GenRandInitFen().replace("w 1111 -", "w");
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness).replace("w 1111 -", "w");
   }
 
   getPotentialPawnMoves([x, y]) {
diff --git a/client/src/variants/Suction.js b/client/src/variants/Suction.js
index 297eef75..0fc88ce4 100644
--- a/client/src/variants/Suction.js
+++ b/client/src/variants/Suction.js
@@ -189,9 +189,9 @@ export const VariantRules = class SuctionRules extends ChessRules {
     }
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
     // Add empty cmove:
-    return ChessRules.GenRandInitFen() + " -";
+    return ChessRules.GenRandInitFen(randomness) + " -";
   }
 
   getFen() {
diff --git a/client/src/variants/Threechecks.js b/client/src/variants/Threechecks.js
index 78aaf1f8..c1d5c5a9 100644
--- a/client/src/variants/Threechecks.js
+++ b/client/src/variants/Threechecks.js
@@ -47,10 +47,10 @@ export const VariantRules = class ThreechecksRules extends ChessRules {
     return super.getCurrentScore();
   }
 
-  static GenRandInitFen() {
-    const randFen = ChessRules.GenRandInitFen();
-    // Add check flags (at 0)
-    return randFen.replace(" w 0 1111", " w 0 111100");
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness)
+      // Add check flags (at 0)
+      .replace(" w 0 1111", " w 0 111100");
   }
 
   getFlagsFen() {
diff --git a/client/src/variants/Upsidedown.js b/client/src/variants/Upsidedown.js
index 538fb4a8..44dab7f0 100644
--- a/client/src/variants/Upsidedown.js
+++ b/client/src/variants/Upsidedown.js
@@ -20,9 +20,18 @@ export const VariantRules = class UpsidedownRules extends ChessRules {
     );
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "RNBQKBNR/PPPPPPPP/8/8/8/8/pppppppp/rnbqkbnr w 0";
+
     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;
+      }
+
       let positions = ArrayFun.range(8);
 
       let randIndex = randInt(8);
diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js
index fa89cb21..565f6275 100644
--- a/client/src/variants/Wildebeest.js
+++ b/client/src/variants/Wildebeest.js
@@ -244,9 +244,18 @@ export const VariantRules = class WildebeestRules extends ChessRules {
     return 2;
   }
 
-  static GenRandInitFen() {
+  static GenRandInitFen(randomness) {
+    if (!randomness) randomness = 2;
+    if (randomness == 0)
+      return "rnccwkqbbnr/ppppppppppp/11/11/11/11/11/11/PPPPPPPPPPP/RNBBQKWCCNR w 0 1111 -";
+
     let pieces = { w: new Array(10), b: new Array(10) };
     for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
       let positions = ArrayFun.range(11);
 
       // Get random squares for bishops + camels (different colors)
@@ -264,7 +273,7 @@ export const VariantRules = class WildebeestRules extends ChessRules {
       for (let idx of randIndexes.concat(randIndexes_tmp).sort((a, b) => {
         return b - a;
       })) {
-        //largest indices first
+        // Largest indices first
         positions.splice(idx, 1);
       }
 
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index 0a66ffc1..baf10364 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -45,7 +45,9 @@ export default {
     if (!routeFen) this.alertAndQuit("Missing FEN");
     else {
       this.gameRef.fen = routeFen.replace(/_/g, " ");
-      this.initialize();
+      // orientation is optional: taken from FEN if missing
+      const orientation = this.$route.query["side"];
+      this.initialize(orientation);
     }
   },
   methods: {
@@ -58,7 +60,7 @@ export default {
         this.$router.replace(newUrl);
       }, 500);
     },
-    initialize: async function() {
+    initialize: async function(orientation) {
       // Obtain VariantRules object
       await import("@/variants/" + this.gameRef.vname + ".js")
       .then((vModule) => {
@@ -66,17 +68,17 @@ export default {
         if (!V.CanAnalyze)
           // Late check, in case the user tried to enter URL by hand
           this.alertAndQuit("Analysis disabled for this variant");
-        else this.loadGame();
+        else this.loadGame(orientation);
       })
       .catch((err) => { this.alertAndQuit("Mispelled variant name", true); });
     },
-    loadGame: function() {
+    loadGame: function(orientation) {
       // NOTE: no need to set score (~unused)
       this.game.vname = this.gameRef.vname;
       this.game.fen = this.gameRef.fen;
       this.curFen = this.game.fen;
       this.adjustFenSize();
-      this.game.mycolor = V.ParseFen(this.gameRef.fen).turn;
+      this.game.mycolor = orientation || V.ParseFen(this.gameRef.fen).turn;
       this.$set(this.game, "fenStart", this.gameRef.fen);
     },
     // Triggered by "fenchange" emitted in BaseGame:
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 61ced0ae..469d46b2 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -54,6 +54,12 @@ main
             v-model="newchallenge.cadence"
             placeholder="5+0, 1h+30s, 5d ..."
           )
+        fieldset
+          label(for="selectRandomLevel") {{ st.tr["Randomness"] }}
+          select#selectRandomLevel(v-model="newchallenge.randomness")
+            option(value="0") {{ st.tr["Deterministic"] }}
+            option(value="1") {{ st.tr["Symmetric random"] }}
+            option(value="2") {{ st.tr["Asymmetric random"] }}
         fieldset(v-if="st.user.id > 0")
           label(for="selectPlayers") {{ st.tr["Play with?"] }}
           input#selectPlayers(
@@ -178,6 +184,7 @@ export default {
         vid: parseInt(localStorage.getItem("vid")) || 0,
         to: "", //name of challenged player (if any)
         cadence: localStorage.getItem("cadence") || "",
+        randomness: parseInt(localStorage.getItem("randomness")) || 2,
         // VariantRules object, stored to not interfere with
         // diagrams of targetted challenges:
         V: null,
@@ -541,6 +548,7 @@ export default {
               id: c.id,
               from: this.st.user.sid,
               to: c.to,
+              randomness: c.randomness,
               fen: c.fen,
               vid: c.vid,
               cadence: c.cadence,
@@ -561,6 +569,7 @@ export default {
           ) {
             let newChall = Object.assign({}, chall);
             newChall.type = this.classifyObject(chall);
+            newChall.randomness = chall.randomness;
             newChall.added = Date.now();
             let fromValues = Object.assign({}, this.people[chall.from]);
             delete fromValues["pages"]; //irrelevant in this context
@@ -719,6 +728,8 @@ export default {
       }
       // NOTE: "from" information is not required here
       let chall = Object.assign({}, this.newchallenge);
+      delete chall["V"];
+      delete chall["diag"];
       const finishAddChallenge = cid => {
         chall.id = cid || "c" + getRandString();
         // Remove old challenge if any (only one at a time of a given type):
@@ -753,6 +764,7 @@ export default {
         // Remember cadence  + vid for quicker further challenges:
         localStorage.setItem("cadence", chall.cadence);
         localStorage.setItem("vid", chall.vid);
+        localStorage.setItem("randomness", chall.randomness);
         document.getElementById("modalNewgame").checked = false;
         // Show the challenge if not on current display
         if (
@@ -841,7 +853,7 @@ export default {
       // These game informations will be shared
       let gameInfo = {
         id: getRandString(),
-        fen: c.fen || V.GenRandInitFen(),
+        fen: c.fen || V.GenRandInitFen(c.randomness),
         // White player index 0, black player index 1:
         players: c.mycolor
           ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
diff --git a/server/db/create.sql b/server/db/create.sql
index c05c87ac..e14e9a0f 100644
--- a/server/db/create.sql
+++ b/server/db/create.sql
@@ -43,6 +43,7 @@ create table Challenges (
   uid integer,
   target integer,
   vid integer,
+  randomness integer,
   fen varchar,
   cadence varchar,
   foreign key (uid) references Users(id),
diff --git a/server/models/Challenge.js b/server/models/Challenge.js
index b7c20b88..243da709 100644
--- a/server/models/Challenge.js
+++ b/server/models/Challenge.js
@@ -8,8 +8,9 @@ const UserModel = require("./User");
  *   uid: user id (int)
  *   target: recipient id (optional)
  *   vid: variant id (int)
+ *   randomness: integer in 0..2
  *   fen: varchar (optional)
- *   cadence: string (3m+2s, 7d+1d ...)
+ *   cadence: string (3m+2s, 7d ...)
  */
 
 const ChallengeModel =
@@ -19,6 +20,7 @@ const ChallengeModel =
     return (
       c.vid.toString().match(/^[0-9]+$/) &&
       c.cadence.match(/^[0-9dhms +]+$/) &&
+      c.randomness.toString().match(/^[0-2]$/) &&
       c.fen.match(/^[a-zA-Z0-9, /-]*$/) &&
       (!c.to || UserModel.checkNameEmail({name: c.to}))
     );
@@ -29,10 +31,11 @@ const ChallengeModel =
     db.serialize(function() {
       const query =
         "INSERT INTO Challenges " +
-        "(added, uid, " + (!!c.to ? "target, " : "") + "vid, fen, cadence) " +
-          "VALUES " +
-        "(" + Date.now() + "," + c.uid + "," + (!!c.to ? c.to + "," : "") +
-          c.vid + ",'" + c.fen + "','" + c.cadence + "')";
+          "(added, uid, " + (!!c.to ? "target, " : "") +
+          "vid, randomness, fen, cadence) " +
+        "VALUES " +
+          "(" + Date.now() + "," + c.uid + "," + (!!c.to ? c.to + "," : "") +
+          c.vid + "," + c.randomness + ",'" + c.fen + "','" + c.cadence + "')";
       db.run(query, function(err) {
         cb(err, {cid: this.lastID});
       });
diff --git a/server/routes/challenges.js b/server/routes/challenges.js
index efc69701..aee6fd2a 100644
--- a/server/routes/challenges.js
+++ b/server/routes/challenges.js
@@ -11,6 +11,7 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => {
     {
       fen: req.body.chall.fen,
       cadence: req.body.chall.cadence,
+      randomness: req.body.chall.randomness,
       vid: req.body.chall.vid,
       uid: req.userId,
       to: req.body.chall.to, //string: user name (may be empty)
-- 
2.44.0