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 ?)
4 // Game logic on a variant page: 3 modes, analyze, computer or human
5 Vue
.component('my-game', {
6 // gameId: to find the game in storage (assumption: it exists)
7 // fen: to start from a FEN without identifiers (analyze mode)
8 props: ["conn","gameId","fen","mode","allowChat","allowMovelist"],
11 // if oppid == "computer" then mode = "computer" (otherwise human)
12 myid: "", //our ID, always set
13 //this.myid = localStorage.getItem("myid")
14 oppid: "", //opponent ID in case of HH game
15 score: "*", //'*' means 'unfinished'
17 oppConnected: false, //TODO?
19 // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
20 sound: parseInt(localStorage
["sound"] || "2"),
21 // Web worker to play computer moves without freezing interface:
22 compWorker: new Worker('/javascripts/playCompMove.js'),
23 timeStart: undefined, //time when computer starts thinking
24 vr: null, //VariantRules object, describing the game state + rules
28 moves: [], //TODO: initialize if gameId is defined...
29 // orientation :: button flip
30 // userColor: given by gameId, or fen (if no game Id)
31 // gameOver: known if gameId; otherwise assue false
32 // lastMove: update after every play, initialize with last move from list (if continuation)
33 //orientation ? userColor ? gameOver ? lastMove ?
38 fen: function(newFen
) {
39 this.vr
= new VariantRules(newFen
);
46 showChat: function() {
47 return this.allowChat
&& this.mode
=='human' && this.score
!= '*';
49 showMoves: function() {
50 return this.allowMovelist
&& window
.innerWidth
>= 768;
53 return variant
.name
!= "Dark" || this.score
!= "*";
56 // Modal end of game, and then sub-components
57 // TODO: provide chat parameters (connection, players ID...)
58 // and also moveList parameters (just moves ?)
59 // TODO: connection + turn indicators en haut à droite (superposé au menu)
60 // TODO: controls: abort, clear, resign, draw (avec confirm box)
61 // et si partie terminée : (mode analyse) just clear, back / play
62 // + flip button toujours disponible
63 // gotoMove : vr = new VariantRules(fen stocké dans le coup [TODO])
65 // NOTE: move.color must be fulfilled after each move played, because of Marseille (or Avalanche) chess
66 // --> useful in moveList component (universal comma separator ?)
69 <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
70 <input id="modal-eog" type="checkbox" class="modal"/>
71 <div role="dialog" aria-labelledby="eogMessage">
72 <div class="card smallpad small-modal text-center">
73 <label for="modal-eog" class="modal-close">
75 <h3 id="eogMessage" class="section">
80 <my-chat v-if="showChat">
82 <my-board v-bind:vr="vr" :mode="mode" :orientation="orientation" :user-color="mycolor" @play-move="play">
84 <div v-if="showFen && !!vr" id="fen-div" class="section-content">
85 <p id="fen-string" class="text-center">
89 <div id="pgn-div" class="section-content">
90 <a id="download" href="#">
92 <button id="downloadBtn" @click="download">
93 {{ translate("Download PGN") }}
96 <my-move-list v-if="showMoves">
100 created: function() {
102 // console.log(this.fen);
103 // console.log(this.gameId);
107 this.vr
= new VariantRules(this.fen
);
108 // TODO: after game, archive in indexedDB
109 // TODO: this events listener is central. Refactor ? How ?
110 const socketMessageListener
= msg
=> {
111 const data
= JSON
.parse(msg
.data
);
115 case "newmove": //..he played!
116 this.play(data
.move, (variant
.name
!="Dark" ? "animate" : null));
118 case "pong": //received if we sent a ping (game still alive on our side)
119 if (this.gameId
!= data
.gameId
)
120 break; //games IDs don't match: definitely over...
121 this.oppConnected
= true;
122 // Send our "last state" informations to opponent
123 L
= this.vr
.moves
.length
;
124 this.conn
.send(JSON
.stringify({
128 lastMove: (L
>0?this.vr
.moves
[L
-1]:undefined),
132 case "lastate": //got opponent infos about last move
133 L
= this.vr
.moves
.length
;
134 if (this.gameId
!= data
.gameId
)
135 break; //games IDs don't match: nothing we can do...
136 // OK, opponent still in game (which might be over)
137 if (this.score
!= "*")
139 // We finished the game (any result possible)
140 this.conn
.send(JSON
.stringify({
147 else if (!!data
.score
) //opponent finished the game
148 this.endGame(data
.score
);
149 else if (data
.movesCount
< L
)
151 // We must tell last move to opponent
152 this.conn
.send(JSON
.stringify({
156 lastMove: this.vr
.moves
[L
-1],
160 else if (data
.movesCount
> L
) //just got last move from him
161 this.play(data
.lastMove
, "animate");
163 case "resign": //..you won!
164 this.endGame(this.mycolor
=="w"?"1-0":"0-1");
166 // TODO: also use (dis)connect info to count online players?
169 if (this.mode
=="human" && this.oppid
== data
.id
)
170 this.oppConnected
= (data
.code
== "connect");
171 if (this.oppConnected
&& this.score
!= "*")
173 // Send our name to the opponent, in case of he hasn't it
174 this.conn
.send(JSON
.stringify({
175 code:"myname", name:this.myname
, oppid: this.oppid
}));
181 const socketCloseListener
= () => {
182 this.conn
.addEventListener('message', socketMessageListener
);
183 this.conn
.addEventListener('close', socketCloseListener
);
185 this.conn
.onmessage
= socketMessageListener
;
186 this.conn
.onclose
= socketCloseListener
;
188 // Computer moves web worker logic: (TODO: also for observers in HH games)
189 this.compWorker
.postMessage(["scripts",variant
.name
]);
191 this.compWorker
.onmessage = function(e
) {
192 let compMove
= e
.data
;
194 return; //may happen if MarseilleRules and subTurn==2 (TODO: a bit ugly...)
195 if (!Array
.isArray(compMove
))
196 compMove
= [compMove
]; //to deal with MarseilleRules
197 // TODO: imperfect attempt to avoid ghost move:
198 compMove
.forEach(m
=> { m
.computer
= true; });
199 // (first move) HACK: small delay to avoid selecting elements
200 // before they appear on page:
201 const delay
= Math
.max(500-(Date
.now()-self
.timeStart
), 0);
203 const animate
= (variant
.name
!="Dark" ? "animate" : null);
204 if (self
.mode
== "computer") //warning: mode could have changed!
205 self
.play(compMove
[0], animate
);
206 if (compMove
.length
== 2)
208 if (self
.mode
== "computer")
209 self
.play(compMove
[1], animate
);
214 // this.conn est une prop, donnée depuis variant.js
215 //dans variant.js (plutôt room.js) conn gère aussi les challenges
216 // Puis en webRTC, repenser tout ça.
218 translate: translate
,
219 loadGame: function() {
220 // TODO: load this.gameId ...
222 setEndgameMessage: function(score
) {
223 let eogMessage
= "Undefined";
227 eogMessage
= translations
["White win"];
230 eogMessage
= translations
["Black win"];
233 eogMessage
= translations
["Draw"];
236 eogMessage
= "Unfinished";
239 this.endgameMessage
= eogMessage
;
241 download: function() {
242 // Variants may have special PGN structure (so next function isn't defined here)
243 // TODO: get fenStart from local game (using gameid)
244 const content
= V
.GetPGN(this.moves
, this.mycolor
, this.score
, fenStart
, this.mode
);
245 // Prepare and trigger download link
246 let downloadAnchor
= document
.getElementById("download");
247 downloadAnchor
.setAttribute("download", "game.pgn");
248 downloadAnchor
.href
= "data:text/plain;charset=utf-8," + encodeURIComponent(content
);
249 downloadAnchor
.click();
251 showScoreMsg: function(score
) {
252 this.setEndgameMessage(score
);
253 let modalBox
= document
.getElementById("modal-eog");
254 modalBox
.checked
= true;
255 setTimeout(() => { modalBox
.checked
= false; }, 2000);
257 endGame: function(score
) {
259 if (["human","computer"].includes(this.mode
))
261 const prefix
= (this.mode
=="computer" ? "comp-" : "");
262 localStorage
.setItem(prefix
+"score", score
);
264 this.showScoreMsg(score
);
265 if (this.mode
== "human" && this.oppConnected
)
267 // Send our nickname to opponent
268 this.conn
.send(JSON
.stringify({
269 code:"myname", name:this.myname
, oppid:this.oppid
}));
271 // TODO: what about cursor ?
272 //this.cursor = this.vr.moves.length; //to navigate in finished game
274 resign: function(e
) {
275 this.getRidOfTooltip(e
.currentTarget
);
276 if (this.mode
== "human" && this.oppConnected
)
279 this.conn
.send(JSON
.stringify({code: "resign", oppid: this.oppid
}));
280 } catch (INVALID_STATE_ERR
) {
281 return; //socket is not ready (and not yet reconnected)
284 this.endGame(this.mycolor
=="w"?"0-1":"1-0");
286 playComputerMove: function() {
287 this.timeStart
= Date
.now();
288 this.compWorker
.postMessage(["askmove"]);
290 animateMove: function(move) {
291 let startSquare
= document
.getElementById(this.getSquareId(move.start
));
292 let endSquare
= document
.getElementById(this.getSquareId(move.end
));
293 let rectStart
= startSquare
.getBoundingClientRect();
294 let rectEnd
= endSquare
.getBoundingClientRect();
295 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
297 document
.querySelector("#" + this.getSquareId(move.start
) + " > img.piece");
298 // HACK for animation (with positive translate, image slides "under background")
299 // Possible improvement: just alter squares on the piece's way...
300 squares
= document
.getElementsByClassName("board");
301 for (let i
=0; i
<squares
.length
; i
++)
303 let square
= squares
.item(i
);
304 if (square
.id
!= this.getSquareId(move.start
))
305 square
.style
.zIndex
= "-1";
307 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," +
308 translation
.y
+ "px)";
309 movingPiece
.style
.transitionDuration
= "0.2s";
310 movingPiece
.style
.zIndex
= "3000";
312 for (let i
=0; i
<squares
.length
; i
++)
313 squares
.item(i
).style
.zIndex
= "auto";
314 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
315 this.play(move); //TODO: plutôt envoyer message "please play"
318 play: function(move, programmatic
) {
319 if (!!programmatic
) //computer or human opponent
320 return this.animateMove(move);
321 // Not programmatic, or animation is over
323 move.notation
= this.vr
.getNotation(move);
326 move.fen
= this.vr
.getFen();
328 new Audio("/sounds/move.mp3").play().catch(err
=> {});
329 if (this.mode
== "human")
331 updateStorage(move); //after our moves and opponent moves
332 if (this.vr
.turn
== this.userColor
)
333 this.conn
.send(JSON
.stringify({code:"newmove", move:move, oppid:this.oppid
}));
335 else if (this.mode
== "computer")
337 // Send the move to web worker (including his own moves)
338 this.compWorker
.postMessage(["newmove",move]);
340 if (this.score
== "*" || this.mode
== "analyze")
342 // Stack move on movesList
343 this.moves
.push(move);
345 // Is opponent in check?
346 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
347 const score
= this.vr
.getCurrentScore();
350 if (["human","computer"].includes(this.mode
))
352 else //just show score on screen (allow undo)
353 this.showScoreMsg(score
);
354 // TODO: notify end of game (give score)
356 else if (this.mode
== "computer" && this.vr
.turn
!= this.userColor
)
357 this.playComputerMove();
359 undo: function(move) {
362 new Audio("/sounds/undo.mp3").play().catch(err
=> {});
363 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
364 if (this.mode
== "analyze")
370 //TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
371 //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
374 //TODO: quand partie terminée (ci-dessus) passer partie dans indexedDB