trivabble/public/trivabble.js

2652 lines
88 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
*
* @licstart
* This file is part of Trivabble.
*
* Trivabble is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Trivabble is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Trivabble. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: https://trivabble.1s.fr/
* @source: https://gitlab.com/raphj/trivabble/
*/
/*global libD, myConfirm, myAlert, myPrompt, myChoice*/
(function () {
"use strict";
const VERSION = 202005070130;
const Conf = window.TrivabbleConf || {};
const DictionaryList = window.DictionaryList || {};
const BoardList = window.BoardList || {};
function setConf(parameterName, defaultValue) {
if (typeof Conf[parameterName] === "undefined") {
Conf[parameterName] = defaultValue;
} else if (typeof Conf[parameterName] !== typeof defaultValue) {
myAlert("Head's up - configuration " + parameterName + " does not have the right type. It should be a " + (typeof defaultValue) + ", it is a " + (typeof Conf[parameterName]));
throw new Error("Wrong type for configuration " + parameterName + ", expected " + (typeof defaultValue) + ", got " + (typeof Conf[parameterName]));
}
}
function middle(key) {
const setting = getSetting(key);
return setting[Math.floor(setting.length / 2)];
}
setConf("POLLING_DELAY", 2000);
setConf("ENABLE_WEBSOCKETS", true);
setConf("ENABLE_EVENT_SOURCE", true);
setConf("MAX_WEBSOCKET_ERRORS", 1);
setConf("APP_PATH", "");
setConf("API_ENTRY_POINT", Conf.APP_PATH + "/:trivabble");
setConf("ENABLE_MSG_SOUND", true);
setConf("ENABLE_TILE_SOUND", true);
setConf("DOUBLE_TAP_DURATIONS", [650, 1100, 1800, 3000, 5000]);
setConf("DOUBLE_TAP_DURATION", middle("DOUBLE_TAP_DURATIONS"));
setConf("FLASH_LIGHT_COLOR", "#ee6633");
setConf("FLASH_LIGHT_DURATIONS", [800, 1600, 3200]);
setConf("FLASH_LIGHT_DURATION", middle("FLASH_LIGHT_DURATIONS"));
setConf("PREMIUM_SEVEN_TILES", 50);
setConf("SCORE_LAST_PLAYER", true);
setConf("ENABLE_TIMER", false);
setConf("CELL_CAPTIONS", "dots"); // "clip", "dots", "none", "short"
setConf("ENABLE_CUSTOM_BOARD", false);
function isSetting(key) {
return Object.prototype.hasOwnProperty.call(Conf, key) ||
Object.prototype.hasOwnProperty.call(localStorage, "trivabble" + key);
}
function getSetting(key, defaultValue) {
let type;
let value;
if (Object.prototype.hasOwnProperty.call(Conf, key)) {
// get default value from configuration
value = Conf[key];
type = typeof value;
} else if (Object.prototype.hasOwnProperty.call(SettingsTypes, key)) {
type = SettingsTypes[key];
}
/* try to retrieve value from localstorage */
if (Object.prototype.hasOwnProperty.call(localStorage, "trivabble" + key)) {
value = localStorage.getItem("trivabble" + key);
/* get type from localStorage if no default is set */
if (typeof type === "undefined") {
type = "string";
}
/* cast from string to type */
if (type === "boolean") {
value = (value === "true");
} else if (type === "number") {
value = Number(value);
} else if (type === "object") {
value = JSON.parse(value);
} else if (type === "string") {
value = String(value);
} else {
console.error("Unsupported type");
}
} else if (typeof defaultValue !== "undefined") {
return defaultValue;
}
return value;
}
function unsetSetting(key) {
localStorage.removeItem("trivabble" + key);
}
function setSetting(key, value) {
if (getSetting(key) === value) {
return;
}
let type;
/* try to retrieve type from configuration */
if (Object.prototype.hasOwnProperty.call(SettingsTypes, key)) {
type = SettingsTypes[key];
} else if (Object.prototype.hasOwnProperty.call(Conf, key)) {
type = typeof Conf[key];
}
/* storage value in localstorage */
if (type === typeof value) {
if (type === "object") {
value = JSON.stringify(value);
}
if ((type === "boolean") ||
(type === "number") ||
(type === "object") ||
(type === "string")) {
if (Object.prototype.hasOwnProperty.call(Conf, key) && (Conf[key] === value)) {
if (Object.prototype.hasOwnProperty.call(localStorage, "trivabble" + key)) {
delete localStorage["trivabble" + key];
}
} else {
localStorage.setItem("trivabble" + key, value);
}
} else {
console.error("Unsupported type");
}
} else {
console.error("Incoherent or missing type. See the SettingsTypes object.");
}
}
const SettingsTypes = {
BoardLang: "string",
DisableSpellChecker: "boolean",
GameNumber: "number",
BoardLabel: "string",
Lang: "string",
PlayerName: "string",
SpellCheckerEnabledOnce: "boolean",
TimerGameDate: "number",
TimerTurnDate: "number"
};
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
return s;
};
const trivabble = window.trivabble = {l10n: _};
function format(s, v1, v2, v3) {
return s
.replace(/\{0\}/gu, v1)
.replace(/\{1\}/gu, v2)
.replace(/\{2\}/gu, v3);
}
let board;
let rack;
const boardCells = [];
let scoreOf;
let bag;
let boardLangSelect;
let boardLabelSelect;
const downloadedDictionaries = {};
const boardDef = {nbRows: 0, nbColumns: 0, rackLength: 0};
const playerLetters = [];
let currentPlayer = "";
let audioNotification;
let audioChat;
let chatMessages;
let chatTextarea;
let tablePlayers = {};
let participantPlaceholder;
let participants;
let name;
let blockMove = 0;
let remainingLetters = 0;
let needsRestart = false;
const gameInProgress = false;
let currentTilePlayed = {};
let eventSource = null;
let webSocket = null;
let connectionLostMessage = null;
let boundEventShowConnectionLost = null;
let retryPollingTimeout = 0;
let pollingServer = false;
let connectionStopped = false;
let currentMessageId = 1;
const waitingMsgs = [];
let lastPlayer = null;
let serverVersion = 0;
function mouseDown(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mousedown", fun, false);
}
function mouseUp(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mouseup", fun, false);
}
function mouseMove(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mousemove", fun, false);
}
function setRack(rack) {
for (let i = 0; i < boardDef.rackLength; i++) {
setTileParent(playerLetters[i], rack[i] || "");
}
}
function getFreeRackSpaceIndex() {
for (let i = 0; i < boardDef.rackLength; i++) {
if (!playerLetters[i].getElementsByClassName("tile")[0]) {
return i;
}
}
return -1;
}
function showConnectionLost(msg) {
unbindEventsShowConnectionLost();
if (!connectionLostMessage) {
connectionLostMessage = chatMessage("", msg || _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
connectionLostMessage.classList.add("error");
}
}
function unbindEventsShowConnectionLost() {
if (boundEventShowConnectionLost) {
document.body.removeEventListener("mousemove", boundEventShowConnectionLost);
document.body.removeEventListener("mousedown", boundEventShowConnectionLost);
document.body.removeEventListener("touchstart", boundEventShowConnectionLost);
boundEventShowConnectionLost = null;
}
}
function checkDictionaryExistance() {
const code = document.getElementById("board-lang").value;
const availableLang = Object.prototype.hasOwnProperty.call(DictionaryList, code);
document.getElementById("disable-spell-checker-p").hidden = !availableLang;
document.getElementById("no-spell-checker-p").hidden = availableLang;
if (availableLang && !document.getElementById("disable-spell-checker").checked) {
document.getElementById("check-spelling").hidden = false;
document.getElementById("info-spell-checking").hidden = false;
} else {
document.getElementById("check-spelling").hidden = true;
document.getElementById("info-spell-checking").hidden = true;
}
}
function getDictionary(code, callback, force) {
if (downloadedDictionaries[code]) {
if (downloadedDictionaries[code].length) {
if (callback) {
callback(downloadedDictionaries[code]);
return;
}
}
// FIXME we silently never call the callback here
return;
}
if (!force && !getSetting("SpellCheckerEnabledOnce")) {
myConfirm(
_("Spell checking requires Trivabble to download a dictionary. Do you confirm?"),
function () {
setSetting("SpellCheckerEnabledOnce", true);
getDictionary(code, callback, true);
}
);
return;
}
// Avoid redownloading the dictionary if a download is on its way
downloadedDictionaries[code] = [];
const file = new XMLHttpRequest();
file.open("GET", "dict/" + code + ".dict", true);
file.onreadystatechange = function () {
if (file.readyState === 4) {
if (file.status === 200) {
try {
const dictionary = file.responseText.split("\n");
downloadedDictionaries[code] = dictionary;
callback(dictionary);
return;
} catch (e) {
delete downloadedDictionaries[code];
myAlert(_("Incorrect dictionary file."));
}
} else {
delete downloadedDictionaries[code];
myAlert(_("Can't load dictionary file."));
}
}
};
file.send();
}
function removeElem(elem) {
elem.parentNode.removeChild(elem);
}
function connectionReady() {
pollingReady = true;
retriedImmediately = false;
unbindEventsShowConnectionLost();
if (connectionLostMessage) {
connectionLostMessage.querySelector(".msg-content").textContent = _("The problem is solved, sorry for the inconvenience!");
connectionLostMessage.classList.remove("error");
connectionLostMessage.classList.add("ok");
const removeMessage = removeElem.bind(null, connectionLostMessage);
connectionLostMessage.addEventListener("click", removeMessage);
connectionLostMessage.addEventListener("touch", removeMessage);
connectionLostMessage = null;
}
}
let tileInitCoords = null;
let tileInitMouseCoords = null;
let tileDest = null;
let tileInitDest = null;
let movingTile = null;
let rackBCR = null;
let boardBCR = null;
let bagBCR = null;
let moveCMD = null;
let rackChanged = false;
function getLetter(l) {
const tile = l.getElementsByClassName("tile")[0];
if (!tile) {
return "";
}
return tile.getElementsByClassName("tile-letter")[0].textContent;
}
function isParentOf(p, elem) {
while (elem) {
if (elem === p) {
return true;
}
elem = elem.parentNode;
}
return false;
}
function getParentWithClass(element, className) {
while (element && element !== document.documentElement) {
if (element.classList.contains(className)) {
return element;
}
element = element.parentNode;
}
return null;
}
function cleanupDragTile() {
mouseUp(document, dragTileEnd, true);
mouseMove(document, dragTileMove, true);
tileInitMouseCoords = null;
tileInitCoords = null;
tileInitDest = null;
tileDest = null;
movingTile = null;
boardBCR = null;
bagBCR = null;
rackBCR = null;
moveCMD = null;
}
function dragTileEnd() {
if (movingTile.parentNode === tileInitDest) {
cleanupDragTile();
return;
}
movingTile.style.left = "";
movingTile.style.top = "";
movingTile.style.width = "";
movingTile.style.height = "";
/* Keep track of letter */
if (moveCMD.from === "rack") {
if (moveCMD.to === "board") {
currentTilePlayed[moveCMD.indexTo] = "-";
}
} else {/* moveCMD.from === "board" */
if (moveCMD.to === "board") {
currentTilePlayed[moveCMD.indexTo] = currentTilePlayed[moveCMD.indexFrom];
}
delete currentTilePlayed[moveCMD.indexFrom];
}
if (tileDest === bag) {
moveCMD.to = "bag";
moveCMD.indexTo = -1;
movingTile.parentNode.removeChild(movingTile);
} else if (tileDest) {
tileDest.appendChild(movingTile);
if (isParentOf(board, tileDest)) {
for (const tile of [].slice.call(board.getElementsByClassName("tile-highlight"))) {
tile.classList.remove("tile-highlight");
}
}
} else if (tileInitDest.getElementsByClassName("tile")[0]) {
for (let i = 0; i < boardDef.rackLength; i++) {
if (!playerLetters[i].getElementsByClassName("tile")[0]) {
playerLetters[i].appendChild(movingTile);
break;
}
}
} else {
tileInitDest.appendChild(movingTile);
}
if (tileDest) {
tileDest.classList.remove("tile-target");
const moveRack = {
cmd: "setRack",
rack: playerLetters.map(getLetter)
};
if (moveCMD.to === moveCMD.from) {
if (moveCMD.indexTo === moveCMD.indexFrom) {
if (rackChanged) {
sendCmds([moveRack]);
}
} else if (moveCMD.from === "rack") {
sendCmds([moveRack]);
} else if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
sendCmds([moveCMD]);
}
} else if (moveCMD.to === "rack") {
moveRack.rack[moveCMD.indexTo] = "";
sendCmds([moveRack, moveCMD]);
} else if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
sendCmds([moveCMD]);
}
}
cleanupDragTile();
}
function dragTileMove(e) {
if (movingTile.parentNode !== document.body) {
movingTile.style.left = tileInitCoords.left + window.scrollX + "px";
movingTile.style.top = tileInitCoords.top + window.scrollY + "px";
movingTile.style.width = tileInitCoords.width + "px";
movingTile.style.height = tileInitCoords.height + "px";
document.body.appendChild(movingTile);
}
let newLeft = (tileInitCoords.left + (e.clientX - tileInitMouseCoords.clientX));
let newTop = (tileInitCoords.top + (e.clientY - tileInitMouseCoords.clientY));
movingTile.style.left = (newLeft + window.scrollX) + "px";
movingTile.style.top = (newTop + window.scrollY) + "px";
let newDest = null;
newTop += tileInitCoords.height / 2;
newLeft += tileInitCoords.width / 2;
if (
(newTop > boardBCR.top && newTop < (boardBCR.top + boardBCR.height)) &&
(newLeft > boardBCR.left && newLeft < (boardBCR.left + boardBCR.width))
) {
const rowIndex = Math.floor(
(
(newTop - boardBCR.top) / boardBCR.height
) * board.rows.length
);
if (rowIndex > 0 && rowIndex < board.rows.length - 1) {
const row = board.rows[rowIndex];
const colIndex = Math.floor(
(
(newLeft - boardBCR.left) / boardBCR.width
) * row.cells.length
);
if (colIndex > 0 && colIndex < row.cells.length - 1) {
newDest = row.cells[colIndex].firstChild;
}
}
if (newDest && newDest.getElementsByClassName("tile")[0]) {
newDest = null;
}
if (newDest) {
moveCMD.to = "board";
moveCMD.indexTo = boardCells.indexOf(newDest.parentNode);
}
} else if (
(newTop > rackBCR.top && newTop < (rackBCR.top + rackBCR.height)) &&
(newLeft > rackBCR.left && newLeft < (rackBCR.left + rackBCR.width))
) {
const index = Math.floor(
(
(newLeft - rackBCR.left) / rackBCR.width
) * playerLetters.length
);
newDest = playerLetters[index];
if (newDest && newDest.getElementsByClassName("tile")[0]) {
let i = index + 1;
let tile;
while (playerLetters[i]) {
tile = playerLetters[i].getElementsByClassName("tile")[0];
if (!tile) {
let j = i;
while (j > index) {
rackChanged = true;
playerLetters[j].appendChild(
playerLetters[j - 1].getElementsByClassName("tile")[0]
);
j--;
}
break;
}
i++;
}
if (newDest.getElementsByClassName("tile")[0]) {
i = index - 1;
while (playerLetters[i]) {
tile = playerLetters[i].getElementsByClassName("tile")[0];
if (!tile) {
let j = i;
while (j < index) {
playerLetters[j].appendChild(
playerLetters[j + 1].getElementsByClassName("tile")[0]
);
j++;
}
break;
}
i--;
}
}
}
if (newDest.getElementsByClassName("tile")[0]) {
newDest = null;
} else {
moveCMD.to = "rack";
moveCMD.indexTo = index;
}
} else if (
(newTop > bagBCR.top && newTop < (bagBCR.top + bagBCR.height)) &&
(newLeft > bagBCR.left && newLeft < (bagBCR.left + bagBCR.width))
) {
newDest = bag;
}
if (newDest !== tileDest) {
if (tileDest) {
tileDest.classList.remove("tile-target");
}
if (newDest) {
newDest.classList.add("tile-target");
}
}
tileDest = newDest;
}
function dragTileBegin(e) {
preventDefault(e);
if (blockMove || waitingMsgs.length) {
return;
}
if (!pollingReady) {
showConnectionLost();
return;
}
loadAudio();
rackChanged = false;
movingTile = e.currentTarget;
tileInitMouseCoords = e;
tileInitCoords = movingTile.getBoundingClientRect();
tileInitDest = movingTile.parentNode;
boardBCR = board.getBoundingClientRect();
rackBCR = rack.getBoundingClientRect();
bagBCR = bag.getBoundingClientRect();
let from = null;
let index;
let p = movingTile;
while (p && !from) {
index = boardCells.indexOf(p);
if (index !== -1) {
from = "board";
break;
}
index = playerLetters.indexOf(p);
if (index !== -1) {
from = "rack";
break;
}
p = p.parentNode;
}
if (!from) {
fatalError(new Error("Error: did not find the parent of the moving tile"));
return;
}
moveCMD = {
cmd: "moveLetter",
from: from,
indexFrom: index
};
mouseMove(document, dragTileMove);
mouseUp(document, dragTileEnd);
}
function setLetter(tile, letter, highlight) {
tile.firstChild.textContent = letter;
tile.lastChild.textContent = scoreOf[letter] || "";
if (highlight) {
tile.classList.add("tile-highlight");
if (getSetting("ENABLE_TILE_SOUND")) {
audioNotification.play();
}
} else {
tile.classList.remove("tile-highlight");
}
}
function preventDefault(e) {
e.preventDefault();
e.stopPropagation();
return false;
}
function touchStartTilePreventDefault(e) {
flashLightTouch(e);
return preventDefault(e);
}
function makeLetter(letter, highlight, noEvents) {
const tile = document.createElement("span");
tile.translate = false;
tile.className = "tile";
tile.appendChild(document.createElement("span"));
tile.lastChild.className = "tile-letter";
tile.appendChild(document.createElement("span"));
tile.lastChild.className = "tile-score";
if (!noEvents) {
mouseDown(tile, dragTileBegin);
tile.addEventListener("contextmenu", preventDefault);
tile.addEventListener("touchstart", touchStartTilePreventDefault);
tile.addEventListener("touchmove", preventDefault);
}
setLetter(tile, letter, highlight);
return tile;
}
function setTileParent(p, letter, highlight) {
let tile = p.getElementsByClassName("tile")[0];
if (tile) {
if (letter) {
setLetter(tile, letter, highlight);
} else {
p.removeChild(tile);
}
} else if (letter) {
tile = makeLetter(letter, highlight);
p.appendChild(tile);
}
}
function setCell(index, letter, highlight) {
if (lastPlayer) {
if (letter) {
currentTilePlayed[index] = letter;
} else {
delete currentTilePlayed[index];
}
}
setTileParent(boardCells[index].getElementsByClassName("tile-placeholder")[0], letter, highlight);
}
function setBoard(board) {
for (let i = 0; i < boardDef.nbRows * boardDef.nbColumns; i++) {
setCell(i, board[i]);
}
}
function set(key, value) {
switch (key) {
case "playerName":
name.textContent = value;
setSetting("PlayerName", value);
break;
case "gameNumber":
document.getElementById("number").textContent = value;
setSetting("GameNumber", value);
break;
case "boardLang":
document.getElementById("board-lang").value = value;
setSetting("BoardLang", value);
checkDictionaryExistance(value);
break;
case "boardLabel":
document.getElementById("board-label").value = value;
setSetting("BoardLabel", value);
[boardDef.nbRows, boardDef.nbColumns, boardDef.rackLength] =
(typeof value === "undefined") ? [0, 0, 0] : value
.match(/[0-9]*/gu)
.filter(function (x) {return (x.length !== 0);})
.map(function (x) {return parseInt(x);});
initBoard();
break;
}
}
function checkGameInProgress(f) {
if (!gameInProgress) {
return f();
}
myConfirm(
_("Your game is not over. Are you sure you want to leave now?"),
function () {
myAlert(
format(
_("You are about to leave the current game. To recover it, please note its number: {0}"),
getSetting("GameNumber")
),
f
);
}
);
}
let pollingReady = false;
let retriedImmediately = false;
function forceReload(msg) {
needsRestart = true;
myConfirm(
msg,
function () {
location.reload();
}, function () {
blockMove = 1000;
document.getElementById("panel").appendChild(document.createElement("a"));
document.getElementById("panel").lastChild.href = "#";
document.getElementById("panel").lastChild.onclick = location.reload.bind(location);
document.getElementById("panel").lastChild.textContent = _("To continue playing, click here");
}
);
}
function fatalError(e) {
if (!needsRestart) {
// No need to show the user that a problem just happened if a message asking to restart has already been shown.
forceReload(_("Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."));
}
throw (e || new Error("interrupting the game because of a fatal error"));
}
function jsonError(json, e) {
console.error("An error ocurred while parsing this JSON message:", json);
fatalError(e);
}
function setRackCell(index, letter) {
setTileParent(playerLetters[index], letter);
}
function blinkTransitionEnd(element) {
element.removeEventListener("animationend", element._te, false);
element.classList.remove("blink");
}
function blink(element) {
element._te = blinkTransitionEnd.bind(null, element);
element.addEventListener("animationend", element._te, false);
setTimeout(element._te, 5000);
element.classList.add("blink");
}
function chatMessage(sender, content) {
chatMessages.style.width = chatTextarea.offsetWidth + "px"; // HACK to prevent the chat box from getting too big
const msgDom = document.createElement("div");
msgDom.className = "msg";
if (sender) {
msgDom.appendChild(document.createElement("span"));
msgDom.lastChild.className = "msg-sender";
msgDom.lastChild.textContent = _("{0}: ").replace("{0}", sender);
}
if (content instanceof Element) {
msgDom.appendChild(content);
} else {
msgDom.appendChild(document.createElement("span"));
msgDom.lastChild.className = "msg-content";
msgDom.lastChild.textContent = content;
}
chatMessages.appendChild(msgDom);
chatMessages.scrollTop = chatMessages.scrollHeight;
if (sender && sender !== getSetting("PlayerName")) {
if (getSetting("ENABLE_MSG_SOUND")) {
audioChat.play();
}
blink(chatMessages);
}
return msgDom;
}
function infoMessage(info) {
const content = document.createElement("div");
content.appendChild(document.createElement("span"));
content.lastChild.className = "info";
content.lastChild.textContent = info || "";
chatMessage("", content);
return content;
}
function handleChatMessage(msg) {
if (msg.specialMsg) {
switch (msg.specialMsg.type) {
case "rack": {
const content = infoMessage(format(_("{0} shows their rack:"), msg.sender));
const letters = document.createElement("div");
letters.className = "tile-list";
for (let i = 0; i < msg.specialMsg.rack.length; i++) {
letters.appendChild(makeLetter(msg.specialMsg.rack[i], false, true));
}
content.appendChild(letters);
return;
}
case "changeBoardDef": {
const newLang = boardLangSelect.querySelector("[value=" + msg.specialMsg.newBoardLang + "]").textContent;
const newLabel = msg.specialMsg.newBoardLabel;
infoMessage(
(msg.sender === getSetting("PlayerName"))
? format(_("You changed the board to {0} for language {1}"), newLabel, newLang)
: format(_("{0} changed the board to {1} for language {2}"), msg.sender, newLabel, newLang)
);
if (newLabel !== "15x15-7") {
infoMessage(_("Caution, this feature is experimental; any feedback will be appreciated"));
}
break;
}
case "highlightCell": {
localHighlightCell(msg.specialMsg.cell);
infoMessage(msg.msg);
break;
}
}
}
chatMessage(msg.sender, msg.content);
}
function refreshCurrentPlayer() {
const row = tablePlayers[currentPlayer];
if (currentPlayer && row && !row.classList.contains("current-player")) {
row.classList.add("current-player");
blink(row.firstChild);
}
}
function setCurrentPlayer(player) {
if (currentPlayer && tablePlayers[currentPlayer]) {
tablePlayers[currentPlayer].classList.remove("current-player");
setSetting("TimerTurnDate", timerDate());
}
currentPlayer = player;
refreshCurrentPlayer();
}
function getScoreCell(playerName) {
return tablePlayers[playerName].childNodes[2].childNodes[0];
}
function setPlayers(players) {
if (participantPlaceholder) {
participantPlaceholder.parentNode.removeChild(participantPlaceholder);
participantPlaceholder = null;
}
for (let i = 0; i < players.length; i++) {
const player = players[i];
const playerName = players[i].player;
if (!tablePlayers[playerName]) {
let before = null;
for (let j = 1; j < participants.rows.length; j++) {
if (playerName < participants.rows[j].cells[0].textContent) {
before = participants.rows[j];
break;
}
}
const row = document.createElement("tr");
participants.insertBefore(row, before);
row.appendChild(document.createElement("td"));
row.lastChild.textContent = playerName;
row.appendChild(document.createElement("td"));
row.appendChild(document.createElement("td"));
row.lastChild.className = "score-cell";
row.lastChild.appendChild(document.createElement("span"));
row.lastChild.lastChild.onclick = (
function (row) {
return function () {
if (!pollingReady) {
showConnectionLost();
return;
}
myPrompt(
format(
_("Enter the new score of {0}:"),
row.firstChild.textContent
),
function (res) {
res = parseInt(res);
if (!isNaN(res)) {
sendCmds([{
cmd: "score",
player: row.firstChild.textContent,
score: res
}]);
}
},
row.lastChild.firstChild.textContent,
{type: "number"}
);
};
}
)(row);
row.lastChild.appendChild(document.createTextNode(" "));
row.lastChild.appendChild(document.createElement("button"));
row.lastChild.lastChild.textContent = "+";
row.lastChild.lastChild.onclick = (
function (row) {
return function () {
if (!pollingReady) {
showConnectionLost();
return;
}
myPrompt(
format(
_("Enter the score to add to {0}:"),
row.firstChild.textContent
),
function (res) {
res = parseInt(res);
if (!isNaN(res)) {
sendCmds([{
cmd: "score",
player: row.firstChild.textContent,
score: parseInt(row.childNodes[2].childNodes[0].textContent) + res
}]);
}
},
row.lastChild.firstChild.textContent,
{type: "number"}
);
};
}
)(row);
row.appendChild(document.createElement("td"));
row.lastChild.className = "turn-cell";
row.lastChild.appendChild(document.createElement("button"));
const img = new Image();
img.src = "baton.svg";
row.lastChild.lastChild.appendChild(img);
row.lastChild.lastChild.onclick = (function (playerName) {
sendCmds([{
cmd: "currentPlayer",
player: playerName === currentPlayer ? "" : playerName
}]);
}).bind(null, playerName);
tablePlayers[playerName] = row;
}
if (Object.prototype.hasOwnProperty.call(player, "score")) {
const scoreCell = getScoreCell(playerName);
if (parseInt(scoreCell.textContent) !== player.score) {
currentTilePlayed = {};
}
scoreCell.textContent = player.score;
blink(scoreCell);
}
if (Object.prototype.hasOwnProperty.call(player, "rackCount")) {
const countCell = tablePlayers[playerName].childNodes[1];
countCell.textContent = player.rackCount;
blink(countCell);
}
}
refreshCurrentPlayer();
}
function handleLastPlayer(data) {
if (data.player) {
if (lastPlayer !== data.player) {
currentTilePlayed = {};
}
lastPlayer = data.player;
}
}
function applyAction(data) {
switch (data.action) {
case "pushBag": //TODO
break;
case "popBag": //TODO
break;
case "reset":
setTimerDate(true);
tablePlayers = {};
while (participants.rows[1]) {
participants.removeChild(participants.rows[1]);
}
sendCmds([{cmd: "hello"}]);
lastPlayer = null;
currentTilePlayed = {};
break;
case "moveLetter":
handleLastPlayer(data);
if (data.from === "board") {
setCell(data.indexFrom, "");
} else if (data.from === "rack") {
setRackCell(data.indexFrom, "");
}
if (data.to === "board") {
setCell(
data.indexTo,
data.letter,
data.player && data.player !== getSetting("PlayerName")
);
} else if (data.to === "rack") {
setRackCell(data.indexTo, data.letter);
}
break;
case "setCell":
handleLastPlayer(data);
setCell(
data.indexTo,
data.letter,
data.player && data.player !== getSetting("PlayerName")
);
if ((data.letter !== "") && (currentTilePlayed[data.indexTo] === "-")) {
currentTilePlayed[data.indexTo] = data.letter;
}
break;
case "setRackCell":
setRackCell(data.indexTo, data.letter);
}
}
function handleReceivedData(data) {
if (Array.isArray(data)) {
data.forEach(handleReceivedData);
return;
}
if (data.id) {
const pos = waitingMsgs.indexOf(data.id);
if (pos !== -1) {
waitingMsgs.splice(pos, 1);
}
}
if (data.version) {
serverVersion = data.version;
}
if (data.pleaseRestart) {
forceReload(_("Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"));
}
if (data.error) {
fatalError(new Error("Error from the server: " + data.error + " - " + data.reason));
}
if (data.stopping) {
stopConnection();
retryPolling(
(typeof data.stopping === "number" && data.stopping > 0)
? data.stopping
: 0
);
}
if (data.msg) {
handleChatMessage(data.msg);
}
if (typeof data.currentPlayer === "string") {
setCurrentPlayer(data.currentPlayer);
}
if (data.players) {
setPlayers(data.players);
}
if (data.playerName) {
set("playerName", data.playerName);
}
if (data.gameNumber) {
set("gameNumber", Number(data.gameNumber));
}
if (data.availableBoardLangs) {
setAvailableBoardLangs(data.availableBoardLangs);
}
if (data.boardLang) {
set("boardLang", data.boardLang);
(boardLangSelect || {}).value = data.boardLang;
}
if (data.boardLabel) {
if (Object.keys(BoardList).indexOf(data.boardLabel) === -1) {
myAlert("Can't find board '" + data.boardLabel + "'. Change board or start a new game.");
(boardLabelSelect || {}).value = "";
} else {
set("boardLabel", data.boardLabel);
(boardLabelSelect || {}).value = data.boardLabel;
}
}
if (data.letterValues) {
scoreOf = data.letterValues;
}
if (data.board) {
setBoard(data.board);
}
if (typeof data.remainingLetters === "number") {
remainingLetters = data.remainingLetters;
document.getElementById("remaining-letters").textContent = data.remainingLetters;
}
if (data.rack) {
setRack(data.rack);
}
if (data.action) {
applyAction(data);
}
}
function retryPolling(delay) {
if (needsRestart) {
return;
}
pollingReady = false;
if (!boundEventShowConnectionLost) {
boundEventShowConnectionLost = showConnectionLost.bind(null, null);
document.body.addEventListener("mousemove", boundEventShowConnectionLost);
document.body.addEventListener("mousedown", boundEventShowConnectionLost);
document.body.addEventListener("touchstart", boundEventShowConnectionLost);
}
if (retryPollingTimeout) {
return;
}
if (delay || retriedImmediately) {
retryPollingTimeout = setTimeout(
function () {
connectionStopped = false;
pollingServer = false;
startConnection();
retryPollingTimeout = 0;
},
delay || getSetting("POLLING_DELAY")
);
} else {
retriedImmediately = true;
connectionStopped = false;
pollingServer = false;
startConnection();
}
}
function parseAndHandleReceivedData(r) {
let m;
try {
m = JSON.parse(r);
} catch (e) {
jsonError(r, e);
}
try {
handleReceivedData(m);
} catch (e) {
fatalError(e);
}
}
function closeConnections() {
if (eventSource) {
eventSource.onerror = null;
eventSource.close();
eventSource = null;
}
if (webSocket) {
webSocket.onclose = null;
webSocket.onerror = null;
webSocket.close();
webSocket = null;
}
}
function stopConnection() {
closeConnections();
if (retryPollingTimeout) {
clearTimeout(retryPollingTimeout);
retryPollingTimeout = 0;
}
connectionStopped = true;
pollingServer = false;
pollingReady = false;
}
let blacklistWebsockets = false;
let webSocketErrors = 0;
function bindConnectionEvents(connection) {
connection.onopen = connectionReady;
connection.onmessage = function (e) {
if (connection === webSocket) {
webSocketErrors = 0;
}
connectionReady();
parseAndHandleReceivedData(e.data);
};
connection.onerror = function (e) {
console.error("Connection error:", e);
if (connection === eventSource) {
retryPolling();
} else {
webSocketErrors++;
if (webSocketErrors > getSetting("MAX_WEBSOCKET_ERRORS")) {
blacklistWebsockets = true;
}
}
};
if (connection === webSocket) {
connection.onclose = function (e) {
webSocket = null;
if (e.code === 1002 || e.code === 1003) {
// CLOSE_PROTOCOL_ERROR or CLOSE_UNSUPPORTED
blacklistWebsockets = true;
pollServerWithEventSource();
return;
}
retryPolling();
};
}
}
function canConnect() {
return !pollingServer && !needsRestart && !connectionStopped && navigator.onLine !== false;
// DO NOT change navigator.onLine !== false by navigator.onLine === true
// This would block connection on a browser not supporting this property
}
function pollServerWithEventSource() {
if (canConnect() && getSetting("ENABLE_EVENT_SOURCE") && window.EventSource) {
closeConnections();
pollingServer = true;
eventSource = new EventSource(getSetting("API_ENTRY_POINT") + "/sse/" + JSON.stringify(cmdsWithContext()));
bindConnectionEvents(eventSource);
return;
}
pollServerWithXHR();
}
function pollServerWithWebSocket() {
if (canConnect() && getSetting("ENABLE_WEBSOCKETS") && !blacklistWebsockets && window.WebSocket) {
closeConnections();
pollingServer = true;
webSocket = new WebSocket(
(window.location.protocol === "http:" ? "ws://" : "wss://") +
window.location.host +
getSetting("API_ENTRY_POINT") + "/ws/" +
JSON.stringify(cmdsWithContext())
);
bindConnectionEvents(webSocket);
return;
}
pollServerWithEventSource();
}
function xhrRequest(data, onreadystatechange) {
const xhr = new XMLHttpRequest();
xhr.open("POST", getSetting("API_ENTRY_POINT"), true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(JSON.stringify(data));
xhr.onreadystatechange = onreadystatechange.bind(null, xhr);
}
function pollServerWithXHR() {
if (!canConnect()) {
return;
}
pollingServer = true;
let currentIndex = 0;
let expectedLength = 0;
xhrRequest(cmdsWithContext(), function (xhr) {
if (xhr.readyState === 2) {
connectionReady();
} else if (xhr.readyState === 3) {
connectionReady();
while (true) {
if (!expectedLength) {
let i = currentIndex;
while (i < xhr.responseText.length) {
if ("0123456789".indexOf(xhr.responseText.charAt(i)) === -1) {
expectedLength = parseInt(xhr.responseText.substring(currentIndex, i));
currentIndex = i;
break;
}
++i;
}
}
if (expectedLength && (xhr.responseText.length >= currentIndex + expectedLength)) {
const end = currentIndex + expectedLength;
let msgs;
try {
msgs = JSON.parse(
xhr.responseText.substring(
currentIndex,
end
)
);
currentIndex = end;
expectedLength = 0;
} catch (e) {
jsonError(xhr.responseText.substring(
currentIndex,
end
), e);
}
handleReceivedData(msgs);
} else {
break;
}
}
} else if (xhr.readyState === 4) {
startConnection();
}
});
}
function startConnection() {
pollServerWithWebSocket();
}
function sendXHR(cmds) {
if (needsRestart) {
return;
}
blockMove++;
xhrRequest(cmdsWithContext(cmds), function (xhr) {
if (xhr.readyState === 4) {
if (xhr.status === 0 || xhr.status >= 300) {
setTimeout(sendCmds.bind(null, cmds), getSetting("POLLING_DELAY"));
return;
}
parseAndHandleReceivedData(xhr.responseText);
blockMove--;
if (!pollingReady) {
startConnection();
}
}
});
}
function cmdsWithContext(cmds) {
return {
gameNumber: getSetting("GameNumber") || "",
playerName: getSetting("PlayerName"),
boardLang: getSetting("BoardLang"),
boardLabel: getSetting("BoardLabel"),
bagFactor: BoardList[getSetting("BoardLabel")].factor,
version: VERSION,
cmds: cmds
};
}
function sendCmds(cmds) {
if (webSocket && webSocket.readyState === 1) {
if (serverVersion >= 202004281800) {
for (const cmd of cmds) {
waitingMsgs.push(currentMessageId);
cmd.id = currentMessageId++;
}
}
webSocket.send(JSON.stringify(cmds));
return;
}
if (!pollingReady) {
startConnection();
setTimeout(sendCmds, getSetting("POLLING_DELAY"), cmds);
return;
}
sendXHR(cmds);
}
function joinGame() {
checkGameInProgress(
function () {
myPrompt(
_("To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.").replace("{0}", getSetting("GameNumber")),
function (n) {
n = parseInt(n);
if (isNaN(n)) {
myAlert(_("It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."));
} else {
setSetting("GameNumber", n);
location.reload();
}
}
);
}
);
}
const specialTypesText = {
doubleLetter: "Double\nLetter",
doubleWord: "Double\nWord",
tripleLetter: "Triple\nLetter",
tripleWord: "Triple\nWord"
};
const specialTypesShortText = {
doubleLetter: "DL",
doubleWord: "DW",
tripleLetter: "TL",
tripleWord: "TW"
};
function specialCell(type, cell) {
cell.firstChild.appendChild(document.createElement("span"));
cell.classList.add("special-cell");
cell.classList.add("special-cell-" + type);
cell.lastChild.lastChild.longText = cell.title = _(specialTypesText[type]);
cell.lastChild.lastChild.shortText = _(specialTypesShortText[type]);
cell.lastChild.lastChild.className = "special-cell-label";
}
function changeName() {
myPrompt(
_("To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."),
function (newName) {
if (newName && newName.trim()) {
setSetting("PlayerName", newName.trim());
name.textContent = getSetting("PlayerName");
}
},
getSetting("PlayerNamer")
);
}
function startGame(number) {
if (number) {
setSetting("GameNumber", number);
}
startConnection();
}
let audioTileLoaded = false;
let audioMsgLoaded = false;
function loadAudio() {
if (!audioTileLoaded && getSetting("ENABLE_TILE_SOUND")) {
audioTileLoaded = true;
audioNotification.load();
}
if (!audioMsgLoaded && getSetting("ENABLE_MSG_SOUND")) {
audioMsgLoaded = true;
audioChat.load();
}
}
function bagClicked(e) {
preventDefault(e);
if (blockMove || waitingMsgs.length) {
return;
}
if (!remainingLetters) {
myAlert(_("You cannot take another tile: the bag is empty."));
return;
}
loadAudio();
if (!pollingReady) {
showConnectionLost();
return;
}
const index = getFreeRackSpaceIndex();
if (index === -1) {
myAlert(_("You cannot take another tile: your rack is full."));
} else {
sendCmds(
[{cmd: "moveLetter", from: "bag", to: "rack", indexTo: index}]
);
}
}
function clearGame() {
myConfirm(
_("Are you sure you want to put all the tiles back in the bag (in order to play another game)?"),
function () {
sendCmds([{cmd: "resetGame"}]);
}
);
}
function onChangeBoardLang() {
const code = document.getElementById("board-lang").value;
const lang = boardLangSelect.selectedOptions[0].textContent;
myConfirm(
format(_("Are you sure you want to change board to '{0}'? This will put all the tiles back in the bag and start another game."), _(lang)),
function () {
sendCmds([{cmd: "changeBoard", lang: code, label: getSetting("BoardLabel"), factor: BoardList[getSetting("BoardLabel")].factor}]);
},
function () {
boardLangSelect.value = getSetting("BoardLang");
}
);
}
function onChangeBoardLabel() {
const label = document.getElementById("board-label").value;
myConfirm(
format(_("Are you sure you want to change board to {0}? This will put all the tiles back in the bag and start another game."), label),
function () {
sendCmds([{cmd: "changeBoard", lang: getSetting("BoardLang"), label: label, factor: BoardList[label].factor}]);
},
function () {
boardLabelSelect.value = getSetting("BoardLabel");
}
);
}
function clearRack() {
myConfirm(
_("Are you sure you want to put all your tiles back in the bag?"),
function () {
const cmds = [];
for (let i = 0; i < playerLetters.length; i++) {
if (playerLetters[i].getElementsByClassName("tile")[0]) {
cmds.push({
cmd: "moveLetter",
from: "rack",
indexFrom: i,
to: "bag",
indexTo: -1
});
}
}
sendCmds(cmds);
setRack("");
}
);
}
function showRack() {
myConfirm(
_("Are you sure you want to show your rack to everybody?"),
function () {
const letters = playerLetters.map(
function (tilePlaceholer) {
return tilePlaceholer.querySelector(".tile");
}
).filter(function (tile) {
return tile !== null;
});
sendCmds([{
cmd: "msg",
msg: _("Here is my rack:") + " " + letters.map(
function (tile) {
return (
tile.textContent === ""
? _("joker")
: tile.querySelector(".tile-letter").textContent
);
}
).join(", "),
specialMsg: {
type: "rack",
rack: letters.map(
function (tile) {
return tile.querySelector(".tile-letter").textContent;
}
)
}
}]);
}
);
}
function getLetterFromBoard(row, col) {
if ((row < 0) || (row >= boardDef.nbRows) || (col < 0) || (col >= boardDef.nbColumns)) {
return "";
}
try {
const index = (row * boardDef.nbColumns) + col;
const tilePlaceholder = boardCells[index].getElementsByClassName("tile-placeholder")[0];
const tile = tilePlaceholder.getElementsByClassName("tile")[0];
return tile.firstChild.textContent;
} catch (e) {
return "";
}
}
function searchWordInLine(row, col, incRow, incCol) {
const initLetter = getLetterFromBoard(row, col);
/* Look for first letter in line */
while (getLetterFromBoard(row - incRow, col - incCol) !== "") {
row -= incRow;
col -= incCol;
}
/* Look for last letter in line */
const word = {letters: "", row: row, col: col, incRow: incRow, incCol: incCol};
let letter;
while ((letter = getLetterFromBoard(row, col)) !== "") {
word.letters += letter;
row += incRow;
col += incCol;
}
return (word.letters === initLetter) ? null : word;
}
function isNewWord(newWord, words) {
if (newWord === null) {
return false;
}
for (const k of Object.keys(words)) {
if ((words[k].letters === newWord.letters) &&
(words[k].row === newWord.row) &&
(words[k].col === newWord.col) &&
(words[k].incRow === newWord.incRow) &&
(words[k].incCol === newWord.incCol)) {
return false;
}
}
return true;
}
function searchNewWords() {
const words = [];
for (const i of Object.keys(currentTilePlayed)) {
/* Get board position */
const row = Math.floor(i / boardDef.nbColumns);
const col = i % boardDef.nbColumns;
/* Look for word in column */
const newWordInCol = searchWordInLine(row, col, 1, 0);
if (isNewWord(newWordInCol, words)) {
words.push(newWordInCol);
}
/* Look for word in row */
const newWordInRow = searchWordInLine(row, col, 0, 1);
if (isNewWord(newWordInRow, words)) {
words.push(newWordInRow);
}
/* Should we look for word in diag? */
/* If nothing found, it's a one letter word */
if ((newWordInCol === null) && (newWordInRow === null)) {
words.push({letters: getLetterFromBoard(row, col), row: row, col: col, incRow: 0, incCol: 0});
}
}
return words;
}
function modifyTileClass(word, value, action) {
/* action: true for add, false for remove */
if (action) {
/* only for new words */
for (let l = 0; l < word.letters.length; l++) {
const index = ((word.row + (l * word.incRow)) * boardDef.nbColumns) + word.col + (l * word.incCol);
const tilePlaceholder = boardCells[index].getElementsByClassName("tile-placeholder")[0];
const tile = tilePlaceholder.getElementsByClassName("tile")[0];
tile.classList.add(value);
}
} else {
/* tiles on board */
for (let index = 0; index < boardDef.nbRows * boardDef.nbColumns; index++) {
const tilePlaceholder = boardCells[index].getElementsByClassName("tile-placeholder")[0];
const tile = tilePlaceholder.getElementsByClassName("tile")[0];
if (typeof tile !== "undefined") {
tile.classList.remove(value);
}
}
/* tiles on rack */
for (const tilePlaceholder of rack.getElementsByClassName("tile-placeholder")) {
const tile = tilePlaceholder.getElementsByClassName("tile")[0];
if (typeof tile !== "undefined") {
tile.classList.remove(value);
}
}
/* tile on the move */
if (movingTile !== null) {
movingTile.classList.remove(value);
}
}
}
function checkSpellingClicked() {
if (getSetting("SpellCheckerEnabledOnce") === "false") {
return;
}
getDictionary(getSetting("BoardLang"), checkSpelling);
}
function checkSpelling(dictionary) {
const newWords = searchNewWords();
if (newWords.length === 0) {
myAlert(_("No new word found"));
return;
}
/* Check if words are in dictionary */
let words = [];
let badWords = [];
for (const k of Object.keys(newWords)) {
const currentWord = newWords[k].letters.replace(/ /gu, "?");
if (words.indexOf(currentWord) === -1) {
words.push(currentWord);
}
modifyTileClass(newWords[k], "tile-spelling-ok", true);
const pattern = "^" + newWords[k].letters.replace(/ /gu, ".") + "$";
const find = dictionary.find(function (word) {
return word.match(pattern);
});
if (!find) {
if (badWords.indexOf(currentWord) === -1) {
badWords.push(currentWord);
}
modifyTileClass(newWords[k], "tile-spelling-nok", true);
}
}
setTimeout(
function () {
modifyTileClass(null, "tile-spelling-ok", false);
modifyTileClass(null, "tile-spelling-nok", false);
}, 5000);
words = words.join(", ").trim();
badWords = badWords.join(", ").trim();
/* Advice on erroneous spelling */
if (badWords.length > 0) {
const msg = chatMessage("", format(_("Incorrect spelling for {0} from {1}"), badWords, words));
msg.classList.add("error");
} else {
chatMessage("", format(_("All words are correct: {0}"), words));
}
}
function getRowIndex(tr) {
// tr.rowIndex is -1 on WebKit-based browsers in Debian Jessie, hence this function
return [].indexOf.call(tr.parentNode.rows, tr);
}
function scoreWords() {
let totalScore = 0;
const newWords = searchNewWords();
for (const k of Object.keys(newWords)) {
let wordFactor = 1;
let wordScore = 0;
const word = newWords[k];
for (let l = 0; l < word.letters.length; l++) {
const index = ((word.row + (l * word.incRow)) * boardDef.nbColumns) + word.col + (l * word.incCol);
/* Letter score */
const tilePlaceholder = boardCells[index].getElementsByClassName("tile-placeholder")[0];
const tile = tilePlaceholder.getElementsByClassName("tile")[0];
const letterScore = tile.lastChild.textContent;
let letterFactor = 1;
/* Is a freshly played letter? */
if (currentTilePlayed[index] === word.letters[l]) {
/* Letter factor */
if (boardCells[index].classList.contains("special-cell-doubleLetter")) {
letterFactor = 2;
} else if (boardCells[index].classList.contains("special-cell-tripleLetter")) {
letterFactor = 3;
}
/* Word factor */
if (boardCells[index].classList.contains("special-cell-doubleWord")) {
wordFactor *= 2;
} else if (boardCells[index].classList.contains("special-cell-tripleWord")) {
wordFactor *= 3;
}
}
wordScore += letterScore * letterFactor;
}
totalScore += wordScore * wordFactor;
}
if (!totalScore) {
myAlert(_("There are no points to add."));
return;
}
/* Check for trivabble (the premium score) */
if (Object.keys(currentTilePlayed).length === boardDef.rackLength) {
totalScore += getSetting("PREMIUM_SEVEN_TILES");
}
/* Ѕcore last player or the one who pressed the button */
const playerName = getSetting("SCORE_LAST_PLAYER") ? lastPlayer : getSetting("PlayerName");
myChoice(
format(_("{0} points will be added to:"), totalScore), {
choices: Object.keys(tablePlayers),
dispositionInline: true
},
function (name) {
if (!name) {
return;
}
currentTilePlayed = {};
sendCmds([{
cmd: "score",
player: name,
score: parseInt(getScoreCell(name).textContent) + totalScore
}]);
},
playerName || lastPlayer || currentPlayer || getSetting("PlayerName")
);
}
function triggerFlashLight(cell) {
if (!cell) {
return;
}
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const col = cell.cellIndex - 1;
const row = getRowIndex(cell.parentNode) - 1;
sendCmds([{
cmd: "msg",
msg: _("Look at:") + " " + letters[row] + (cell.cellIndex + 1),
specialMsg: {
type: "highlightCell",
cell: (row * boardDef.nbColumns) + col
}
}]);
}
function getCellFromEvent(e) {
const tilePlaceholder = getParentWithClass(e.target, "tile-placeholder");
if (tilePlaceholder) {
const cell = tilePlaceholder.parentNode;
if (cell.cellIndex && getRowIndex(cell.parentNode)) {
return cell;
}
}
return null;
}
function flashLightDblClick(e) {
triggerFlashLight(getCellFromEvent(e));
}
function flashLightTouch(e) {
const cell = getCellFromEvent(e);
if (!cell) {
return;
}
if ((flashLightTouch.last !== cell) || (Date.now() - flashLightTouch.date > getSetting("DOUBLE_TAP_DURATION"))) {
flashLightTouch.last = cell;
flashLightTouch.date = Date.now();
return;
}
triggerFlashLight(cell);
}
function localHighlightCell(id) {
const placeholder = boardCells[id].getElementsByClassName("tile-placeholder")[0];
placeholder.classList.add("flash-light-highlighting");
placeholder.classList.add("flash-light-highlight");
const sixthFlashLightDuration = getSetting("FLASH_LIGHT_DURATION") / 6;
function on() {
placeholder.classList.add("flash-light-highlight");
}
function off() {
placeholder.classList.remove("flash-light-highlight");
}
setTimeout(off, sixthFlashLightDuration);
setTimeout(on, sixthFlashLightDuration * 2);
setTimeout(off, sixthFlashLightDuration * 3);
setTimeout(on, sixthFlashLightDuration * 4);
setTimeout(off, sixthFlashLightDuration * 5);
setTimeout(
function () {
placeholder.classList.remove("flash-light-highlighting");
},
sixthFlashLightDuration * 6
);
}
function initChat() {
const btn = document.getElementById("chat-btn");
chatTextarea.onmouseup = function () {
chatMessages.style.width = chatTextarea.offsetWidth + "px";
};
btn.onclick = function () {
if (!pollingReady) {
return;
}
loadAudio();
sendCmds([{
cmd: "msg",
msg: chatTextarea.value
}]);
chatTextarea.value = "";
};
chatTextarea.onkeydown = function (e) {
if (e.keyCode === 13) {
preventDefault(e);
chatTextarea.focus();
btn.onclick();
}
};
}
function initSound() {
audioNotification = new Audio();
audioNotification.preload = "auto";
audioNotification.volume = 1;
let audioSourceOGG = document.createElement("source");
audioSourceOGG.src = "notification.ogg";
let audioSourceMP3 = document.createElement("source");
audioSourceMP3.src = "notification.mp3";
audioNotification.appendChild(audioSourceOGG);
audioNotification.appendChild(audioSourceMP3);
audioChat = new Audio();
audioChat.preload = "auto";
audioChat.volume = 1;
audioSourceOGG = document.createElement("source");
audioSourceOGG.src = "receive.ogg";
audioSourceMP3 = document.createElement("source");
audioSourceMP3.src = "receive.mp3";
audioChat.appendChild(audioSourceOGG);
audioChat.appendChild(audioSourceMP3);
const tilesSound = document.getElementById("tiles-sound");
if (tilesSound) {
/* migration of old settings for tiles sound */
const oldSetting = getSetting("TilesSound");
if (oldSetting) {
setSetting("ENABLE_TILE_SOUND", oldSetting === "true");
delete localStorage.trivabbleTilesSound;
}
tilesSound.checked = getSetting("ENABLE_TILE_SOUND");
tilesSound.onclick = function () {
setSetting("ENABLE_TILE_SOUND", this.checked);
};
}
const msgSound = document.getElementById("msg-sound");
if (msgSound) {
/* migration of old settings for message sound */
const oldSetting = getSetting("MsgSound");
if (oldSetting) {
setSetting("ENABLE_MSG_SOUND", oldSetting === "true");
delete localStorage.trivabbleMsgSound;
}
msgSound.checked = getSetting("ENABLE_MSG_SOUND");
msgSound.onclick = function () {
setSetting("ENABLE_MSG_SOUND", this.checked);
};
}
}
function toggleSpellChecker(e) {
const disabled = document.getElementById("disable-spell-checker").checked;
checkDictionaryExistance();
if (e) {
setSetting("DisableSpellChecker", disabled);
}
if (disabled) {
unsetSetting("SpellCheckerEnabledOnce");
}
}
function initSpellChecker() {
document.getElementById("disable-spell-checker").checked = getSetting("DisableSpellChecker");
toggleSpellChecker();
}
function translateDuration(x, values) {
if (x < 0) {
return null;
}
if (x < values.length) {
return values[Math.floor(x)];
}
for (let i = 0; i < values.length; i++) {
if (x <= values[i]) {
return i;
}
}
return values.length - 1;
}
function initFlashLight() {
document.body.addEventListener("dblclick", flashLightDblClick);
document.body.addEventListener("touchstart", flashLightTouch);
const doubleTapDuration = document.getElementById("double-tap-duration");
if (doubleTapDuration) {
doubleTapDuration.max = getSetting("DOUBLE_TAP_DURATIONS").length - 1;
doubleTapDuration.value = translateDuration(getSetting("DOUBLE_TAP_DURATION"), getSetting("DOUBLE_TAP_DURATIONS"));
doubleTapDuration.onchange = function () {
setSetting("DOUBLE_TAP_DURATION", translateDuration(
document.getElementById("double-tap-duration").value,
getSetting("DOUBLE_TAP_DURATIONS")
));
};
}
const flashLightDuration = document.getElementById("flash-light-duration");
if (flashLightDuration) {
flashLightDuration.max = getSetting("FLASH_LIGHT_DURATIONS").length - 1;
flashLightDuration.value = translateDuration(getSetting("FLASH_LIGHT_DURATION"), getSetting("FLASH_LIGHT_DURATIONS"));
flashLightDuration.onchange = function () {
setSetting("FLASH_LIGHT_DURATION", translateDuration(
document.getElementById("flash-light-duration").value,
getSetting("FLASH_LIGHT_DURATIONS")
));
};
}
// thx https://stackoverflow.com/a/26633844
if (!(window.CSS && CSS.supports && CSS.supports("color", "var(--v)")) && document.getElementById("flash-light-color-p")) {
document.getElementById("flash-light-color-p").hidden = true;
return;
}
const flashLightColor = document.getElementById("flash-light-color");
if (flashLightColor) {
flashLightColor.value = getSetting("FLASH_LIGHT_COLOR");
flashLightColor.onchange = function () {
const color = document.getElementById("flash-light-color").value;
setSetting("FLASH_LIGHT_COLOR", color);
document.documentElement.style.setProperty("--flash-light-color", color);
};
flashLightColor.onchange();
}
}
function nextHelpMessage() {
if (typeof nextHelpMessage.index === "undefined") {
nextHelpMessage.index = -1;
}
const helpMessages = document.getElementById("help-messages");
if (helpMessages) {
const listOfMessages = helpMessages.getElementsByTagName("p");
if ((nextHelpMessage.index >= 0) && (nextHelpMessage.index < listOfMessages.length)) {
listOfMessages[nextHelpMessage.index].hidden = true;
}
nextHelpMessage.index++;
if (nextHelpMessage.index === listOfMessages.length) {
nextHelpMessage.index = 0;
}
listOfMessages[nextHelpMessage.index].hidden = false;
}
}
function timerString(delta) {
const d = Math.floor(delta / (60 * 60 * 24));
const h = Math.floor((delta % (60 * 60 * 24)) / (60 * 60));
const m = Math.floor((delta % (60 * 60)) / 60);
const s = Math.floor(delta % 60);
const sec = (s < 10) ? "0" + s : s;
const min = (m < 10) ? "0" + m : m;
const hour = (h < 10) ? "0" + h : h;
return ((d > 0) ? format(_("{0}d"), d) + " " : "") +
((h + d > 0) ? format(_("{0}h {1}' {2}''"), (d > 0) ? hour : h, min, sec) : format(_("{0}' {1}''"), m, sec));
}
function timerDate() {
return Math.floor(Date.now() / 1000);
}
function setTimerDate(force) {
const currentTimer = timerDate();
if (force || !isSetting("TimerTurnDate")) {
setSetting("TimerTurnDate", currentTimer);
}
if (force || !isSetting("TimerGameDate")) {
setSetting("TimerGameDate", currentTimer);
}
}
function initTimer() {
setTimerDate();
document.getElementById("enable-timer").onclick = function () {
setTimerState(document.getElementById("enable-timer").checked);
};
setTimerState(getSetting("ENABLE_TIMER"));
}
let timerTimeout = 0;
function updateTimer() {
const currentTimer = timerDate();
document.getElementById("timer-turn").textContent = timerString(currentTimer - getSetting("TimerTurnDate"));
document.getElementById("timer-game").textContent = timerString(currentTimer - getSetting("TimerGameDate"));
}
function setTimerState(enabled) {
setSetting("ENABLE_TIMER", enabled);
if (timerTimeout) {
clearInterval(timerTimeout);
timerTimeout = 0;
}
if (enabled) {
document.getElementById("timer").hidden = false;
document.getElementById("enable-timer").checked = true;
timerTimeout = setInterval(updateTimer, 1000);
updateTimer();
} else {
document.getElementById("timer").hidden = true;
document.getElementById("enable-timer").checked = false;
setTimerDate();
}
}
function initCellCaptions() {
setCellCaptions(getSetting("CELL_CAPTIONS"));
document.getElementById("cell-captions").onchange = function () {
const mode = document.getElementById("cell-captions").value;
setCellCaptions(mode);
setSetting("CELL_CAPTIONS", mode);
};
}
function setCellCaptions(mode) {
document.getElementById("cell-captions").value = mode;
board.classList.remove("special-cell-label-short");
board.classList.remove("special-cell-label-clip");
board.classList.remove("special-cell-label-dots");
board.classList.remove("special-cell-label-none");
for (const cell of [].slice.call(document.getElementsByClassName("special-cell-label"))) {
if (cell.textContent !== "★") {
cell.textContent = (mode === "short") ? cell.shortText : cell.longText;
}
}
board.classList.add("special-cell-label-" + mode);
}
function repromptName(f) {
if (getSetting("PlayerName") && getSetting("PlayerName").trim()) {
f();
} else {
myPrompt(
_("It seems your did not give your name. You need to do it for the game to run properly."),
function (newName) {
if (newName && newName.trim()) {
setSetting("PlayerName", newName.trim());
}
repromptName(f);
}
);
}
}
trivabble.applyL10n = function () {
document.documentElement.lang = libD.lang;
document.documentElement.setAttribute("xml:lang", libD.lang);
for (const node of [].slice.call(document.querySelectorAll("[data-l10n]"))) {
if (node.dataset.l10n === "text-content") {
node.textContent = _(node.textContent.trim());
} else {
node.setAttribute(node.dataset.l10n, _(node.getAttribute(node.dataset.l10n)));
}
}
trivabble.run();
};
function setAvailableBoardLangs(availableBoardLangs) {
if (!boardLangSelect) {
const boardLangSelection = document.getElementById("board-lang-selection");
boardLangSelect = boardLangSelection.querySelector("select");
boardLangSelection.hidden = false;
}
boardLangSelect.textContent = "";
for (const key of Object.keys(availableBoardLangs)) {
boardLangSelect.add(new Option(_(availableBoardLangs[key]), key));
}
Array.prototype.sort.call(boardLangSelect.options, function (a, b) {
return (
(a.textContent > b.textContent)
? 1
: (
(b.textContent > a.textContent)
? -1
: 0
)
);
});
boardLangSelect.value = getSetting("BoardLang");
}
function langSelectionChange(e) {
setSetting("Lang", e.target.value);
location.reload();
}
function showSettings() {
const modal = document.querySelector(".modal");
modal.classList.add("show-modal");
modal.querySelector("button").focus();
}
function hideSettings() {
document.querySelector(".modal").classList.remove("show-modal");
}
function initGlobals() {
board = document.getElementById("board");
rack = document.getElementById("rack");
participants = document.getElementById("participants");
name = document.getElementById("name");
bag = document.getElementById("bag");
chatMessages = document.getElementById("chat-messages");
chatTextarea = document.getElementById("chat-ta");
participantPlaceholder = document.getElementById("participants-placeholder");
boardLabelSelect = document.getElementById("board-label");
}
function initEvents() {
mouseDown(bag, bagClicked);
document.getElementById("clear-game").onclick = clearGame;
document.getElementById("board-lang").onchange = onChangeBoardLang;
document.getElementById("board-label").onchange = onChangeBoardLabel;
document.getElementById("change-name").onclick = changeName;
document.getElementById("join-game").onclick = joinGame;
document.getElementById("clear-rack").onclick = clearRack;
document.getElementById("show-rack").onclick = showRack;
document.getElementById("check-spelling").onclick = checkSpellingClicked;
document.getElementById("btn-settings").onclick = showSettings;
document.getElementById("btn-settings-close").onclick = hideSettings;
document.getElementById("next-help-msg").onclick = nextHelpMessage;
document.getElementById("disable-spell-checker").onclick = toggleSpellChecker;
document.getElementById("score-words").onclick = scoreWords;
window.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
document.querySelector(".modal").classList.remove("show-modal");
}
});
}
function initGame() {
if (!getSetting("PlayerName")) {
myPrompt(
_("Hello! To begin, enter your name. Your adversaries will see this name when you play with them."),
function (name) {
if (name && name.trim()) {
setSetting("PlayerName", name);
}
repromptName(initGame);
setCellCaptions(getSetting("CELL_CAPTIONS"));
}
);
return;
}
if (getSetting("ENABLE_CUSTOM_BOARD") && (Object.keys(BoardList).length > 1)) {
boardLabelSelect.textContent = "";
for (const key of Object.keys(BoardList)) {
boardLabelSelect.add(new Option(key, key));
}
document.getElementById("board-label-selection").hidden = false;
}
name.textContent = getSetting("PlayerName");
if (getSetting("GameNumber")) {
document.getElementById("number").textContent = getSetting("GameNumber");
}
if (getSetting("BoardLang")) {
document.getElementById("board-lang").value = getSetting("BoardLang");
}
if (getSetting("BoardLabel")) {
document.getElementById("board-label").value = getSetting("BoardLabel");
}
startGame(getSetting("GameNumber"));
}
function initBoard() {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const def = BoardList[getSetting("BoardLabel")];
let cell;
let row;
board.innerHTML = "<tr><td class=\"corner\"></td></tr>";
for (let j = 0; j < boardDef.nbColumns; j++) {
board.rows[0].appendChild(document.createElement("th"));
board.rows[0].lastChild.textContent = j + 1;
}
for (let i = 0; i < boardDef.nbRows; i++) {
row = board.insertRow(-1);
row.appendChild(document.createElement("th"));
row.lastChild.textContent = letters[i];
for (let j = 0; j < boardDef.nbColumns; j++) {
cell = document.createElement("td");
boardCells.push(cell);
row.appendChild(cell);
cell.appendChild(document.createElement("div"));
cell.lastChild.className = "tile-placeholder";
const cellName = letters[i] + (j + 1);
if (def.CS.indexOf(cellName) !== -1) {
specialCell("doubleWord", row.lastChild);
cell = row.lastChild.getElementsByClassName("special-cell-label")[0];
cell.textContent = "★";
row.lastChild.id = "center-cell";
} else if (def.TW.indexOf(cellName) !== -1) {
specialCell("tripleWord", row.lastChild);
} else if (def.DW.indexOf(cellName) !== -1) {
specialCell("doubleWord", row.lastChild);
} else if (def.TL.indexOf(cellName) !== -1) {
specialCell("tripleLetter", row.lastChild);
} else if (def.DL.indexOf(cellName) !== -1) {
specialCell("doubleLetter", row.lastChild);
}
}
row.appendChild(document.createElement("th"));
row.lastChild.textContent = letters[i];
}
board.rows[0].appendChild(board.rows[0].cells[0].cloneNode(false));
row = board.insertRow(-1);
row.appendChild(board.rows[0].cells[0].cloneNode(false));
for (let i = 0; i < boardDef.nbColumns; i++) {
row.appendChild(document.createElement("th"));
row.lastChild.textContent = i + 1;
}
row.appendChild(board.rows[0].cells[0].cloneNode(false));
rack.innerHTML = "";
playerLetters.length = 0;
for (let i = 0; i < boardDef.rackLength; i++) {
const span = document.createElement("span");
span.className = "tile-placeholder";
rack.appendChild(span);
playerLetters.push(span);
}
setCellCaptions(getSetting("CELL_CAPTIONS"));
}
function initLang() {
const lang = libD.lang = getSetting("Lang") || libD.lang;
const langSel = document.getElementById("select-lang");
langSel.value = lang;
langSel.onchange = langSelectionChange;
const script = document.createElement("script");
script.src = "l10n/js/" + lang + ".js";
script.onerror = trivabble.l10nError;
document.getElementsByTagName("head")[0].appendChild(script);
}
trivabble.run = function () {
initGlobals();
initEvents();
initChat();
initGame();
initSound();
initSpellChecker();
initFlashLight();
nextHelpMessage();
initTimer();
initCellCaptions();
};
trivabble.l10nError = trivabble.run;
window.addEventListener("DOMContentLoaded", initLang);
window.addEventListener("beforeunload", stopConnection);
window.addEventListener("offline", function () {
stopConnection();
showConnectionLost(_("It seems your browser went offline. Please reconnect to continue playing."));
});
window.addEventListener("online", startConnection);
}());