Get rid of ugly this.... calls
[vchess.git] / client / src / components / BaseGame.vue
1 <template lang="pug">
2 div
3 input#modalEog.modal(type="checkbox")
4 div(role="dialog" aria-labelledby="eogMessage")
5 .card.smallpad.small-modal.text-center
6 label.modal-close(for="modalEog")
7 h3#eogMessage.section {{ endgameMessage }}
8 .float70 //TODO: use mini-css predefined styles
9 Board(:vr="vr" :last-move="lastMove" :analyze="analyze"
10 :user-color="game.mycolor" :orientation="orientation"
11 :vname="game.vname" @play-move="play")
12 .button-group
13 button(@click="() => play()") Play
14 button(@click="() => undo()") Undo
15 button(@click="flip") Flip
16 button(@click="gotoBegin") GotoBegin
17 button(@click="gotoEnd") GotoEnd
18 #fenDiv(v-if="showFen && !!vr")
19 p {{ vr.getFen() }}
20 #pgnDiv
21 a#download(href="#")
22 button(@click="download") {{ st.tr["Download PGN"] }}
23 .float30 //TODO: should be optional (adjust widths dynamically)
24 MoveList(v-if="showMoves"
25 :moves="moves" :cursor="cursor" @goto-move="gotoMove")
26 </template>
27
28 <script>
29 import Board from "@/components/Board.vue";
30 import MoveList from "@/components/MoveList.vue";
31 import { store } from "@/store";
32 import { getSquareId } from "@/utils/squareId";
33 import { getDate } from "@/utils/datetime";
34
35 export default {
36 name: 'my-base-game',
37 components: {
38 Board,
39 MoveList,
40 },
41 // "vr": VariantRules object, describing the game state + rules
42 props: ["vr","game"],
43 data: function() {
44 return {
45 st: store.state,
46 // NOTE: all following variables must be reset at the beginning of a game
47 endgameMessage: "",
48 orientation: "w",
49 score: "*", //'*' means 'unfinished'
50 moves: [],
51 cursor: -1, //index of the move just played
52 lastMove: null,
53 };
54 },
55 watch: {
56 // game initial FEN changes when a new game starts
57 "game.fenStart": function() {
58 this.re_setVariables();
59 },
60 // Received a new move to play:
61 "game.moveToPlay": function() {
62 this.play(this.game.moveToPlay, "receive", this.game.vname=="Dark");
63 },
64 "game.score": function() {
65 this.endGame(this.game.score, this.game.scoreMsg);
66 },
67 },
68 computed: {
69 showMoves: function() {
70 return true;
71 //return window.innerWidth >= 768;
72 },
73 showFen: function() {
74 return this.game.vname != "Dark" || this.score != "*";
75 },
76 analyze: function() {
77 return this.game.mode == "analyze" || this.score != "*";
78 },
79 },
80 created: function() {
81 if (!!this.game.fenStart)
82 this.re_setVariables();
83 },
84 methods: {
85 re_setVariables: function() {
86 this.endgameMessage = "";
87 this.orientation = this.game.mycolor || "w"; //default orientation for observed games
88 this.score = this.game.score || "*"; //mutable (if initially "*")
89 this.moves = JSON.parse(JSON.stringify(this.game.moves || []));
90 // Post-processing: decorate each move with color + current FEN:
91 // (to be able to jump to any position quickly)
92 let vr_tmp = new V(this.game.fenStart); //vr is already at end of game
93 this.moves.forEach(move => {
94 // NOTE: this is doing manually what play() function below achieve,
95 // but in a lighter "fast-forward" way
96 move.color = vr_tmp.turn;
97 move.notation = vr_tmp.getNotation(move);
98 vr_tmp.play(move);
99 move.fen = vr_tmp.getFen();
100 });
101 const L = this.moves.length;
102 this.cursor = L-1;
103 this.lastMove = (L > 0 ? this.moves[L-1] : null);
104 },
105 download: function() {
106 const content = this.getPgn();
107 // Prepare and trigger download link
108 let downloadAnchor = document.getElementById("download");
109 downloadAnchor.setAttribute("download", "game.pgn");
110 downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
111 downloadAnchor.click();
112 },
113 getPgn: function() {
114 let pgn = "";
115 pgn += '[Site "vchess.club"]\n';
116 pgn += '[Variant "' + this.game.vname + '"]\n';
117 pgn += '[Date "' + getDate(new Date()) + '"]\n';
118 pgn += '[White "' + this.game.players[0].name + '"]\n';
119 pgn += '[Black "' + this.game.players[1].name + '"]\n';
120 pgn += '[Fen "' + this.game.fenStart + '"]\n';
121 pgn += '[Result "' + this.score + '"]\n\n';
122 let counter = 1;
123 let i = 0;
124 while (i < this.moves.length)
125 {
126 pgn += (counter++) + ".";
127 for (let color of ["w","b"])
128 {
129 let move = "";
130 while (i < this.moves.length && this.moves[i].color == color)
131 move += this.moves[i++].notation + ",";
132 move = move.slice(0,-1); //remove last comma
133 pgn += move + (i < this.moves.length ? " " : "");
134 }
135 }
136 return pgn + "\n";
137 },
138 getScoreMessage: function(score) {
139 let eogMessage = "Undefined";
140 switch (score)
141 {
142 case "1-0":
143 eogMessage = this.st.tr["White win"];
144 break;
145 case "0-1":
146 eogMessage = this.st.tr["Black win"];
147 break;
148 case "1/2":
149 eogMessage = this.st.tr["Draw"];
150 break;
151 case "?":
152 eogMessage = this.st.tr["Unfinished"];
153 break;
154 }
155 return eogMessage;
156 },
157 showEndgameMsg: function(message) {
158 this.endgameMessage = message;
159 let modalBox = document.getElementById("modalEog");
160 modalBox.checked = true;
161 setTimeout(() => { modalBox.checked = false; }, 2000);
162 },
163 endGame: function(score, message) {
164 this.score = score;
165 if (!message)
166 message = this.getScoreMessage(score);
167 this.showEndgameMsg(score + " . " + message);
168 this.$emit("gameover", score);
169 },
170 animateMove: function(move) {
171 let startSquare = document.getElementById(getSquareId(move.start));
172 let endSquare = document.getElementById(getSquareId(move.end));
173 let rectStart = startSquare.getBoundingClientRect();
174 let rectEnd = endSquare.getBoundingClientRect();
175 let translation = {x:rectEnd.x-rectStart.x, y:rectEnd.y-rectStart.y};
176 let movingPiece =
177 document.querySelector("#" + getSquareId(move.start) + " > img.piece");
178 // HACK for animation (with positive translate, image slides "under background")
179 // Possible improvement: just alter squares on the piece's way...
180 const squares = document.getElementsByClassName("board");
181 for (let i=0; i<squares.length; i++)
182 {
183 let square = squares.item(i);
184 if (square.id != getSquareId(move.start))
185 square.style.zIndex = "-1";
186 }
187 movingPiece.style.transform = "translate(" + translation.x + "px," +
188 translation.y + "px)";
189 movingPiece.style.transitionDuration = "0.2s";
190 movingPiece.style.zIndex = "3000";
191 setTimeout( () => {
192 for (let i=0; i<squares.length; i++)
193 squares.item(i).style.zIndex = "auto";
194 movingPiece.style = {}; //required e.g. for 0-0 with KR swap
195 this.play(move);
196 }, 250);
197 },
198 play: function(move, receive, noanimate) {
199 const navigate = !move;
200 // Forbid playing outside analyze mode when cursor isn't at moves.length-1
201 // (except if we receive opponent's move, human or computer)
202 if (!navigate && !this.analyze && !receive
203 && this.cursor < this.moves.length-1)
204 {
205 return;
206 }
207 if (navigate)
208 {
209 if (this.cursor == this.moves.length-1)
210 return; //no more moves
211 move = this.moves[this.cursor+1];
212 }
213 if (!!receive && !noanimate) //opponent move, variant != "Dark"
214 {
215 if (this.cursor < this.moves.length-1)
216 this.gotoEnd(); //required to play the move
217 return this.animateMove(move);
218 }
219 if (!navigate)
220 {
221 move.color = this.vr.turn;
222 move.notation = this.vr.getNotation(move);
223 }
224 // Not programmatic, or animation is over
225 this.vr.play(move);
226 this.cursor++;
227 this.lastMove = move;
228 if (this.st.settings.sound == 2)
229 new Audio("/sounds/move.mp3").play().catch(err => {});
230 if (!navigate)
231 {
232 move.fen = this.vr.getFen();
233 if (this.score == "*" || this.analyze)
234 {
235 // Stack move on movesList at current cursor
236 if (this.cursor == this.moves.length)
237 this.moves.push(move);
238 else
239 this.moves = this.moves.slice(0,this.cursor).concat([move]);
240 }
241 }
242 if (!this.analyze)
243 this.$emit("newmove", move); //post-processing (e.g. computer play)
244 // Is opponent in check?
245 this.incheck = this.vr.getCheckSquares(this.vr.turn);
246 const score = this.vr.getCurrentScore();
247 if (score != "*")
248 {
249 if (!this.analyze)
250 this.endGame(score);
251 else
252 {
253 // Just show score on screen (allow undo)
254 const message = this.getScoreMessage(score);
255 this.showEndgameMsg(score + " . " + message);
256 }
257 }
258 },
259 undo: function(move) {
260 const navigate = !move;
261 if (navigate)
262 {
263 if (this.cursor < 0)
264 return; //no more moves
265 move = this.moves[this.cursor];
266 }
267 this.vr.undo(move);
268 this.cursor--;
269 this.lastMove = (this.cursor >= 0 ? this.moves[this.cursor] : undefined);
270 if (this.st.settings.sound == 2)
271 new Audio("/sounds/undo.mp3").play().catch(err => {});
272 this.incheck = this.vr.getCheckSquares(this.vr.turn);
273 if (!navigate)
274 this.moves.pop();
275 },
276 gotoMove: function(index) {
277 this.vr.re_init(this.moves[index].fen);
278 this.cursor = index;
279 this.lastMove = this.moves[index];
280 },
281 gotoBegin: function() {
282 this.vr.re_init(this.game.fenStart);
283 this.cursor = -1;
284 this.lastMove = null;
285 },
286 gotoEnd: function() {
287 this.gotoMove(this.moves.length-1);
288 },
289 flip: function() {
290 this.orientation = V.GetNextCol(this.orientation);
291 },
292 },
293 };
294 </script>