From 07052665845283c65b50a76537669d0602ba436b Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 6 Apr 2020 11:35:32 +0200
Subject: [PATCH] Experimental in-page analyze + show rules from Game page

---
 TODO                                       |  11 --
 client/src/App.vue                         |   9 +-
 client/src/components/BaseGame.vue         |  66 ++++++---
 client/src/components/ComputerGame.vue     |   1 +
 client/src/components/MoveList.vue         |   4 +-
 client/src/styles/TODO                     |   3 +
 client/src/translations/rules/Arena/es.pug |   2 +-
 client/src/utils/printDiagram.js           |  11 ++
 client/src/views/Analyse.vue               |   4 +-
 client/src/views/Game.vue                  | 147 +++++++++++++++++++--
 client/src/views/Rules.vue                 |  24 +---
 11 files changed, 211 insertions(+), 71 deletions(-)
 create mode 100644 client/src/styles/TODO

diff --git a/TODO b/TODO
index a4ffcfd7..219aa835 100644
--- a/TODO
+++ b/TODO
@@ -1,16 +1,5 @@
 Chakart :)
 
-// mode analyse + charger rules dans page modal
-
-+ bouton analyse en vert !
-// TODO: rules button in Game Page :: modal just show rules text, easy
-// // TODO: analyse mode in Game Page :: just stay, pass in analyze, easy
-// // --> indicateur isConnected: vérifie aussi que pas en mode analyse ?! non...
-// // Attention si coup reçu pendant mode analyse faut d'abord sortir du mode
-// // (qu'on soit joueur ou spectateur)
-
-Ball --> capture ballon prend à distance ?! bof, si on a ballon et capture ennemi : lui passe la balle ?
-
 Ambiguous chess
 https://www.chessvariants.com/mvopponent.dir/ambiguous-chess.html
 Need special highlight square --> similar to enlightened for Dark, in Board.vue
diff --git a/client/src/App.vue b/client/src/App.vue
index def45262..eef7169f 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -116,6 +116,9 @@ body
 .row > div
   padding: 0
 
+a
+  text-decoration: underline
+
 header
   width: 100%
   display: flex
@@ -185,6 +188,7 @@ nav
         justify-content: flex-start
         & > a
           display: inline-block
+          text-decoration: none
           color: #2c3e50
           &.router-link-exact-active
             color: #42b983
@@ -200,10 +204,6 @@ nav
       & > #leftMenu
         margin-top: 42px
         padding-bottom: 5px
-        & > a
-          color: #2c3e50
-          &.router-link-exact-active
-            color: #42b983
       & > #rightMenu
         padding-top: 5px
         border-top: 1px solid darkgrey
@@ -266,6 +266,7 @@ footer
     align-self: center;
     &:link
       color: #2c3e50
+      text-decoration: none
     &:visited, &:hover
       color: #2c3e50
       text-decoration: none
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 572c80ad..a0bcef0a 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -14,7 +14,7 @@ div#baseGame
         ref="board"
         :vr="vr"
         :last-move="lastMove"
-        :analyze="game.mode=='analyze'"
+        :analyze="mode=='analyze'"
         :score="game.score"
         :user-color="game.mycolor"
         :orientation="orientation"
@@ -40,6 +40,7 @@ div#baseGame
           img.inline(src="/images/icons/play.svg")
         button(@click="gotoEnd()")
           img.inline(src="/images/icons/fast-forward.svg")
+      p(v-show="showFen") {{ (!!vr ? vr.getFen() : "") }}
     #movesList
       MoveList(
         :show="showMoves"
@@ -52,7 +53,7 @@ div#baseGame
         :cursor="cursor"
         @download="download"
         @showrules="showRules"
-        @analyze="analyzePosition"
+        @analyze="toggleAnalyze"
         @goto-move="gotoMove"
         @reset-arrows="resetArrows"
       )
@@ -84,6 +85,7 @@ export default {
       vr: null, //VariantRules object, game state
       endgameMessage: "",
       orientation: "w",
+      mode: "",
       score: "*", //'*' means 'unfinished'
       moves: [],
       cursor: -1, //index of the move just played
@@ -111,6 +113,12 @@ export default {
           : ""
       );
     },
+    showFen: function() {
+      return (
+        this.mode == "analyze" &&
+        this.$router.currentRoute.path.indexOf("/analyse") === -1
+      );
+    },
     // TODO: is it OK to pass "computed" as properties?
     // Also, some are seemingly not recomputed when vr is initialized.
     showMoves: function() {
@@ -192,14 +200,15 @@ export default {
       this.$refs["board"].cancelResetArrows();
     },
     showRules: function() {
-      //this.$router.push("/variants/" + this.game.vname);
-      window.open("#/variants/" + this.game.vname, "_blank"); //better
+      // The button is here only on Game page:
+      document.getElementById("modalRules").checked = true;
     },
     re_setVariables: function(game) {
       if (!game) game = this.game; //in case of...
       this.endgameMessage = "";
       // "w": default orientation for observed games
       this.orientation = game.mycolor || "w";
+      this.mode = game.mode || game.type; //TODO: merge...
       this.moves = JSON.parse(JSON.stringify(game.moves || []));
       // Post-processing: decorate each move with notation and FEN
       this.vr = new V(game.fenStart);
@@ -217,6 +226,7 @@ export default {
           this.vr.play(m);
           const checkSquares = this.vr.getCheckSquares();
           if (checkSquares.length > 0) m.notation += "+";
+          if (idxM == Lm - 1) m.fen = this.vr.getFen();
           if (idx == L - 1 && idxM == Lm - 1) {
             this.incheck = checkSquares;
             const score = this.vr.getCurrentScore();
@@ -243,14 +253,29 @@ export default {
       if (index >= 0) this.lastMove = this.moves[index];
       else this.lastMove = null;
     },
-    analyzePosition: function() {
-      let newUrl =
-        "/analyse/" +
-        this.game.vname +
-        "/?fen=" +
-        this.vr.getFen().replace(/ /g, "_");
-      if (!!this.game.mycolor) newUrl += "&side=" + this.game.mycolor;
-      window.open("#" + newUrl);
+    toggleAnalyze: function() {
+      if (this.mode != "analyze") {
+        // Enter analyze mode:
+        this.gameMode = this.mode; //was not 'analyze'
+        this.mode = "analyze";
+        this.gameCursor = this.cursor;
+        this.gameMoves = JSON.parse(JSON.stringify(this.moves));
+        document.getElementById("analyzeBtn").classList.add("active");
+      }
+      else {
+        // Exit analyze mode:
+        this.mode = this.gameMode ;
+        this.cursor = this.gameCursor;
+        this.moves = this.gameMoves;
+        let fen = this.game.fenStart;
+        if (this.cursor >= 0) {
+          let mv = this.moves[this.cursor];
+          if (!Array.isArray(mv)) mv = [mv];
+          fen = mv[mv.length-1].fen;
+        }
+        this.vr = new V(fen);
+        document.getElementById("analyzeBtn").classList.remove("active");
+      }
     },
     download: function() {
       const content = this.getPgn();
@@ -407,7 +432,7 @@ export default {
         smove.notation = this.vr.getNotation(smove);
         smove.unambiguous = V.GetUnambiguousNotation(smove);
         this.vr.play(smove);
-        if (!!this.lastMove) {
+        if (this.inMultimove && !!this.lastMove) {
           if (!Array.isArray(this.lastMove))
             this.lastMove = [this.lastMove, smove];
           else this.lastMove.push(smove);
@@ -472,7 +497,7 @@ export default {
             else this.lastMove.notation += "#";
           }
         }
-        if (score != "*" && this.game.mode == "analyze") {
+        if (score != "*" && this.mode == "analyze") {
           const message = getScoreMessage(score);
           // Just show score on screen (allow undo)
           this.showEndgameMsg(score + " . " + this.st.tr[message]);
@@ -488,7 +513,7 @@ export default {
           this.emitFenIfAnalyze();
           this.inMultimove = false;
           this.score = computeScore();
-          if (this.game.mode != "analyze" && !navigate) {
+          if (this.mode != "analyze" && !navigate) {
             if (!noemit) {
               // Post-processing (e.g. computer play).
               const L = this.moves.length;
@@ -526,16 +551,19 @@ export default {
       // Forbid playing outside analyze mode, except if move is received.
       // Sufficient condition because Board already knows which turn it is.
       if (
-        this.game.mode != "analyze" &&
+        this.mode != "analyze" &&
         !navigate &&
         !received &&
         (this.game.score != "*" || this.cursor < this.moves.length - 1)
       ) {
         return;
       }
-      // To play a received move, cursor must be at the end of the game:
-      if (received && this.cursor < this.moves.length - 1)
-        this.gotoEnd();
+      if (!!received) {
+        if (this.mode == "analyze") this.toggleAnalyze();
+        if (this.cursor < this.moves.length - 1)
+          // To play a received move, cursor must be at the end of the game:
+          this.gotoEnd();
+      }
       playMove();
     },
     cancelCurrentMultimove: function() {
diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue
index 1cd37ab0..44bfb6a6 100644
--- a/client/src/components/ComputerGame.vue
+++ b/client/src/components/ComputerGame.vue
@@ -71,6 +71,7 @@ export default {
       game.players = [{ name: "Myself" }, { name: "Computer" }];
       if (game.mycolor == "b") game.players = game.players.reverse();
       game.score = "*"; //finished games are removed
+      game.mode = this.gameInfo.mode;
       this.currentUrl = document.location.href; //to avoid playing outside page
       this.game = game;
       this.$refs["basegame"].re_setVariables(game);
diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue
index 6c999755..ccb07758 100644
--- a/client/src/components/MoveList.vue
+++ b/client/src/components/MoveList.vue
@@ -27,7 +27,7 @@ div
       :aria-label="st.tr['Resize board']"
     )
       img.inline(src="/images/icons/resize.svg")
-    button.tooltip(
+    button#analyzeBtn.tooltip(
       v-if="canAnalyze"
       @click="$emit('analyze')"
       :aria-label="st.tr['Analyse']"
@@ -240,6 +240,8 @@ span#rulesBtn
 
 button
   margin: 0
+  &.active
+    background-color: #50E99A
 
 #aboveMoves button
   padding-bottom: 5px
diff --git a/client/src/styles/TODO b/client/src/styles/TODO
new file mode 100644
index 00000000..e3f897ee
--- /dev/null
+++ b/client/src/styles/TODO
@@ -0,0 +1,3 @@
+@import "./styles/_variables.scss";
+https://css-tricks.com/how-to-import-a-sass-file-into-every-vue-component-in-an-app/
+--> Stop duplicating CSS
diff --git a/client/src/translations/rules/Arena/es.pug b/client/src/translations/rules/Arena/es.pug
index 67366aa3..999f861d 100644
--- a/client/src/translations/rules/Arena/es.pug
+++ b/client/src/translations/rules/Arena/es.pug
@@ -39,7 +39,7 @@ p.
 h3 Fuente
 
 p
-  | La
+  | La 
   a(href="https://www.chessvariants.com/32turn.dir/arenachess.html")
     | variante Arena
   | &nbsp;en chessvariants.com.
diff --git a/client/src/utils/printDiagram.js b/client/src/utils/printDiagram.js
index 9ebdeed6..00ff2c79 100644
--- a/client/src/utils/printDiagram.js
+++ b/client/src/utils/printDiagram.js
@@ -100,3 +100,14 @@ export function getDiagram(args) {
   }
   return boardDiv;
 }
+
+// Method to replace diagrams in loaded HTML
+export function replaceByDiag(match, p1, p2) {
+  const diagParts = p2.split(" ");
+  return getDiagram({
+    position: diagParts[0],
+    marks: diagParts[1],
+    orientation: diagParts[2],
+    shadow: diagParts[3]
+  });
+}
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index 2f087b99..39022e8a 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -64,7 +64,9 @@ export default {
       if (!routeFen) this.alertAndQuit("Missing FEN");
       else {
         this.gameRef.fen = routeFen.replace(/_/g, " ");
-        // orientation is optional: taken from FEN if missing
+        // orientation is optional: taken from FEN if missing.
+        // NOTE: currently no internal usage of 'side', but could be used by
+        // manually settings the URL (TODO?).
         const orientation = this.$route.query["side"];
         this.initialize(orientation);
       }
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index fe487653..0d7a4ee9 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -1,5 +1,14 @@
 <template lang="pug">
 main
+  input#modalRules.modal(type="checkbox")
+  div#rulesDiv(
+    role="dialog"
+    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"
@@ -67,7 +76,7 @@ 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
+    #aboveBoard.col-sm-12
       span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }}
       span.variant-name {{ game.vname }}
       span#nextGame(
@@ -144,6 +153,7 @@ main
             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'}"
@@ -172,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";
@@ -194,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: "",
@@ -253,7 +264,7 @@ export default {
           this.toggleChat("close")
         });
       });
-    ["rematchDiv", "scoreDiv"].forEach(
+    ["rulesDiv", "rematchDiv", "scoreDiv"].forEach(
       (eltName) => {
         document.getElementById(eltName)
           .addEventListener("click", processModalClick);
@@ -307,6 +318,9 @@ export default {
     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;
     },
@@ -348,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 = "";
@@ -1228,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);
       });
     },
@@ -1585,6 +1613,15 @@ export default {
   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
@@ -1603,9 +1640,6 @@ p.score-section
 #playersInfo > p
   margin: 0
 
-@media screen and (min-width: 768px)
-  #actions
-    width: 300px
 @media screen and (max-width: 767px)
   .game
     width: 100%
@@ -1624,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
@@ -1644,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
@@ -1713,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>
diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue
index 127d1db7..c48feb08 100644
--- a/client/src/views/Rules.vue
+++ b/client/src/views/Rules.vue
@@ -42,7 +42,7 @@ main
 <script>
 import ComputerGame from "@/components/ComputerGame.vue";
 import { store } from "@/store";
-import { getDiagram } from "@/utils/printDiagram";
+import { replaceByDiag } from "@/utils/printDiagram";
 import { CompgameStorage } from "@/utils/compgameStorage";
 export default {
   name: "my-rules",
@@ -89,7 +89,7 @@ export default {
         .replace(/\\"/g, '"')
         .replace('module.exports = "', "")
         .replace(/"$/, "")
-        .replace(/(fen:)([^:]*):/g, this.replaceByDiag)
+        .replace(/(fen:)([^:]*):/g, replaceByDiag)
       );
     }
   },
@@ -98,20 +98,6 @@ export default {
       if (this.display != "rules") this.display = "rules";
       else if (this.gameInProgress) this.display = "computer";
     },
-    parseFen(fen) {
-      const fenParts = fen.split(" ");
-      return {
-        position: fenParts[0],
-        marks: fenParts[1],
-        orientation: fenParts[2],
-        shadow: fenParts[3]
-      };
-    },
-    // Method to replace diagrams in loaded HTML
-    replaceByDiag: function(match, p1, p2) {
-      const args = this.parseFen(p2);
-      return getDiagram(args);
-    },
     re_setVariant: async function(vname) {
       await import("@/variants/" + vname + ".js")
       .then((vModule) => {
@@ -164,12 +150,6 @@ export default {
 
 <!-- NOTE: not scoped here, because HTML is injected (TODO) -->
 <style lang="sass">
-.warn
-  padding: 3px
-  color: red
-  background-color: lightgrey
-  font-weight: bold
-
 h4#variantName
   text-align: center
   font-weight: bold
-- 
2.44.0