21d205c409cb3263999dd9c81d3301e5acb9b481
1 let $ = document
; //shortcut
6 // https://stackoverflow.com/a/27747377/12660887
7 function dec2hex (dec
) { return dec
.toString(16).padStart(2, "0") }
8 function generateId (len
) {
9 var arr
= new Uint8Array((len
|| 40) / 2)
10 window
.crypto
.getRandomValues(arr
)
11 return Array
.from(arr
, dec2hex
).join('')
14 // Populate variants dropdown list
15 let dropdown
= $.getElementById("selectVariant");
16 dropdown
[0] = new Option("? ? ?", "_random", true, true);
17 dropdown
[0].title
= "Random variant";
18 for (let i
= 0; i
< variants
.length
; i
++) {
19 let newOption
= new Option(
20 variants
[i
].disp
|| variants
[i
].name
, variants
[i
].name
, false, false);
21 newOption
.title
= variants
[i
].desc
;
22 dropdown
[dropdown
.length
] = newOption
;
25 // Ensure that I have a socket ID and a name
26 if (!localStorage
.getItem("sid"))
27 localStorage
.setItem("sid", generateId(8));
28 if (!localStorage
.getItem("name"))
29 localStorage
.setItem("name", "@non" + generateId(4));
30 const sid
= localStorage
.getItem("sid");
31 $.getElementById("myName").value
= localStorage
.getItem("name");
37 localStorage
.setItem("name", $.getElementById("myName").value
);
40 // Turn a "tab" on, and "close" all others
41 function toggleVisible(element
) {
42 for (elt
of document
.querySelectorAll('body > div')) {
43 if (elt
.id
!= element
) elt
.style
.display
= "none";
44 else elt
.style
.display
= "block";
50 seek_vname
= $.getElementById("selectVariant").value
;
51 send("seekgame", {vname: seek_vname
, name: localStorage
.getItem("name")});
52 toggleVisible("pendingSeek");
54 function cancelSeek() {
55 send("cancelseek", {vname: seek_vname
});
56 toggleVisible("newGame");
59 function sendRematch() {
60 send("rematch", { gid: gid
});
61 toggleVisible("pendingRematch");
63 function cancelRematch() {
64 send("norematch", { gid: gid
});
65 toggleVisible("newGame");
68 // Play with a friend (or not ^^)
69 function showNewGameForm() {
70 const vname
= $.getElementById("selectVariant").value
;
71 if (vname
== "_random") alert("Select a variant first");
73 $.getElementById("gameLink").innerHTML
= "";
74 $.getElementById("selectColor").selectedIndex
= 0;
75 toggleVisible("newGameForm");
76 import(`/variants/${vname}/class.js`).then(module
=> {
77 const Rules
= module
.default;
78 prepareOptions(Rules
);
82 function backToNormalSeek() { toggleVisible("newGame"); }
84 function toggleStyle(e
, word
) {
85 options
[word
] = !options
[word
];
86 e
.target
.classList
.toggle("highlight-word");
90 function prepareOptions(Rules
) {
93 for (let select
of Rules
.Options
.select
) {
95 <label for="var_${select.variable}">${select.label}</label>
96 <select id="var_${select.variable}" data-numeric="1">`;
97 for (option
of select
.options
) {
98 const defaut
= option
.value
== select
.defaut
;
99 optHtml
+= `<option value="${option.value}"`;
100 if (defaut
) optHtml
+= 'selected="true"';
101 optHtml
+= `>${option.label}</option>`;
103 optHtml
+= '</select>';
105 for (let check
of Rules
.Options
.check
) {
107 <label for="var_${check.variable}">${check.label}</label>
108 <input id="var_${check.variable}"
110 if (check
.defaut
) optHtml
+= 'checked="true"';
113 if (Rules
.Options
.styles
.length
>= 1) optHtml
+= '<p class="words">';
114 for (let style
of Rules
.Options
.styles
) {
116 <span class="word" onClick="toggleStyle(event, '${style}')">
120 if (Rules
.Options
.styles
.length
>= 1) optHtml
+= "</p>";
121 $.getElementById("gameOptions").innerHTML
= optHtml
;
124 function getGameLink() {
125 const vname
= $.getElementById("selectVariant").value
;
126 const color
= $.getElementById("selectColor").value
;
127 for (const select
of $.querySelectorAll("#gameOptions > select")) {
128 let value
= select
.value
;
129 if (select
.attributes
["data-numeric"]) value
= parseInt(value
, 10);
130 options
[ select
.id
.split("_")[1] ] = value
;
132 for (const check
of $.querySelectorAll("#gameOptions > input"))
133 options
[ check
.id
.split("_")[1] ] = check
.checked
;
136 player: { sid: sid
, name: localStorage
.getItem("name"), color: color
},
141 const fillGameInfos
= (gameInfos
, oppIndex
) => {
142 fetch(`/variants/${gameInfos.vname}/rules.html`)
143 .then(res
=> res
.text())
147 <strong>${gameInfos.vdisp}</strong>
148 <span>vs. ${gameInfos.players[oppIndex].name}</span>
153 Object
.entries(gameInfos
.options
).map(opt
=> {
155 '<span class="option">' +
156 (opt
[1] === true ? opt
[0] : `${opt[0]}:${opt[1]}`) +
167 <button onClick="toggleGameInfos()">Back to game</button>`;
168 $.getElementById("gameInfos").innerHTML
= htmlContent
;
175 let socket
, gid
, attempt
= 0;
176 const autoReconnectDelay
= () => {
177 return [100, 200, 500, 1000, 3000, 10000, 30000][Math
.min(attempt
, 6)];
180 function copyClipboard(msg
) { navigator
.clipboard
.writeText(msg
); }
181 function getWhatsApp(msg
) {
182 return `https://api.whatsapp.com/send?text=${encodeURIComponent(msg)}`;
185 const tryResumeGame
= () => {
187 // If a game is found, resume it:
188 if (localStorage
.getItem("gid")) {
189 gid
= localStorage
.getItem("gid");
190 send("getgame", { gid: gid
});
193 // If URL indicates "play with a friend", start game:
194 const hashIdx
= document
.URL
.indexOf('#');
196 const urlParts
= $.URL
.split('#');
198 send("joingame", { gid: gid
, name: localStorage
.getItem("name") });
199 localStorage
.setItem("gid", gid
);
200 history
.replaceState(null, '', urlParts
[0]);
205 const messageCenter
= (msg
) => {
206 const obj
= JSON
.parse(msg
.data
);
210 if (!$.hasFocus()) notifyMe("game");
215 // Game vs. friend just created on server: share link now
216 case "gamecreated": {
217 const link
= `${Params.http_server}/#${obj.gid}`;
218 $.getElementById("gameLink").innerHTML
= `
220 <a href="${getWhatsApp(link)}">WhatsApp</a>
222 <span onClick='copyClipboard("${link}")'>ToClipboard</span>
228 // Game vs. friend joined after 1 minute (try again!)
230 alert("Game no longer available");
232 // Get infos of a running game (already launched)
236 // Tried to resume a game which is now gone:
238 localStorage
.removeItem("gid");
240 // Receive opponent's move:
242 if (!$.hasFocus()) notifyMe("move");
243 vr
.playReceivedMove(obj
.moves
, () => {
244 if (vr
.getCurrentScore(obj
.moves
[obj
.moves
.length
-1]) != "*") {
245 localStorage
.removeItem("gid");
246 setTimeout( () => toggleVisible("gameStopped"), 2000 );
248 else toggleTurnIndicator(true);
251 // Opponent stopped game (draw, abort, resign...)
253 toggleVisible("gameStopped");
254 localStorage
.removeItem("gid");
256 // Opponent cancelled rematch:
258 toggleVisible("newGame");
263 const handleError
= (err
) => {
264 if (err
.code
=== 'ECONNREFUSED') {
265 removeAllListeners();
266 alert("Server refused connection. Please reload page later");
271 const handleClose
= () => {
273 removeAllListeners();
275 }, autoReconnectDelay());
278 const removeAllListeners
= () => {
279 socket
.removeEventListener("open", tryResumeGame
);
280 socket
.removeEventListener("message", messageCenter
);
281 socket
.removeEventListener("error", handleError
);
282 socket
.removeEventListener("close", handleClose
);
285 const connectToWSS
= () => {
287 new WebSocket(`${Params.socket_server}${Params.socket_path}?sid=${sid}`);
288 socket
.addEventListener("open", tryResumeGame
);
289 socket
.addEventListener("message", messageCenter
);
290 socket
.addEventListener("error", handleError
);
291 socket
.addEventListener("close", handleClose
);
296 const send
= (code
, data
) => {
297 socket
.send(JSON
.stringify(Object
.assign({code: code
}, data
)));
303 function toggleTurnIndicator(myTurn
) {
304 let indicator
= $.getElementById("chessboard");
305 if (myTurn
) indicator
.style
.outline
= "thick solid green";
306 else indicator
.style
.outline
= "thick solid lightgrey";
309 function notifyMe(code
) {
310 const doNotify
= () => {
311 // NOTE: empty body (TODO?)
312 new Notification("New " + code
, { vibrate: [200, 100, 200] });
313 new Audio("/assets/new_" + code
+ ".mp3").play();
315 if (Notification
.permission
=== 'granted') doNotify();
316 else if (Notification
.permission
!== 'denied') {
317 Notification
.requestPermission().then((permission
) => {
318 if (permission
=== 'granted') doNotify();
324 const afterPlay
= (move) => { //pack into one moves array, then send
331 if (vr
.turn
!= playerColor
) {
332 toggleTurnIndicator(false);
333 send("newmove", { gid: gid
, moves: curMoves
, fen: vr
.getFen() });
335 const result
= vr
.getCurrentScore(move);
338 toggleVisible("gameStopped");
339 send("gameover", { gid: gid
});
345 // Avoid loading twice the same stylesheet:
346 const conditionalLoadCSS
= (vname
) => {
347 const allIds
= [].slice
.call($.styleSheets
).map(s
=> s
.id
);
348 const newId
= vname
+ "_css";
349 if (!allIds
.includes(newId
)) {
350 $.getElementsByTagName("head")[0].insertAdjacentHTML(
352 `<link id="${newId}" rel="stylesheet"
353 href="/variants/${vname}/style.css"/>`);
358 function initializeGame(obj
) {
359 const options
= obj
.options
|| {};
360 import(`/variants/${obj.vname}/class.js`).then(module
=> {
361 const Rules
= module
.default;
362 conditionalLoadCSS(obj
.vname
);
363 playerColor
= (sid
== obj
.players
[0].sid
? "w" : "b");
364 // Init + remove potential extra DOM elements from a previous game:
365 document
.getElementById("boardContainer").innerHTML
= `
366 <div id="upLeftInfos"
367 onClick="toggleGameInfos()">
368 <img src="/assets/icon_infos.svg"/>
370 <div id="upRightStop"
371 onClick="confirmStopGame()">
372 <img src="/assets/icon_close.svg"/>
374 <div class="resizeable" id="chessboard"></div>`;
376 seed: obj
.seed
, //may be null if FEN already exists (running game)
378 element: "chessboard",
380 afterPlay: afterPlay
,
385 if (playerColor
== "w") send("setfen", {gid: obj
.gid
, fen: vr
.getFen()});
386 localStorage
.setItem("gid", obj
.gid
);
388 const select
= $.getElementById("selectVariant");
390 for (let i
=0; i
<select
.options
.length
; i
++) {
391 if (select
.options
[i
].value
== obj
.vname
) {
392 obj
.vdisp
= select
.options
[i
].text
;
396 fillGameInfos(obj
, playerColor
== "w" ? 1 : 0);
397 if (obj
.randvar
) toggleVisible("gameInfos");
398 else toggleVisible("boardContainer");
399 toggleTurnIndicator(vr
.turn
== playerColor
);
403 function confirmStopGame() {
404 if (confirm("Stop game?")) {
405 send("gameover", { gid: gid
, relay: true });
406 localStorage
.removeItem("gid");
407 toggleVisible("gameStopped");
411 function toggleGameInfos() {
412 if ($.getElementById("gameInfos").style
.display
== "none")
413 toggleVisible("gameInfos");
415 toggleVisible("boardContainer");
416 // Quickfix for the "vanished piece" bug (move played while on game infos)
417 vr
.setupPieces(); //TODO: understand better
421 $.body
.addEventListener("keydown", (e
) => {
422 if (!localStorage
.getItem("gid")) return;
423 if (e
.keyCode
== 27) confirmStopGame();
424 else if (e
.keyCode
== 32) {