Experimental in-page analyze + show rules from Game page
[vchess.git] / client / src / views / Game.vue
index 19033f5..0d7a4ee 100644 (file)
@@ -1,15 +1,35 @@
 <template lang="pug">
 main
-  input#modalInfo.modal(type="checkbox")
-  div#infoDiv(
+  input#modalRules.modal(type="checkbox")
+  div#rulesDiv(
     role="dialog"
-    data-checkbox="modalInfo"
+    data-checkbox="modalRules"
+  )
+    .card
+      label.modal-close(for="modalRules")
+      h4#variantNameInGame(@click="gotoRules") {{ game.vname }}
+      div(v-html="rulesContent")
+  input#modalScore.modal(type="checkbox")
+  div#scoreDiv(
+    role="dialog"
+    data-checkbox="modalScore"
   )
     .card.text-center
-      label.modal-close(for="modalInfo")
+      label.modal-close(for="modalScore")
+      p.score-section
+        span.score {{ game.score }}
+        | &nbsp;:&nbsp;
+        span.score-msg {{ st.tr[game.scoreMsg] }}
+  input#modalRematch.modal(type="checkbox")
+  div#rematchDiv(
+    role="dialog"
+    data-checkbox="modalRematch"
+  )
+    .card.text-center
+      label.modal-close(for="modalRematch")
       a(
         :href="'#/game/' + rematchId"
-        onClick="document.getElementById('modalInfo').checked=false"
+        onClick="document.getElementById('modalRematch').checked=false"
       )
         | {{ st.tr["Rematch in progress"] }}
   input#modalChat.modal(
@@ -56,8 +76,8 @@ main
         button.refuseBtn(@click="cancelMove()")
           span {{ st.tr["Cancel"] }}
   .row
-    #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
-      span.variant-cadence {{ game.cadence }}
+    #aboveBoard.col-sm-12
+      span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }}
       span.variant-name {{ game.vname }}
       span#nextGame(
         v-if="nextIds.length > 0"
@@ -96,7 +116,7 @@ main
       )
         img(src="/images/icons/rematch.svg")
       #playersInfo
-        p
+        p(v-if="isLargeScreen()")
           span.name(:class="{connected: isConnected(0)}")
             | {{ game.players[0].name || "@nonymous" }}
           span.time(
@@ -118,6 +138,30 @@ main
             span.time-separator(v-if="!!virtualClocks[1][1]") :
             span.time-right(v-if="!!virtualClocks[1][1]")
               | {{ virtualClocks[1][1] }}
+        p(v-else)
+          span.name(:class="{connected: isConnected(0)}")
+            | {{ game.players[0].name || "@nonymous" }}
+          span.split-names -
+          span.name(:class="{connected: isConnected(1)}")
+            | {{ game.players[1].name || "@nonymous" }}
+          br
+          span.time(
+            v-if="game.score=='*'"
+            :class="{yourturn: !!vr && vr.turn == 'w'}"
+          )
+            span.time-left {{ virtualClocks[0][0] }}
+            span.time-separator(v-if="!!virtualClocks[0][1]") :
+            span.time-right(v-if="!!virtualClocks[0][1]")
+              | {{ virtualClocks[0][1] }}
+          span.separator
+          span.time(
+            v-if="game.score=='*'"
+            :class="{yourturn: !!vr && vr.turn == 'b'}"
+          )
+            span.time-left {{ virtualClocks[1][0] }}
+            span.time-separator(v-if="!!virtualClocks[1][1]") :
+            span.time-right(v-if="!!virtualClocks[1][1]")
+              | {{ virtualClocks[1][1] }}
   BaseGame(
     ref="basegame"
     :game="game"
@@ -130,6 +174,7 @@ import BaseGame from "@/components/BaseGame.vue";
 import Chat from "@/components/Chat.vue";
 import { store } from "@/store";
 import { GameStorage } from "@/utils/gameStorage";
+import { ImportgameStorage } from "@/utils/importgameStorage";
 import { ppt } from "@/utils/datetime";
 import { notify } from "@/utils/notifications";
 import { ajax } from "@/utils/ajax";
@@ -137,7 +182,7 @@ import { extractTime } from "@/utils/timeControl";
 import { getRandString } from "@/utils/alea";
 import { getScoreMessage } from "@/utils/scoring";
 import { getFullNotation } from "@/utils/notation";
-import { getDiagram } from "@/utils/printDiagram";
+import { getDiagram, replaceByDiag } from "@/utils/printDiagram";
 import { processModalClick } from "@/utils/modalClick";
 import { playMove, getFilteredMove } from "@/utils/playUndo";
 import { ArrayFun } from "@/utils/array";
@@ -159,6 +204,7 @@ export default {
       // virtualClocks will be initialized from true game.clocks
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
+      rulesContent: "",
       drawOffer: "",
       rematchId: "",
       rematchOffer: "",
@@ -186,8 +232,7 @@ export default {
       retrySendmove: null,
       clockUpdate: null,
       // Related to (killing of) self multi-connects:
-      newConnect: {},
-      killed: {}
+      newConnect: {}
     };
   },
   watch: {
@@ -213,10 +258,18 @@ export default {
     this.atCreation();
   },
   mounted: function() {
-    ["chatWrap", "infoDiv"].forEach(eltName => {
-      document.getElementById(eltName)
-        .addEventListener("click", processModalClick);
-    });
+    document.getElementById("chatWrap")
+      .addEventListener("click", (e) => {
+        processModalClick(e, () => {
+          this.toggleChat("close")
+        });
+      });
+    ["rulesDiv", "rematchDiv", "scoreDiv"].forEach(
+      (eltName) => {
+        document.getElementById(eltName)
+          .addEventListener("click", processModalClick);
+      }
+    );
     if ("ontouchstart" in window) {
       // Disable tooltips on smartphones:
       document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
@@ -262,6 +315,12 @@ export default {
       }
       this.send("losefocus");
     },
+    isLargeScreen: function() {
+      return window.innerWidth >= 500;
+    },
+    gotoRules: function() {
+      this.$router.push("/variants/" + this.game.vname);
+    },
     participateInChat: function(p) {
       return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
     },
@@ -303,6 +362,7 @@ export default {
       if (!!chatComp) chatComp.chats = [];
       this.virtualClocks = [[0,0], [0,0]];
       this.vr = null;
+      this.rulesContent = "";
       this.drawOffer = "";
       this.lastateAsked = false;
       this.rematchOffer = "";
@@ -317,7 +377,6 @@ export default {
       this.retrySendmove = null;
       this.clockUpdate = null;
       this.newConnect = {};
-      this.killed = {};
       // 1] Initialize connection
       this.connexionString =
         params.socketUrl +
@@ -416,22 +475,33 @@ export default {
       if (!!oppsid && !!this.people[oppsid]) return oppsid;
       return null;
     },
-    toggleChat: function() {
-      if (document.getElementById("modalChat").checked)
+    // NOTE: action if provided is always a closing action
+    toggleChat: function(action) {
+      if (!action && document.getElementById("modalChat").checked)
         // Entering chat
         document.getElementById("inputChat").focus();
-      // TODO: next line is only required when exiting chat,
-      // but the event for now isn't well detected.
-      document.getElementById("chatBtn").classList.remove("somethingnew");
+      else {
+        document.getElementById("chatBtn").classList.remove("somethingnew");
+        if (!!this.game.mycolor) {
+          // Update "chatRead" variable either on server or locally
+          if (this.game.type == "corr")
+            this.updateCorrGame({ chatRead: this.game.mycolor });
+          else if (this.game.type == "live")
+            GameStorage.update(this.gameRef, { chatRead: true });
+        }
+      }
     },
     processChat: function(chat) {
       this.send("newchat", { data: chat });
       // NOTE: anonymous chats in corr games are not stored on server (TODO?)
-      if (this.game.type == "corr" && this.st.user.id > 0)
-        this.updateCorrGame({ chat: chat });
-      else if (this.game.type == "live") {
-        chat.added = Date.now();
-        GameStorage.update(this.gameRef, { chat: chat });
+      if (!!this.game.mycolor) {
+        if (this.game.type == "corr")
+          this.updateCorrGame({ chat: chat });
+        else {
+          // Live game
+          chat.added = Date.now();
+          GameStorage.update(this.gameRef, { chat: chat });
+        }
       }
     },
     clearChat: function() {
@@ -450,6 +520,7 @@ export default {
       }
     },
     getGameType: function(game) {
+      if (!!game.id.toString().match(/^i/)) return "import";
       return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
     },
     // Notify something after a new move (to opponent and me on MyGames page)
@@ -504,7 +575,8 @@ export default {
             if (sid != this.st.user.sid) {
               this.send("askidentity", { target: sid });
               this.people[sid] = { tmpIds: data.sockIds[sid] };
-            } else {
+            }
+            else {
               // Complete my tmpIds:
               Object.assign(this.people[sid].tmpIds, data.sockIds[sid]);
             }
@@ -522,7 +594,8 @@ export default {
                 }
               }
             );
-            this.newConnect[data.from] = true; //for self multi-connects tests
+            // For self multi-connects tests:
+            this.newConnect[data.from[0]] = true;
             this.send("askidentity", { target: data.from[0] });
           } else {
             this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true };
@@ -552,13 +625,6 @@ export default {
           }
           break;
         }
-        case "killed":
-          // I logged in elsewhere:
-          this.conn.removeEventListener("message", this.socketMessageListener);
-          this.conn.removeEventListener("close", this.socketCloseListener);
-          this.conn = null;
-          alert(this.st.tr["New connexion detected: tab now offline"]);
-          break;
         case "askidentity": {
           // Request for identification
           const me = {
@@ -576,60 +642,77 @@ export default {
           // player.tmpIds is already set
           player.name = user.name;
           player.id = user.id;
+          if (this.game.type == "live") {
+            const myGidx =
+              this.game.players.findIndex(p => p.sid == this.st.user.sid);
+            // Sometimes a player name isn't stored yet (TODO: why?)
+            if (
+              myGidx >= 0 &&
+              !this.game.players[1 - myGidx].name &&
+              this.game.players[1 - myGidx].sid == user.sid &&
+              !!user.name
+            ) {
+              this.game.players[1-myGidx].name = user.name;
+              GameStorage.update(
+                this.gameRef,
+                { playerName: { idx: 1 - myGidx, name: user.name } }
+              );
+            }
+          }
           this.$forceUpdate(); //TODO: shouldn't be required
           // If I multi-connect, kill current connexion if no mark (I'm older)
           if (this.newConnect[user.sid]) {
+            delete this.newConnect[user.sid];
             if (
               user.id > 0 &&
               user.id == this.st.user.id &&
-              user.sid != this.st.user.sid &&
-              !this.killed[this.st.user.sid]
+              user.sid != this.st.user.sid
             ) {
-                this.send("killme", { sid: this.st.user.sid });
-                this.killed[this.st.user.sid] = true;
+              this.cleanBeforeDestroy();
+              alert(this.st.tr["New connexion detected: tab now offline"]);
+              break;
             }
-            delete this.newConnect[user.sid];
           }
-          if (!this.killed[this.st.user.sid]) {
-            // Ask potentially missed last state, if opponent and I play
-            if (
-              !this.gotLastate &&
-              !!this.game.mycolor &&
-              this.game.type == "live" &&
-              this.game.score == "*" &&
-              this.game.players.some(p => p.sid == user.sid)
-            ) {
-              this.send("asklastate", { target: user.sid });
-              let counter = 1;
-              this.askLastate = setInterval(
-                () => {
-                  // Ask at most 3 times:
-                  // if no reply after that there should be a network issue.
-                  if (
-                    counter < 3 &&
-                    !this.gotLastate &&
-                    !!this.people[user.sid]
-                  ) {
-                    this.send("asklastate", { target: user.sid });
-                    counter++;
-                  } else {
-                    clearInterval(this.askLastate);
-                  }
-                },
-                1500
-              );
-            }
+          // Ask potentially missed last state, if opponent and I play
+          if (
+            !this.gotLastate &&
+            !!this.game.mycolor &&
+            this.game.type == "live" &&
+            this.game.score == "*" &&
+            this.game.players.some(p => p.sid == user.sid)
+          ) {
+            this.send("asklastate", { target: user.sid });
+            let counter = 1;
+            this.askLastate = setInterval(
+              () => {
+                // Ask at most 3 times:
+                // if no reply after that there should be a network issue.
+                if (
+                  counter < 3 &&
+                  !this.gotLastate &&
+                  !!this.people[user.sid]
+                ) {
+                  this.send("asklastate", { target: user.sid });
+                  counter++;
+                } else {
+                  clearInterval(this.askLastate);
+                }
+              },
+              1500
+            );
           }
           break;
         }
         case "askgame":
-          // Send current (live) game if not asked by any of the players
+          // Send current (live or import) game,
+          // if not asked by any of the players
           if (
-            this.game.type == "live" &&
+            this.game.type != "corr" &&
             this.game.players.every(p => p.sid != data.from[0])
           ) {
             const myGame = {
               id: this.game.id,
+              // FEN is current position, unused for now
               fen: this.game.fen,
               players: this.game.players,
               vid: this.game.vid,
@@ -767,7 +850,7 @@ export default {
         case "drawoffer":
           // NOTE: observers don't know who offered draw
           this.drawOffer = "received";
-          if (this.game.type == "live") {
+          if (!!this.game.mycolor && this.game.type == "live") {
             GameStorage.update(
               this.gameRef,
               { drawOffer: V.GetOppCol(this.game.mycolor) }
@@ -777,7 +860,7 @@ export default {
         case "rematchoffer":
           // NOTE: observers don't know who offered rematch
           this.rematchOffer = data.data ? "received" : "";
-          if (this.game.type == "live") {
+          if (!!this.game.mycolor && this.game.type == "live") {
             GameStorage.update(
               this.gameRef,
               { rematchOffer: V.GetOppCol(this.game.mycolor) }
@@ -800,7 +883,7 @@ export default {
             this.$router.push("/game/" + gameInfo.id);
           } else {
             this.rematchId = gameInfo.id;
-            document.getElementById("modalInfo").checked = true;
+            document.getElementById("modalRematch").checked = true;
           }
           break;
         }
@@ -809,7 +892,8 @@ export default {
           this.$refs["chatcomp"].newChat(chat);
           if (this.game.type == "live") {
             chat.added = Date.now();
-            GameStorage.update(this.gameRef, { chat: chat });
+            if (!!this.game.mycolor)
+              GameStorage.update(this.gameRef, { chat: chat });
           }
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
@@ -876,7 +960,7 @@ export default {
       }
     },
     clickDraw: function() {
-      if (!this.game.mycolor) return; //I'm just spectator
+      if (!this.game.mycolor || this.game.type == "import") return;
       if (["received", "threerep"].includes(this.drawOffer)) {
         if (!confirm(this.st.tr["Accept draw?"])) return;
         const message =
@@ -928,7 +1012,7 @@ export default {
       });
     },
     clickRematch: function() {
-      if (!this.game.mycolor) return; //I'm just spectator
+      if (!this.game.mycolor || this.game.type == "import") return;
       if (this.rematchOffer == "received") {
         // Start a new game!
         let gameInfo = {
@@ -1001,19 +1085,29 @@ export default {
       this.gameOver(score, side + " surrender");
     },
     loadGame: function(game, callback) {
-      this.vr = new V(game.fen);
-      const gtype = this.getGameType(game);
+      const gtype = game.type || this.getGameType(game);
       const tc = extractTime(game.cadence);
       const myIdx = game.players.findIndex(p => {
         return p.sid == this.st.user.sid || p.id == this.st.user.id;
       });
+      // Sometimes the name isn't stored yet (TODO: why?)
+      if (
+        myIdx >= 0 &&
+        gtype == "live" &&
+        !game.players[myIdx].name &&
+        !!this.st.user.name
+      ) {
+        game.players[myIdx].name = this.st.user.name;
+        GameStorage.update(
+          game.id,
+          { playerName: { idx: myIdx, name: this.st.user.name } }
+        );
+      }
       // "mycolor" is undefined for observers
       const mycolor = [undefined, "w", "b"][myIdx + 1];
-      // Live games before 26/03/2020 don't have chat history:
-      if (!game.chats) game.chats = []; //TODO: remove line
-      // Sort chat messages from newest to oldest
-      game.chats.sort((c1, c2) => c2.added - c1.added);
       if (gtype == "corr") {
+        if (mycolor == 'w') game.chatRead = game.chatReadWhite;
+        else if (mycolor == 'b') game.chatRead = game.chatReadBlack;
         // NOTE: clocks in seconds
         game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
         game.clocks = [tc.mainTime, tc.mainTime];
@@ -1025,41 +1119,19 @@ export default {
               (Date.now() - game.moves[L-1].played) / 1000;
           }
         }
-        if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
-          // Did a chat message arrive after my last move?
-          let dtLastMove = 0;
-          if (L == 1 && myIdx == 0)
-            dtLastMove = game.moves[0].played;
-          else if (L >= 2) {
-            if (L % 2 == 0) {
-              // It's now white turn
-              dtLastMove = game.moves[L-1-(1-myIdx)].played;
-            } else {
-              // Black turn:
-              dtLastMove = game.moves[L-1-myIdx].played;
-            }
-          }
-          if (dtLastMove < game.chats[0].added)
-            document.getElementById("chatBtn").classList.add("somethingnew");
-        }
         // Now that we used idx and played, re-format moves as for live games
         game.moves = game.moves.map(m => m.squares);
       }
-      if (gtype == "live") {
-        if (
-          game.chats.length > 0 &&
-          (!game.initime || game.initime < game.chats[0].added)
-        ) {
-          document.getElementById("chatBtn").classList.add("somethingnew");
-        }
+      else if (gtype == "live") {
         if (game.clocks[0] < 0) {
           // Game is unstarted. clock is ignored until move 2
           game.clocks = [tc.mainTime, tc.mainTime];
           if (myIdx >= 0) {
             // I play in this live game
-            GameStorage.update(game.id, {
-              clocks: game.clocks
-            });
+            GameStorage.update(
+              game.id,
+              { clocks: game.clocks }
+            );
           }
         } else {
           if (!!game.initime)
@@ -1067,6 +1139,21 @@ export default {
             game.clocks[myIdx] -= (Date.now() - game.initime) / 1000;
         }
       }
+      else
+        // gtype == "import"
+        game.clocks = [tc.mainTime, tc.mainTime];
+      // Live games before 26/03/2020 don't have chat history:
+      if (!game.chats) game.chats = []; //TODO: remove line
+      // Sort chat messages from newest to oldest
+      game.chats.sort((c1, c2) => c2.added - c1.added);
+      if (
+        myIdx >= 0 &&
+        game.chats.length > 0 &&
+        (!game.chatRead || game.chatRead < game.chats[0].added)
+      ) {
+        // A chat message arrived since my last reading:
+        document.getElementById("chatBtn").classList.add("somethingnew");
+      }
       // TODO: merge next 2 "if" conditions
       if (!!game.drawOffer) {
         if (game.drawOffer == "t")
@@ -1100,15 +1187,17 @@ export default {
       }
       this.repeat = {}; //reset: scan past moves' FEN:
       let repIdx = 0;
-      let vr_tmp = new V(game.fenStart);
+      this.vr = new V(game.fenStart);
       let curTurn = "n";
       game.moves.forEach(m => {
-        playMove(m, vr_tmp);
-        const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
+        playMove(m, this.vr);
+        const fenIdx = this.vr.getFenForRepeat();
         this.repeat[fenIdx] = this.repeat[fenIdx]
           ? this.repeat[fenIdx] + 1
           : 1;
       });
+      // Imported games don't have current FEN
+      if (!game.fen) game.fen = this.vr.getFen();
       if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
       this.game = Object.assign(
         // NOTE: assign mycolor here, since BaseGame could also be VS computer
@@ -1154,6 +1243,19 @@ export default {
       await import("@/variants/" + game.vname + ".js")
       .then((vModule) => {
         window.V = vModule[game.vname + "Rules"];
+        // (AJAX) Request to get rules content (plain text, HTML)
+        this.rulesContent =
+          require(
+            "raw-loader!@/translations/rules/" +
+            game.vname + "/" +
+            this.st.lang + ".pug"
+          )
+          // Next two lines fix a weird issue after last update (2019-11)
+          .replace(/\\n/g, " ")
+          .replace(/\\"/g, '"')
+          .replace('module.exports = "', "")
+          .replace(/"$/, "")
+          .replace(/(fen:)([^:]*):/g, replaceByDiag);
         this.loadGame(game, callback);
       });
     },
@@ -1177,8 +1279,12 @@ export default {
             }
           }
         );
-      } else
-        // Local game (or live remote)
+      }
+      else if (!!this.gameRef.match(/^i/))
+        // Game import (maybe remote)
+        ImportgameStorage.get(this.gameRef, callback);
+      else
+        // Local live game (or remote)
         GameStorage.get(this.gameRef, callback);
     },
     re_setClocks: function() {
@@ -1217,6 +1323,9 @@ export default {
     },
     // Update variables and storage after a move:
     processMove: function(move, data) {
+      if (this.game.type == "import")
+        // Shouldn't receive any messages in this mode:
+        return;
       if (!data) data = {};
       const moveCol = this.vr.turn;
       const colorIdx = ["w", "b"].indexOf(moveCol);
@@ -1417,9 +1526,12 @@ export default {
           }
         );
         // PlayOnBoard is enough, and more appropriate for Synchrone Chess
-        V.PlayOnBoard(this.vr.board, move);
+        const arMove = (Array.isArray(move) ? move : [move]);
+        for (let i = 0; i < arMove.length; i++)
+          V.PlayOnBoard(this.vr.board, arMove[i]);
         const position = this.vr.getBaseFen();
-        V.UndoOnBoard(this.vr.board, move);
+        for (let i = arMove.length - 1; i >= 0; i--)
+          V.UndoOnBoard(this.vr.board, arMove[i]);
         if (["all","byrow"].includes(V.ShowMoves)) {
           this.curDiag = getDiagram({
             position: position,
@@ -1454,6 +1566,8 @@ export default {
       this.game.score = score;
       if (!scoreMsg) scoreMsg = getScoreMessage(score);
       this.game.scoreMsg = scoreMsg;
+      // Display result in a un-missable way:
+      document.getElementById("modalScore").checked = true;
       this.$set(this.game, "scoreMsg", scoreMsg);
       const myIdx = this.game.players.findIndex(p => {
         return p.sid == this.st.user.sid || p.id == this.st.user.id;
@@ -1495,10 +1609,24 @@ export default {
 </script>
 
 <style lang="sass" scoped>
-#infoDiv > .card
+#scoreDiv > .card, #rematchDiv > .card
   padding: 15px 0
   max-width: 430px
 
+#rulesDiv > .card
+  padding: 5px 0
+  max-width: 75%
+  max-height: 100%
+  @media screen and (max-width: 1024px)
+    max-width: 85%
+  @media screen and (max-width: 767px)
+    max-width: 100%
+
+p.score-section
+  font-size: 1.3em
+  span.score
+    font-weight: bold
+
 .connected
   background-color: lightgreen
 
@@ -1512,9 +1640,6 @@ export default {
 #playersInfo > p
   margin: 0
 
-@media screen and (min-width: 768px)
-  #actions
-    width: 300px
 @media screen and (max-width: 767px)
   .game
     width: 100%
@@ -1533,12 +1658,8 @@ button
     @media screen and (max-width: 767px)
       height: 18px
 
-@media screen and (max-width: 767px)
-  #aboveBoard
-    text-align: center
-@media screen and (min-width: 768px)
-  #aboveBoard
-    margin-left: 30%
+#aboveBoard
+  text-align: center
 
 .variant-cadence
   padding-right: 10px
@@ -1553,6 +1674,12 @@ span#nextGame
   display: inline-block
   margin-right: 10px
 
+span.separator
+  display: inline-block
+  margin: 0
+  padding: 0
+  width: 10px
+
 span.name
   font-size: 1.5rem
   padding: 0 3px
@@ -1622,4 +1749,91 @@ button.acceptBtn
   background-color: lightgreen
 button.refuseBtn
   background-color: red
+
+h4#variantNameInGame
+  cursor: pointer
+  text-align: center
+  text-decoration: underline
+  font-weight: bold
+</style>
+
+<style lang="sass">
+// TODO: next is duplicated from Rules/. Merge ? How ? ...
+
+figure.diagram-container
+  margin: 15px 0 15px 0
+  text-align: center
+  width: 100%
+  display: block
+  .diagram
+    display: block
+    width: 50%
+    min-width: 240px
+    margin-left: auto
+    margin-right: auto
+  .diag12
+    float: left
+    width: 40%
+    margin-left: calc(10% - 20px)
+    margin-right: 40px
+    @media screen and (max-width: 630px)
+      float: none
+      margin: 0 auto 10px auto
+  .diag22
+    float: left
+    width: 40%
+    margin-right: calc(10% - 20px)
+    @media screen and (max-width: 630px)
+      float: none
+      margin: 0 auto
+  figcaption
+    display: block
+    clear: both
+    padding-top: 5px
+    font-size: 0.8em
+
+p.boxed
+  background-color: #FFCC66
+  padding: 5px
+
+.bigfont
+  font-size: 1.2em
+
+.bold
+  font-weight: bold
+
+.stageDelimiter
+  color: purple
+
+// To show (new) pieces, and/or there values...
+figure.showPieces > img
+  width: 50px
+
+figure.showPieces > figcaption
+  color: #6C6C6C
+
+.section-title
+  padding: 0
+
+.section-title > h4
+  padding: 5px
+
+ol, ul:not(.browser-default)
+  padding-left: 20px
+
+ul:not(.browser-default)
+  margin-top: 5px
+
+ul:not(.browser-default) > li
+  list-style-type: disc
+
+table
+  margin: 15px auto
+
+.italic
+  font-style: italic
+
+img.img-center
+  display: block
+  margin: 0 auto 15px auto
 </style>