1 // TODO: envoyer juste "light move", sans FEN ni notation ...etc
2 // TODO: also "observers" prop, we should send moves to them too (in a web worker ? webRTC ?)
3 // Game logic on a variant page: 3 modes, analyze, computer or human
4 Vue
.component('my-game', {
5 // gameId: to find the game in storage (assumption: it exists)
6 // fen: to start from a FEN without identifiers (analyze mode)
7 props: ["conn","gameId","fen","mode","allowChat","allowMovelist","queryHash","settings"],
10 oppConnected: false, //TODO?
11 // Web worker to play computer moves without freezing interface:
12 compWorker: new Worker('/javascripts/playCompMove.js'),
13 timeStart: undefined, //time when computer starts thinking
14 vr: null, //VariantRules object, describing the game state + rules
18 oppid: "", //opponent ID in case of HH game
19 score: "*", //'*' means 'unfinished'
20 // userColor: given by gameId, or fen in problems mode (if no game Id)...
23 moves: [], //TODO: initialize if gameId is defined...
29 fen: function(newFen
) {
30 this.vr
= new VariantRules(newFen
);
33 this.fenStart
= newFen
;
35 if (this.mode
== "analyze")
37 this.mycolor
= V
.ParseFen(newFen
).turn
;
38 this.orientation
= "w"; //convention (TODO?!)
40 else if (this.mode
== "computer") //only other alternative (HH with gameId)
42 this.mycolor
= (Math
.random() < 0.5 ? "w" : "b");
43 this.orientation
= this.mycolor
;
44 this.compWorker
.postMessage(["init",newFen
]);
50 queryHash: function(newQhash
) {
51 // New query hash = "id=42"; get 42 as gameId
52 this.gameId
= parseInt(newQhash
.substr(2));
57 showChat: function() {
58 return this.allowChat
&& this.mode
=='human' && this.score
!= '*';
60 showMoves: function() {
62 return this.allowMovelist
&& window
.innerWidth
>= 768;
65 return variant
.name
!= "Dark" || this.score
!= "*";
68 // Modal end of game, and then sub-components
69 // TODO: provide chat parameters (connection, players ID...)
70 // TODO: controls: abort, clear, resign, draw (avec confirm box)
72 <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
73 <input id="modal-eog" type="checkbox" class="modal"/>
74 <div role="dialog" aria-labelledby="eogMessage">
75 <div class="card smallpad small-modal text-center">
76 <label for="modal-eog" class="modal-close">
78 <h3 id="eogMessage" class="section">
83 <my-chat v-if="showChat">
85 <my-board v-bind:vr="vr" :last-move="lastMove" :mode="mode"
86 :orientation="orientation" :user-color="mycolor" :settings="settings"
89 <div class="button-group">
90 <button @click="() => play()">Play</button>
91 <button @click="() => undo()">Undo</button>
92 <button @click="flip">Flip</button>
93 <button @click="gotoBegin">GotoBegin</button>
94 <button @click="gotoEnd">GotoEnd</button>
96 <div v-if="showFen && !!vr" id="fen-div" class="section-content">
97 <p id="fen-string" class="text-center">
101 <div id="pgn-div" class="section-content">
102 <a id="download" href="#">
104 <button id="downloadBtn" @click="download">
105 {{ translate("Download PGN") }}
108 <my-move-list v-if="showMoves" :moves="moves" :cursor="cursor" @goto-move="gotoMove">
112 created: function() {
117 this.vr
= new VariantRules(this.fen
);
118 this.fenStart
= this.fen
;
120 // TODO: after game, archive in indexedDB
121 // TODO: this events listener is central. Refactor ? How ?
122 const socketMessageListener
= msg
=> {
123 const data
= JSON
.parse(msg
.data
);
127 case "newmove": //..he played!
128 this.play(data
.move, (variant
.name
!="Dark" ? "animate" : null));
130 case "pong": //received if we sent a ping (game still alive on our side)
131 if (this.gameId
!= data
.gameId
)
132 break; //games IDs don't match: definitely over...
133 this.oppConnected
= true;
134 // Send our "last state" informations to opponent
135 L
= this.vr
.moves
.length
;
136 this.conn
.send(JSON
.stringify({
140 lastMove: (L
>0?this.vr
.moves
[L
-1]:undefined),
144 case "lastate": //got opponent infos about last move
145 L
= this.vr
.moves
.length
;
146 if (this.gameId
!= data
.gameId
)
147 break; //games IDs don't match: nothing we can do...
148 // OK, opponent still in game (which might be over)
149 if (this.score
!= "*")
151 // We finished the game (any result possible)
152 this.conn
.send(JSON
.stringify({
159 else if (!!data
.score
) //opponent finished the game
160 this.endGame(data
.score
);
161 else if (data
.movesCount
< L
)
163 // We must tell last move to opponent
164 this.conn
.send(JSON
.stringify({
168 lastMove: this.vr
.moves
[L
-1],
172 else if (data
.movesCount
> L
) //just got last move from him
173 this.play(data
.lastMove
, "animate");
175 case "resign": //..you won!
176 this.endGame(this.mycolor
=="w"?"1-0":"0-1");
178 // TODO: also use (dis)connect info to count online players?
181 if (this.mode
=="human" && this.oppid
== data
.id
)
182 this.oppConnected
= (data
.code
== "connect");
183 if (this.oppConnected
&& this.score
!= "*")
185 // Send our name to the opponent, in case of he hasn't it
186 this.conn
.send(JSON
.stringify({
187 code:"myname", name:this.myname
, oppid: this.oppid
}));
193 const socketCloseListener
= () => {
194 this.conn
.addEventListener('message', socketMessageListener
);
195 this.conn
.addEventListener('close', socketCloseListener
);
199 this.conn
.onmessage
= socketMessageListener
;
200 this.conn
.onclose
= socketCloseListener
;
203 // Computer moves web worker logic: (TODO: also for observers in HH games)
204 this.compWorker
.postMessage(["scripts",variant
.name
]);
206 this.compWorker
.onmessage = function(e
) {
207 let compMove
= e
.data
;
209 return; //may happen if MarseilleRules and subTurn==2 (TODO: a bit ugly...)
210 if (!Array
.isArray(compMove
))
211 compMove
= [compMove
]; //to deal with MarseilleRules
212 // TODO: imperfect attempt to avoid ghost move:
213 compMove
.forEach(m
=> { m
.computer
= true; });
214 // (first move) HACK: small delay to avoid selecting elements
215 // before they appear on page:
216 const delay
= Math
.max(500-(Date
.now()-self
.timeStart
), 0);
218 const animate
= (variant
.name
!="Dark" ? "animate" : null);
219 if (self
.mode
== "computer") //warning: mode could have changed!
220 self
.play(compMove
[0], animate
);
221 if (compMove
.length
== 2)
223 if (self
.mode
== "computer")
224 self
.play(compMove
[1], animate
);
229 // this.conn est une prop, donnée depuis variant.js
230 //dans variant.js (plutôt room.js) conn gère aussi les challenges
231 // Puis en webRTC, repenser tout ça.
233 translate: translate
,
234 loadGame: function() {
235 const game
= getGameFromStorage(this.gameId
);
236 this.oppid
= game
.oppid
; //opponent ID in case of running HH game
237 this.score
= game
.score
;
238 this.mycolor
= game
.mycolor
|| "w";
239 this.fenStart
= game
.fenStart
;
240 this.moves
= game
.moves
;
241 this.cursor
= game
.moves
.length
;
242 this.lastMove
= (game
.moves
.length
> 0 ? game
.moves
[this.cursor
-1] : null);
244 setEndgameMessage: function(score
) {
245 let eogMessage
= "Undefined";
249 eogMessage
= translations
["White win"];
252 eogMessage
= translations
["Black win"];
255 eogMessage
= translations
["Draw"];
258 eogMessage
= "Unfinished";
261 this.endgameMessage
= eogMessage
;
263 download: function() {
264 const content
= this.getPgn();
265 // Prepare and trigger download link
266 let downloadAnchor
= document
.getElementById("download");
267 downloadAnchor
.setAttribute("download", "game.pgn");
268 downloadAnchor
.href
= "data:text/plain;charset=utf-8," + encodeURIComponent(content
);
269 downloadAnchor
.click();
273 pgn
+= '[Site "vchess.club"]\n';
274 const opponent
= (this.mode
=="human" ? "Anonymous" : "Computer");
275 pgn
+= '[Variant "' + variant
.name
+ '"]\n';
276 pgn
+= '[Date "' + getDate(new Date()) + '"]\n';
277 const whiteName
= ["human","computer"].includes(this.mode
)
278 ? (this.mycolor
=='w'?'Myself':opponent
)
280 const blackName
= ["human","computer"].includes(this.mode
)
281 ? (this.mycolor
=='b'?'Myself':opponent
)
283 pgn
+= '[White "' + whiteName
+ '"]\n';
284 pgn
+= '[Black "' + blackName
+ '"]\n';
285 pgn
+= '[Fen "' + this.fenStart
+ '"]\n';
286 pgn
+= '[Result "' + this.score
+ '"]\n\n';
289 while (i
< this.moves
.length
)
291 pgn
+= (counter
++) + ".";
292 for (let color
of ["w","b"])
295 while (i
< this.moves
.length
&& this.moves
[i
].color
== color
)
296 move += this.moves
[i
++].notation
[0] + ",";
297 move = move.slice(0,-1); //remove last comma
298 pgn
+= move + (i
< this.moves
.length
-1 ? " " : "");
303 showScoreMsg: function(score
) {
304 this.setEndgameMessage(score
);
305 let modalBox
= document
.getElementById("modal-eog");
306 modalBox
.checked
= true;
307 setTimeout(() => { modalBox
.checked
= false; }, 2000);
309 endGame: function(score
) {
311 if (["human","computer"].includes(this.mode
))
313 const prefix
= (this.mode
=="computer" ? "comp-" : "");
314 localStorage
.setItem(prefix
+"score", score
);
316 this.showScoreMsg(score
);
317 if (this.mode
== "human" && this.oppConnected
)
319 // Send our nickname to opponent
320 this.conn
.send(JSON
.stringify({
321 code:"myname", name:this.myname
, oppid:this.oppid
}));
323 // TODO: what about cursor ?
324 //this.cursor = this.vr.moves.length; //to navigate in finished game
326 resign: function(e
) {
327 this.getRidOfTooltip(e
.currentTarget
);
328 if (this.mode
== "human" && this.oppConnected
)
331 this.conn
.send(JSON
.stringify({code: "resign", oppid: this.oppid
}));
332 } catch (INVALID_STATE_ERR
) {
333 return; //socket is not ready (and not yet reconnected)
336 this.endGame(this.mycolor
=="w"?"0-1":"1-0");
338 playComputerMove: function() {
339 this.timeStart
= Date
.now();
340 this.compWorker
.postMessage(["askmove"]);
342 animateMove: function(move) {
343 let startSquare
= document
.getElementById(getSquareId(move.start
));
344 let endSquare
= document
.getElementById(getSquareId(move.end
));
345 let rectStart
= startSquare
.getBoundingClientRect();
346 let rectEnd
= endSquare
.getBoundingClientRect();
347 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
349 document
.querySelector("#" + getSquareId(move.start
) + " > img.piece");
350 // HACK for animation (with positive translate, image slides "under background")
351 // Possible improvement: just alter squares on the piece's way...
352 squares
= document
.getElementsByClassName("board");
353 for (let i
=0; i
<squares
.length
; i
++)
355 let square
= squares
.item(i
);
356 if (square
.id
!= getSquareId(move.start
))
357 square
.style
.zIndex
= "-1";
359 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," +
360 translation
.y
+ "px)";
361 movingPiece
.style
.transitionDuration
= "0.2s";
362 movingPiece
.style
.zIndex
= "3000";
364 for (let i
=0; i
<squares
.length
; i
++)
365 squares
.item(i
).style
.zIndex
= "auto";
366 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
367 this.play(move); //TODO: plutôt envoyer message "please play"
370 play: function(move, programmatic
) {
371 // Forbid playing outside analyze mode when cursor isn't at moves.length-1
372 if (this.mode
!= "analyze" && this.cursor
< this.moves
.length
-1)
374 let navigate
= !move;
377 if (this.cursor
== this.moves
.length
)
378 return; //no more moves
379 move = this.moves
[this.cursor
];
381 if (!!programmatic
) //computer or human opponent
382 return this.animateMove(move);
383 // Not programmatic, or animation is over
385 move.notation
= this.vr
.getNotation(move);
387 move.color
= this.vr
.turn
;
390 this.lastMove
= move;
392 move.fen
= this.vr
.getFen();
393 if (this.settings
.sound
== 2)
394 new Audio("/sounds/move.mp3").play().catch(err
=> {});
395 if (this.mode
== "human")
397 updateStorage(move); //after our moves and opponent moves
398 if (this.vr
.turn
== this.mycolor
)
399 this.conn
.send(JSON
.stringify({code:"newmove", move:move, oppid:this.oppid
}));
401 else if (this.mode
== "computer")
403 // Send the move to web worker (including his own moves)
404 this.compWorker
.postMessage(["newmove",move]);
406 if (!navigate
&& (this.score
== "*" || this.mode
== "analyze"))
408 // Stack move on movesList at current cursor
409 if (this.cursor
== this.moves
.length
)
410 this.moves
.push(move);
412 this.moves
= this.moves
.slice(0,this.cursor
-1).concat([move]);
414 // Is opponent in check?
415 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
416 const score
= this.vr
.getCurrentScore();
419 if (["human","computer"].includes(this.mode
))
421 else //just show score on screen (allow undo)
422 this.showScoreMsg(score
);
423 // TODO: notify end of game (give score)
425 else if (this.mode
== "computer" && this.vr
.turn
!= this.mycolor
)
426 this.playComputerMove();
427 // https://vuejs.org/v2/guide/list.html#Caveats (also for undo)
429 this.$children
[0].$forceUpdate(); //TODO!?
431 undo: function(move) {
432 let navigate
= !move;
435 if (this.cursor
== 0)
436 return; //no more moves
437 move = this.moves
[this.cursor
-1];
441 this.lastMove
= (this.cursor
> 0 ? this.moves
[this.cursor
-1] : undefined);
443 this.$children
[0].$forceUpdate(); //TODO!?
444 if (this.settings
.sound
== 2)
445 new Audio("/sounds/undo.mp3").play().catch(err
=> {});
446 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
447 if (!navigate
&& this.mode
== "analyze")
450 this.$forceUpdate(); //TODO!?
452 gotoMove: function(index
) {
453 this.vr
= new VariantRules(this.moves
[index
].fen
);
454 this.cursor
= index
+1;
455 this.lastMove
= this.moves
[index
];
457 gotoBegin: function() {
458 this.vr
= new VariantRules(this.fenStart
);
460 this.lastMove
= null;
462 gotoEnd: function() {
463 this.gotoMove(this.moves
.length
-1);
464 this.lastMove
= this.moves
[this.moves
.length
-1];
467 this.orientation
= V
.GetNextCol(this.orientation
);
471 //TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
472 //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
474 //TODO: quand partie terminée (ci-dessus) passer partie dans indexedDB