1 // TODO: envoyer juste "light move", sans FEN ni notation ...etc
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 props: ["gameId","mode","allowChat","allowMovelist"],
9 // if oppid == "computer" then mode = "computer" (otherwise human)
10 myid: "", //our ID, always set
11 //this.myid = localStorage.getItem("myid")
12 oppid: "", //opponent ID in case of HH game
13 score: "*", //'*' means 'unfinished'
15 conn: null, //socket connection (et WebRTC connection ?!)
16 oppConnected: false, //TODO?
18 // sound level: 0 = no sound, 1 = sound only on newgame, 2 = always
19 sound: parseInt(localStorage
["sound"] || "2"),
20 // Web worker to play computer moves without freezing interface:
21 compWorker: new Worker('/javascripts/playCompMove.js'),
22 timeStart: undefined, //time when computer starts thinking
23 vr: null, //VariantRules object, describing the game state + rules
27 showChat: function() {
28 return this.allowChat
&& this.mode
=='human' && this.score
!= '*';
30 showMoves: function() {
31 return this.allowMovelist
&& window
.innerWidth
>= 768;
34 return variant
.name
!= "Dark" || this.score
!= "*";
37 // Modal end of game, and then sub-components
38 // TODO: provide chat parameters (connection, players ID...)
39 // and alwo moveList parameters (just moves ?)
40 // TODO: connection + turn indicators en haut à droite (superposé au menu)
41 // TODO: controls: abort, clear, resign, draw (avec confirm box)
42 // et si partie terminée : (mode analyse) just clear, back / play
43 // + flip button toujours disponible
44 // gotoMove : vr = new VariantRules(fen stocké dans le coup [TODO])
46 <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
47 <input id="modal-eog" type="checkbox" class="modal"/>
48 <div role="dialog" aria-labelledby="eogMessage">
49 <div class="card smallpad small-modal text-center">
50 <label for="modal-eog" class="modal-close">
52 <h3 id="eogMessage" class="section">
57 <my-chat v-if="showChat">
59 <my-board v-bind:vr="vr">
61 <div v-show="showFen" id="fen-div" class="section-content">
62 <p id="fen-string" class="text-center">
66 <div id="pgn-div" class="section-content">
67 <a id="download" href: "#">
69 <button id="downloadBtn" @click="download">
70 {{ translations["Download PGN"] }}
73 <my-move-list v-if="showMoves">
78 const url
= socketUrl
;
79 this.conn
= new WebSocket(url
+ "/?sid=" + this.myid
+ "&page=" + variant
._id
);
80 // TODO: after game, archive in indexedDB
81 // TODO: this events listener is central. Refactor ? How ?
82 const socketMessageListener
= msg
=> {
83 const data
= JSON
.parse(msg
.data
);
87 case "newmove": //..he played!
88 this.play(data
.move, (variant
.name
!="Dark" ? "animate" : null));
90 case "pong": //received if we sent a ping (game still alive on our side)
91 if (this.gameId
!= data
.gameId
)
92 break; //games IDs don't match: definitely over...
93 this.oppConnected
= true;
94 // Send our "last state" informations to opponent
95 L
= this.vr
.moves
.length
;
96 this.conn
.send(JSON
.stringify({
100 lastMove: (L
>0?this.vr
.moves
[L
-1]:undefined),
104 case "lastate": //got opponent infos about last move
105 L
= this.vr
.moves
.length
;
106 if (this.gameId
!= data
.gameId
)
107 break; //games IDs don't match: nothing we can do...
108 // OK, opponent still in game (which might be over)
109 if (this.score
!= "*")
111 // We finished the game (any result possible)
112 this.conn
.send(JSON
.stringify({
119 else if (!!data
.score
) //opponent finished the game
120 this.endGame(data
.score
);
121 else if (data
.movesCount
< L
)
123 // We must tell last move to opponent
124 this.conn
.send(JSON
.stringify({
128 lastMove: this.vr
.moves
[L
-1],
132 else if (data
.movesCount
> L
) //just got last move from him
133 this.play(data
.lastMove
, "animate");
135 case "resign": //..you won!
136 this.endGame(this.mycolor
=="w"?"1-0":"0-1");
138 // TODO: also use (dis)connect info to count online players?
141 if (this.mode
=="human" && this.oppid
== data
.id
)
142 this.oppConnected
= (data
.code
== "connect");
143 if (this.oppConnected
&& this.score
!= "*")
145 // Send our name to the opponent, in case of he hasn't it
146 this.conn
.send(JSON
.stringify({
147 code:"myname", name:this.myname
, oppid: this.oppid
}));
153 const socketCloseListener
= () => {
154 this.conn
= new WebSocket(url
+ "/?sid=" + this.myid
+ "&page=" + variant
._id
);
155 this.conn
.addEventListener('message', socketMessageListener
);
156 this.conn
.addEventListener('close', socketCloseListener
);
158 this.conn
.onmessage
= socketMessageListener
;
159 this.conn
.onclose
= socketCloseListener
;
161 // Computer moves web worker logic: (TODO: also for observers in HH games)
162 this.compWorker
.postMessage(["scripts",variant
.name
]);
164 this.compWorker
.onmessage = function(e
) {
165 let compMove
= e
.data
;
167 return; //may happen if MarseilleRules and subTurn==2 (TODO: a bit ugly...)
168 if (!Array
.isArray(compMove
))
169 compMove
= [compMove
]; //to deal with MarseilleRules
170 // TODO: imperfect attempt to avoid ghost move:
171 compMove
.forEach(m
=> { m
.computer
= true; });
172 // (first move) HACK: small delay to avoid selecting elements
173 // before they appear on page:
174 const delay
= Math
.max(500-(Date
.now()-self
.timeStart
), 0);
176 const animate
= (variant
.name
!="Dark" ? "animate" : null);
177 if (self
.mode
== "computer") //warning: mode could have changed!
178 self
.play(compMove
[0], animate
);
179 if (compMove
.length
== 2)
181 if (self
.mode
== "computer")
182 self
.play(compMove
[1], animate
);
187 //TODO: conn pourrait être une prop, donnée depuis variant.js
188 //dans variant.js (plutôt room.js) conn gère aussi les challenges
189 // Puis en webRTC, repenser tout ça.
191 setEndgameMessage: function(score
) {
192 let eogMessage
= "Undefined";
196 eogMessage
= translations
["White win"];
199 eogMessage
= translations
["Black win"];
202 eogMessage
= translations
["Draw"];
205 eogMessage
= "Unfinished";
208 this.endgameMessage
= eogMessage
;
210 download: function() {
211 // Variants may have special PGN structure (so next function isn't defined here)
212 // TODO: get fenStart from local game (using gameid)
213 const content
= V
.GetPGN(this.moves
, this.mycolor
, this.score
, fenStart
, this.mode
);
214 // Prepare and trigger download link
215 let downloadAnchor
= document
.getElementById("download");
216 downloadAnchor
.setAttribute("download", "game.pgn");
217 downloadAnchor
.href
= "data:text/plain;charset=utf-8," + encodeURIComponent(content
);
218 downloadAnchor
.click();
220 showScoreMsg: function(score
) {
221 this.setEndgameMessage(score
);
222 let modalBox
= document
.getElementById("modal-eog");
223 modalBox
.checked
= true;
224 setTimeout(() => { modalBox
.checked
= false; }, 2000);
226 endGame: function(score
) {
228 if (["human","computer"].includes(this.mode
))
230 const prefix
= (this.mode
=="computer" ? "comp-" : "");
231 localStorage
.setItem(prefix
+"score", score
);
233 this.showScoreMsg(score
);
234 if (this.mode
== "human" && this.oppConnected
)
236 // Send our nickname to opponent
237 this.conn
.send(JSON
.stringify({
238 code:"myname", name:this.myname
, oppid:this.oppid
}));
240 // TODO: what about cursor ?
241 //this.cursor = this.vr.moves.length; //to navigate in finished game
243 resign: function(e
) {
244 this.getRidOfTooltip(e
.currentTarget
);
245 if (this.mode
== "human" && this.oppConnected
)
248 this.conn
.send(JSON
.stringify({code: "resign", oppid: this.oppid
}));
249 } catch (INVALID_STATE_ERR
) {
250 return; //socket is not ready (and not yet reconnected)
253 this.endGame(this.mycolor
=="w"?"0-1":"1-0");
255 playComputerMove: function() {
256 this.timeStart
= Date
.now();
257 this.compWorker
.postMessage(["askmove"]);
259 animateMove: function(move) {
260 let startSquare
= document
.getElementById(this.getSquareId(move.start
));
261 let endSquare
= document
.getElementById(this.getSquareId(move.end
));
262 let rectStart
= startSquare
.getBoundingClientRect();
263 let rectEnd
= endSquare
.getBoundingClientRect();
264 let translation
= {x:rectEnd
.x
-rectStart
.x
, y:rectEnd
.y
-rectStart
.y
};
266 document
.querySelector("#" + this.getSquareId(move.start
) + " > img.piece");
267 // HACK for animation (with positive translate, image slides "under background")
268 // Possible improvement: just alter squares on the piece's way...
269 squares
= document
.getElementsByClassName("board");
270 for (let i
=0; i
<squares
.length
; i
++)
272 let square
= squares
.item(i
);
273 if (square
.id
!= this.getSquareId(move.start
))
274 square
.style
.zIndex
= "-1";
276 movingPiece
.style
.transform
= "translate(" + translation
.x
+ "px," +
277 translation
.y
+ "px)";
278 movingPiece
.style
.transitionDuration
= "0.2s";
279 movingPiece
.style
.zIndex
= "3000";
281 for (let i
=0; i
<squares
.length
; i
++)
282 squares
.item(i
).style
.zIndex
= "auto";
283 movingPiece
.style
= {}; //required e.g. for 0-0 with KR swap
284 this.play(move); //TODO: plutôt envoyer message "please play"
287 play: function(move, programmatic
) {
288 if (!!programmatic
) //computer or human opponent
289 return this.animateMove(move);
290 // Not programmatic, or animation is over
292 move.notation
= this.vr
.getNotation(move);
295 move.fen
= this.vr
.getFen();
297 new Audio("/sounds/move.mp3").play().catch(err
=> {});
298 if (this.mode
== "human")
300 updateStorage(move); //after our moves and opponent moves
301 if (this.vr
.turn
== this.userColor
)
302 this.conn
.send(JSON
.stringify({code:"newmove", move:move, oppid:this.oppid
}));
304 else if (this.mode
== "computer")
306 // Send the move to web worker (including his own moves)
307 this.compWorker
.postMessage(["newmove",move]);
309 if (this.score
== "*" || this.mode
== "analyze")
311 // Stack move on movesList
312 this.moves
.push(move);
314 // Is opponent in check?
315 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
316 const score
= this.vr
.getCurrentScore();
319 if (["human","computer"].includes(this.mode
))
321 else //just show score on screen (allow undo)
322 this.showScoreMsg(score
);
323 // TODO: notify end of game (give score)
325 else if (this.mode
== "computer" && this.vr
.turn
!= this.userColor
)
326 this.playComputerMove();
328 undo: function(move) {
331 new Audio("/sounds/undo.mp3").play().catch(err
=> {});
332 this.incheck
= this.vr
.getCheckSquares(this.vr
.turn
);
333 if (this.mode
== "analyze")
339 //TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle"
340 //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions,
343 //TODO: quand partie terminée (ci-dessus) passer partie dans indexedDB