Merge branch 'lmazet/trivabble-l10n-board'

This commit is contained in:
Raphaël Jakse 2020-05-16 11:04:56 +02:00
commit 1815162cd1
17 changed files with 848 additions and 422 deletions

View File

@ -298,12 +298,16 @@
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "off",
"space-infix-ops": ["error", { "int32Hint": false }],
"space-unary-ops": "error",
"spaced-comment": "off",
"strict": [

View File

@ -1,13 +1,21 @@
# -*- Makefile -*-
PORT = 3000
ESLINT?=eslint
ifeq (, $(shell which $(firstword ${ESLINT})))
ESLINT?=npx eslint
endif
help:
@echo make lang: build translation files
@echo make eslint: use ESLint to check code standard conformence
@echo make start-dev-server: start a development server
lang:
cd l10n; make
eslint:
-${ESLINT} **/*.js
start-dev-server:
cd server && make start-dev-server

View File

@ -1,104 +1,117 @@
#!/usr/bin/env node
/*eslint strict: [2, "global"]*/
/*eslint no-sync: ["error", { allowAtRootLevel: true }]*/
var ROOT = "../public/l10n/";
"use strict";
/* Builds translation files.*/
const ROOT = "../public/l10n/";
var fs = require('fs');
/* Builds translation files. */
var langs = fs.readdirSync("po");
var po, i, len;
const fs = require("fs");
let po;
let i;
let len;
function skipLine() {
while (i < len && po[i] !== '\n') {
++i;
}
++i;
while (i < len && po[i] !== "\n") {
++i;
}
++i;
}
function skipSpaces() {
while (i < len && !po[i].trim()) {
++i;
}
while (i < len && !po[i].trim()) {
++i;
}
}
function parseString() {
skipSpaces();
if (po[i] !== '"') {
return "";
}
++i;
var deb = i, end;
while (i < len) {
if (po[i] === "\\") {
++i;
} else if (po[i] === '"') {
var str1 = po.substring(deb, i++);
var end = i;
skipSpaces();
var ndeb = i;
var str2 = parseString();
if (i === ndeb) { // we did not parse anything
i = end;
return str1;
}
skipSpaces();
if (po[i] !== '"') {
return "";
}
++i;
const deb = i;
return str1 + str2;
}
++i;
}
throw new Error("not ended string at character " + deb);
}
for (var l in langs) {
var lang = langs[l];
var jsFile = fs.openSync(ROOT + "js/" + lang + ".js", "w");
fs.writeSync(jsFile, "(function(){var ");
var poFiles = fs.readdirSync("po/" + lang);
for (var p in poFiles) {
var poFile = poFiles[p];
var translationFunction = fs.readFileSync("pot/" + poFile + 't', {encoding:'utf-8'})
.match(/\#TranslationFunction[\s]+([\S]+)/)[1];
fs.writeSync(jsFile, "_=" + translationFunction + ".l10n;");
po = fs.readFileSync("po/" + lang + '/' + poFile, {encoding:'utf-8'});
i = 0; len = po.length;
while (i < len) {
skipSpaces();
if (po[i] === '#') {
skipLine();
continue;
}
if (po.substr(i, 5) === "msgid") {
if (po[i+5].trim() && po[i+5] !== '"') {
skipLine(); // don't understand this line
continue;
}
i+=5;
while (i < len) {
if (po[i] === "\\") {
++i;
} else if (po[i] === '"') {
const str1 = po.substring(deb, i++);
const end = i;
skipSpaces();
msgid = parseString();
} else if (po.substr(i, 6) === "msgstr") {
if (po[i+6].trim() && po[i+6] !== '"') {
skipLine(); // don't understand this line
continue;
}
i+=6;
msgstr = parseString();
fs.writeSync(jsFile, '_("' + lang + '","' + msgid.replace(/\n/g,"") + '","' + msgstr.replace(/\n/g,"") + '");');
}
skipLine();
}
}
const ndeb = i;
const str2 = parseString();
fs.writeSync(jsFile, "if(" + translationFunction + ".applyL10n){" + translationFunction + ".applyL10n();}})();");
fs.close(jsFile, function (e) {
if (e) {
console.error(e);
}
});
if (i === ndeb) { // we did not parse anything
i = end;
return str1;
}
return str1 + str2;
}
++i;
}
throw new Error("not ended string at character " + deb);
}
let msgid;
let msgstr;
for (const lang of fs.readdirSync("po")) {
const jsFile = fs.openSync(ROOT + "js/" + lang + ".js", "w");
fs.writeSync(jsFile, "(function(){var ");
let translationFunction = "translationFunction";
for (const poFile of fs.readdirSync("po/" + lang)) {
translationFunction = fs.readFileSync("pot/" + poFile + "t", {encoding: "utf-8"})
.match(/#TranslationFunction[\s]+(?<functionName>[\S]+)/u).groups.functionName;
fs.writeSync(jsFile, "_=" + translationFunction + ".l10n;");
po = fs.readFileSync("po/" + lang + "/" + poFile, {encoding: "utf-8"});
i = 0;
len = po.length;
while (i < len) {
skipSpaces();
if (po.substr(i, 5) === "msgid") {
if (po[i + 5].trim() && po[i + 5] !== '"') {
skipLine(); // don't understand this line
} else {
i += 5;
skipSpaces();
msgid = parseString();
}
} else if (po.substr(i, 6) === "msgstr") {
if (po[i + 6].trim() && po[i + 6] !== '"') {
skipLine(); // don't understand this line
} else {
i += 6;
msgstr = parseString();
fs.writeSync(
jsFile,
'_("' + lang + '","' + msgid.replace(/\n/gu, "") + '","' + msgstr.replace(/\n/gu, "") + '");'
);
}
}
// if po[i] === "#", ignore
skipLine();
}
}
fs.writeSync(
jsFile,
"if(" + translationFunction + ".applyL10n){" + translationFunction + ".applyL10n();}})();"
);
fs.close(jsFile, function (e) {
if (e) {
console.error(e);
}
});
}

View File

@ -153,3 +153,27 @@ msgstr "{0} montre son jeu :"
msgid "You cannot take another tile: the bag is empty."
msgstr "Vous ne pouvez pas tirer de pièce : le sac est vide."
msgid "Are you sure you want to change board to '{0}'? This will put all the tiles back in the bag and start another game."
msgstr "Êtes-vous sûr·e de vouloir de changer la langue du plateau à '{0}' ? Cela remettra toutes les lettres du jeu dans le sac et commencera une nouvelle partie."
msgid "Board language:"
msgstr "Langue du plateau :"
msgid "English"
msgstr "Anglais"
msgid "French"
msgstr "Français"
msgid "German"
msgstr "Allemand"
msgid "Spanish"
msgstr "Espagnol"
msgid "You changed the language of the board to {0}"
msgstr "Vous avez changé la langue du plateau en {0}"
msgid "{0} changed the language of the board to {1}"
msgstr "{0} a changé la langue du plateau en {1}"

View File

@ -159,3 +159,27 @@ msgstr ""
msgid "You cannot take another tile: the bag is empty."
msgstr ""
msgid "Are you sure you want to change board to '{0}'? This will put all the tiles back in the bag and start another game."
msgstr ""
msgid "Board language:"
msgstr ""
msgid "English"
msgstr ""
msgid "French"
msgstr ""
msgid "German"
msgstr ""
msgid "Spanish"
msgstr ""
msgid "You changed the language of the board to {0}"
msgstr ""
msgid "{0} changed the language of the board to {1}"
msgstr ""

View File

@ -14,7 +14,9 @@
let alertInput;
let divAlertContent;
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {return s;};
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
return s;
};
function promptOK() {
divAlert.style.display = "none";

View File

@ -39,6 +39,14 @@
</select>
</label>
</div>
<div id="board-lang-selection" style="display:none">
<label>
<span data-l10n="text-content">Board language: </span>
<select id="board-lang">
<option value="fr">French</option>
</select>
</label>
</div>
</div>
<div id="content">

View File

@ -6,7 +6,7 @@ window.libD = {
const t = [];
function f (lang, orig, translated) {
function f(lang, orig, translated) {
if (!orig) {
return (
(libD.lang && t[libD.lang] && t[libD.lang][lang])
@ -20,7 +20,7 @@ window.libD = {
}
t[lang][orig] = translated;
};
}
return f;
},

File diff suppressed because one or more lines are too long

View File

@ -1,58 +1,70 @@
//thx http://stackoverflow.com/questions/1517924/javascript-mapping-touch-events-to-mouse-events#1781750
/*global jQuery*/
function touchHandler(event) {
var nn = event.target.nodeName.toLowerCase();
"use strict";
const nn = event.target.nodeName.toLowerCase();
if (nn === "input" || nn === "select" || nn === "button" || nn === "textarea") {
return;
}
var touches = event.changedTouches,
first = touches ? touches[0] : event,
type = "";
const touches = event.changedTouches;
const first = touches ? touches[0] : event;
let type = "";
switch (event.type) {
case "touchstart": type = "mousedown"; break;
case "touchmove": type = "mousemove"; break;
case "touchend": type = "mouseup"; break;
case "tap": type = "click"; break;
case "dbltap": type = "dblclick"; break;
default: return;
case "touchstart":
type = "mousedown";
break;
case "touchmove":
type = "mousemove";
break;
case "touchend":
type = "mouseup";
break;
case "tap":
type = "click";
break;
case "dbltap":
type = "dblclick";
break;
default:
return;
}
//initMouseEvent(type, canBubble, cancelable, view, clickCount,
// screenX, screenY, clientX, clientY, ctrlKey,
// altKey, shiftKey, metaKey, button, relatedTarget);
//initMouseEvent(type, canBubble, cancelable, view, clickCount,
// screenX, screenY, clientX, clientY, ctrlKey,
// altKey, shiftKey, metaKey, button, relatedTarget);
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0/*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
const simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0/*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
}
document.addEventListener('touchend', (function(speed, distance) {
/*
* Copyright (c)2012 Stephen M. McKamey.
* Licensed under The MIT License.
* src: https://raw.github.com/mckamey/doubleTap.js/master/doubleTap.js
*/
document.addEventListener("touchend", (function (speed, distance) {
// Copyright (c)2012 Stephen M. McKamey.
// Licensed under The MIT License.
// src: https://raw.github.com/mckamey/doubleTap.js/master/doubleTap.js
"use strict";
// default dblclick speed to half sec (default for Windows & Mac OS X)
speed = Math.abs(+speed) || 500;//ms
speed = Math.abs(Number(speed)) || 500;//ms
// default dblclick distance to within 40x40 pixel area
distance = Math.abs(+distance) || 40;//px
distance = Math.abs(Number(distance)) || 40;//px
// Date.now() polyfill
var now = Date.now || function() {
return +new Date();
const now = Date.now || function () {
return Number(new Date());
};
var cancelEvent = function(e) {
function cancelEvent(e) {
e = (e || window.event);
if (e) {
@ -69,24 +81,24 @@ document.addEventListener('touchend', (function(speed, distance) {
}
}
return false;
};
}
var taps = 0,
last = 0,
// NaN will always test false
x = NaN,
y = NaN;
let taps = 0;
let last = 0;
// NaN will always test false
let x = NaN;
let y = NaN;
return function (e) {
e = (e || window.event);
var time = now(),
touch = e.changedTouches ? e.changedTouches[0] : e,
nextX = +touch.clientX,
nextY = +touch.clientY,
target = e.target || e.srcElement,
e2,
parent;
const time = now();
const touch = e.changedTouches ? e.changedTouches[0] : e;
const nextX = Number(touch.clientX);
const nextY = Number(touch.clientY);
const target = e.target || e.srcElement;
let e2;
let parent;
if ((last + speed) > time &&
Math.abs(nextX - x) < distance &&
@ -106,9 +118,9 @@ document.addEventListener('touchend', (function(speed, distance) {
// fire tap event
if (document.createEvent) {
e2 = document.createEvent('MouseEvents');
e2 = document.createEvent("MouseEvents");
e2.initMouseEvent(
'tap',
"tap",
true, // click bubbles
true, // click cancelable
e.view, // copy view
@ -145,18 +157,18 @@ document.addEventListener('touchend', (function(speed, distance) {
// DOM Level 0, IE
parent.ontap(e);
} else if (typeof jQuery !== 'undefined') {
} else if (typeof jQuery !== "undefined") {
// cop out and patch IE6-8 with jQuery
jQuery(this).trigger('tap', e);
jQuery(this).trigger("tap", e); /* eslint-disable-line no-invalid-this */
}
}
if (taps === 2) {
// fire dbltap event only for 2nd click
if (document.createEvent) {
e2 = document.createEvent('MouseEvents');
e2 = document.createEvent("MouseEvents");
e2.initMouseEvent(
'dbltap',
"dbltap",
true, // dblclick bubbles
true, // dblclick cancelable
e.view, // copy view
@ -193,9 +205,9 @@ document.addEventListener('touchend', (function(speed, distance) {
// DOM Level 0, IE
parent.ondbltap(e);
} else if (typeof jQuery !== 'undefined') {
} else if (typeof jQuery !== "undefined") {
// cop out and patch IE6-8 with jQuery
jQuery(this).trigger('dbltap', e);
jQuery(this).trigger("dbltap", e); /* eslint-disable-line no-invalid-this */
}
}
}

View File

@ -453,7 +453,7 @@ td.blink {
font-size:small
}
#lang-selection {
#lang-selection, #board-lang-selection {
font-size:small;
}

View File

@ -27,7 +27,7 @@
(function () {
"use strict";
const VERSION = 202005011900;
const VERSION = 202005070130;
const Conf = window.TrivabbleConf || {};
@ -49,8 +49,8 @@
const trivabble = window.trivabble = {l10n: _};
function format(s, v) {
return s.replace("{0}", v);
function format(s, v1, v2) {
return s.replace("{0}", v1).replace("{1}", v2);
}
let board;
@ -58,6 +58,7 @@
const boardCells = [];
let scoreOf;
let bag;
let boardLangSelect;
const playerLetters = [];
let currentPlayer = "";
@ -256,12 +257,10 @@
} else if (moveCMD.to === "rack") {
moveRack.rack[moveCMD.indexTo] = "";
sendCmds([moveRack, moveCMD]);
} else if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
sendCmds([moveCMD]);
}
sendCmds([moveCMD]);
}
}
@ -539,6 +538,10 @@
case "gameNumber":
document.getElementById("number").textContent = localStorage.trivabbleGameNumber = value;
break;
case "boardLang":
localStorage.trivabbleBoardLang = value;
document.getElementById("board-lang").value = value;
break;
}
}
@ -644,24 +647,42 @@
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) {
if (msg.specialMsg.type === "rack") {
const content = document.createElement("div");
content.appendChild(document.createElement("span"));
content.lastChild.className = "info";
content.lastChild.textContent = format(_("{0} shows their rack:"), msg.sender);
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";
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));
for (let i = 0; i < msg.specialMsg.rack.length; i++) {
letters.appendChild(makeLetter(msg.specialMsg.rack[i], false, true));
}
content.appendChild(letters);
return;
}
content.appendChild(letters);
chatMessage("", content);
return;
case "changeBoardLang": {
const newLang = boardLangSelect.querySelector("[value=" + msg.specialMsg.newBoardLang + "]").textContent;
infoMessage(
(msg.sender === localStorage.trivabblePlayerName)
? format(_("You changed the language of the board to {0}"), newLang)
: format(_("{0} changed the language of the board to {1}"), msg.sender, newLang)
);
return;
}
}
}
@ -686,6 +707,167 @@
refreshCurrentPlayer();
}
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
);
};
}
)(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
);
};
}
)(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 = tablePlayers[playerName].childNodes[2].childNodes[0];
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 applyAction(data) {
switch (data.action) {
case "pushBag": //TODO
break;
case "popBag": //TODO
break;
case "reset": //TODO
tablePlayers = {};
while (participants.rows[1]) {
participants.removeChild(participants.rows[1]);
}
sendCmds([{cmd: "hello"}]);
break;
case "moveLetter":
if (data.from === "board") {
setCell(data.indexFrom, "");
} else if (data.from === "rack") {
setRackCell(data.indexFrom, "");
}
if (data.to === "board") {
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
} else if (data.to === "rack") {
setRackCell(data.indexTo, data.letter);
}
break;
case "setCell":
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
break;
case "setRackCell":
setRackCell(data.indexTo, data.letter);
}
}
function handleReceivedData(data) {
if (Array.isArray(data)) {
data.forEach(handleReceivedData);
@ -730,127 +912,7 @@
}
if (data.players) {
if (participantPlaceholder) {
participantPlaceholder.parentNode.removeChild(participantPlaceholder);
participantPlaceholder = null;
}
for (let i = 0; i < data.players.length; i++) {
const player = data.players[i];
const playerName = data.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
);
};
}
)(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
);
};
}
)(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 = tablePlayers[playerName].childNodes[2].childNodes[0];
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();
setPlayers(data.players);
}
if (data.playerName) {
@ -861,6 +923,14 @@
set("gameNumber", data.gameNumber);
}
if (data.availableBoardLangs) {
setAvailableBoardLangs(data.availableBoardLangs);
}
if (data.boardLang) {
set("boardLang", data.boardLang);
(boardLangSelect || {}).value = data.boardLang;
}
if (data.letterValues) {
scoreOf = data.letterValues;
@ -886,43 +956,8 @@
setRack(data.rack);
}
if (!data.action) {
return;
}
switch (data.action) {
case "pushBag": //TODO
break;
case "popBag": //TODO
break;
case "reset": //TODO
tablePlayers = {};
while (participants.rows[1]) {
participants.removeChild(participants.rows[1]);
}
sendCmds([{cmd: "hello"}]);
break;
case "moveLetter":
if (data.from === "board") {
setCell(data.indexFrom, "");
} else if (data.from === "rack") {
setRackCell(data.indexFrom, "");
}
if (data.to === "board") {
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
} else if (data.to === "rack") {
setRackCell(data.indexTo, data.letter);
}
break;
case "setCell":
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
break;
case "setRackCell":
setRackCell(data.indexTo, data.letter);
if (data.action) {
applyAction(data);
}
}
@ -1191,6 +1226,7 @@
return {
gameNumber: localStorage.trivabbleGameNumber || "",
playerName: localStorage.trivabblePlayerName,
boardLang: localStorage.trivabbleBoardLang,
version: VERSION,
cmds: cmds
};
@ -1327,6 +1363,21 @@
);
}
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}]);
},
function () {
boardLangSelect.value = localStorage.trivabbleBoardLang;
}
);
}
function clearRack() {
myConfirm(
_("Are you sure you want to put all your tiles back in the bag?"),
@ -1496,6 +1547,34 @@
trivabble.run();
};
function setAvailableBoardLangs(availableBoardLangs) {
if (!boardLangSelect) {
const boardLangSelection = document.getElementById("board-lang-selection");
boardLangSelect = boardLangSelection.querySelector("select");
boardLangSelection.style.display = "";
}
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 = localStorage.trivabbleBoardLang;
}
function langSelectionChange(e) {
localStorage.trivabbleLang = e.target.value;
location.reload();
@ -1518,6 +1597,7 @@
mouseDown(bag, bagClicked);
document.getElementById("clear-game").onclick = clearGame;
document.getElementById("board-lang").onchange = onChangeBoardLang;
document.getElementById("change-name").onclick = changeName;
document.getElementById("join-game").onclick = joinGame;
document.getElementById("clear-rack").onclick = clearRack;
@ -1592,7 +1672,7 @@
specialCell("doubleWord", board.lastChild.lastChild);
} else if ((i % 4 === 1) && (j % 4 === 1)) {
specialCell("tripleLetter", board.lastChild.lastChild);
} else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14-i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
} else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14 - i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
specialCell("doubleLetter", board.lastChild.lastChild);
}
}
@ -1617,7 +1697,11 @@
document.getElementById("number").textContent = localStorage.trivabbleGameNumber;
}
startGame(localStorage.trivabbleGameNumber);
if (localStorage.trivabbleBoardLang) {
document.getElementById("board-lang").value = localStorage.trivabbleBoardLang;
}
startGame(localStorage.trivabbleGameNumber, localStorage.trivabbleBoardLang);
}
function initLang() {

70
server/lang/de.json Normal file
View File

@ -0,0 +1,70 @@
{
"code": "de",
"name": "German",
"bag": [
" ", " ",
"E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
"N", "N", "N", "N", "N", "N", "N", "N", "N",
"S", "S", "S", "S", "S", "S", "S",
"I", "I", "I", "I", "I", "I",
"R", "R", "R", "R", "R", "R",
"T", "T", "T", "T", "T", "T",
"U", "U", "U", "U", "U", "U",
"A", "A", "A", "A", "A",
"D", "D", "D", "D",
"H", "H", "H", "H",
"G", "G", "G",
"L", "L", "L",
"O", "O", "O",
"M", "M", "M", "M",
"B", "B",
"W",
"Z",
"C", "C",
"F", "F",
"K", "K",
"P",
"Ä",
"J",
"Ü",
"V",
"Ö",
"X",
"Q",
"Y"
],
"letterValues": {
" ": 0,
"E": 1,
"N": 1,
"S": 1,
"I": 1,
"R": 1,
"T": 1,
"U": 1,
"A": 1,
"D": 1,
"H": 2,
"G": 2,
"L": 2,
"O": 2,
"M": 3,
"B": 3,
"W": 3,
"Z": 3,
"C": 4,
"F": 4,
"K": 4,
"P": 4,
"Ä": 6,
"J": 6,
"Ü": 6,
"V": 6,
"Ö": 8,
"X": 8,
"Q": 10,
"Y": 10
}
}

64
server/lang/en.json Normal file
View File

@ -0,0 +1,64 @@
{
"code": "en",
"name": "English",
"bag": [
" ", " ",
"E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
"A", "A", "A", "A", "A", "A", "A", "A", "A",
"I", "I", "I", "I", "I", "I", "I", "I", "I",
"O", "O", "O", "O", "O", "O", "O", "O",
"R", "R", "R", "R", "R", "R",
"N", "N", "N", "N", "N", "N",
"T", "T", "T", "T", "T", "T",
"L", "L", "L", "L",
"S", "S", "S", "S",
"U", "U", "U", "U",
"D", "D", "D", "D",
"G", "G", "G",
"B", "B",
"C", "C",
"M", "M",
"P", "P",
"F", "F",
"H", "H",
"V", "V",
"W", "W",
"Y", "Y",
"K",
"J",
"X",
"Q",
"Z"
],
"letterValues": {
" ": 0,
"E": 1,
"A": 1,
"I": 1,
"O": 1,
"R": 1,
"N": 1,
"T": 1,
"L": 1,
"S": 1,
"U": 1,
"D": 2,
"G": 2,
"B": 3,
"C": 3,
"M": 3,
"P": 3,
"F": 4,
"H": 4,
"V": 4,
"W": 4,
"Y": 4,
"K": 5,
"J": 8,
"X": 8,
"Q": 10,
"Z": 10
}
}

68
server/lang/es.json Normal file
View File

@ -0,0 +1,68 @@
{
"code": "es",
"name": "Spanish",
"bag": [
" ", " ",
"A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
"E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
"O", "O", "O", "O", "O", "O", "O", "O", "O",
"I", "I", "I", "I", "I", "I",
"S", "S", "S", "S", "S", "S",
"N", "N", "N", "N", "N",
"R", "R", "R", "R", "R",
"U", "U", "U", "U", "U",
"L", "L", "L", "L",
"T", "T", "T", "T",
"D", "D", "D", "D", "D",
"G", "G",
"C", "C", "C", "C",
"B", "B",
"M", "M",
"P", "P",
"H", "H",
"F",
"V",
"Y",
"CH",
"Q",
"J",
"LL",
"Ñ",
"RR",
"X",
"Z"
],
"letterValues": {
" ": 0,
"A": 1,
"E": 1,
"O": 1,
"I": 1,
"S": 1,
"N": 1,
"R": 1,
"U": 1,
"L": 1,
"T": 1,
"D": 2,
"G": 2,
"C": 3,
"B": 3,
"M": 3,
"P": 3,
"H": 4,
"F": 4,
"V": 4,
"Y": 4,
"CH": 5,
"Q": 5,
"J": 8,
"LL": 8,
"Ñ": 8,
"RR": 8,
"X": 8,
"Z": 10
}
}

64
server/lang/fr.json Normal file
View File

@ -0,0 +1,64 @@
{
"code": "fr",
"name": "French",
"bag": [
" ", " ",
"E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
"A", "A", "A", "A", "A", "A", "A", "A", "A",
"I", "I", "I", "I", "I", "I", "I", "I",
"N", "N", "N", "N", "N", "N",
"O", "O", "O", "O", "O", "O",
"R", "R", "R", "R", "R", "R",
"S", "S", "S", "S", "S", "S",
"T", "T", "T", "T", "T", "T",
"U", "U", "U", "U", "U", "U",
"L", "L", "L", "L", "L",
"D", "D", "D",
"M", "M", "M",
"G", "G",
"B", "B",
"C", "C",
"P", "P",
"F", "F",
"H", "H",
"V", "V",
"J",
"Q",
"K",
"W",
"X",
"Y",
"Z"
],
"letterValues": {
" ": 0,
"E": 1,
"A": 1,
"I": 1,
"N": 1,
"O": 1,
"R": 1,
"S": 1,
"T": 1,
"U": 1,
"L": 1,
"D": 2,
"M": 2,
"G": 2,
"B": 3,
"C": 3,
"P": 3,
"F": 4,
"H": 4,
"V": 4,
"J": 8,
"Q": 8,
"K": 10,
"W": 10,
"X": 10,
"Y": 10,
"Z": 10
}
}

View File

@ -33,8 +33,9 @@ const host = process.env.TRIVABBLE_HOST || "localhost"
const SAVE_TIMEOUT = 5000;
const KEEP_ALIVE = 30000;
const GAMES_BACKUP = process.env.TRIVABBLE_GAMES_BACKUP || "games.backup.json";
const DEFAULT_BOARD_LANG = process.env.TRIVABBLE_DEFAULT_BOARD_LANG || "fr";
const VERSION = 202004281800;
const VERSION = 202005070100;
function envTrue(name) {
return (process.env[name] || "").toLowerCase() === "true";
@ -49,77 +50,26 @@ if (DEV_ENABLE_SERVING_FILES) {
const debuglog = DEBUG_LOG ? console.log.bind(console) : () => null;
const http = require("http");
const fs = require("fs");
const http = require("http");
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const REQUEST_TYPE_LONG_POLLING = 1;
const REQUEST_TYPE_SSE = 2;
const REQUEST_TYPE_WEBSOCKET = 3;
const frBag = [
" ", " ", // Jokers
"E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
"A", "A", "A", "A", "A", "A", "A", "A", "A",
"I", "I", "I", "I", "I", "I", "I", "I",
"N", "N", "N", "N", "N", "N",
"O", "O", "O", "O", "O", "O",
"R", "R", "R", "R", "R", "R",
"S", "S", "S", "S", "S", "S",
"T", "T", "T", "T", "T", "T",
"U", "U", "U", "U", "U", "U",
"L", "L", "L", "L", "L",
"D", "D", "D",
"M", "M", "M",
"G", "G",
"B", "B",
"C", "C",
"P", "P",
"F", "F",
"H", "H",
"V", "V",
"J",
"Q",
"K",
"W",
"X",
"Y",
"Z"
];
/* eslint no-sync: ["error", { allowAtRootLevel: true }] */
/* eslint-disable quote-props */
/* Manage multi language board */
const boardTilesPerLang = {};
const availableBoardLangs = {};
const frValues = {
" ": 0,
"E": 1,
"A": 1,
"I": 1,
"N": 1,
"O": 1,
"R": 1,
"S": 1,
"T": 1,
"U": 1,
"L": 1,
"D": 2,
"M": 2,
"G": 2,
"B": 3,
"C": 3,
"P": 3,
"F": 4,
"H": 4,
"V": 4,
"J": 8,
"Q": 8,
"K": 10,
"W": 10,
"X": 10,
"Y": 10,
"Z": 10
};
/* eslint-enable quote-props */
for (const lang of fs.readdirSync(path.join(__dirname, "lang"))) {
const data = require(path.join(__dirname, "lang", lang)); // eslint-disable-line global-require
boardTilesPerLang[data.code] = data;
availableBoardLangs[data.code] = data.name;
}
const games = {};
@ -155,7 +105,6 @@ function shuffleInPlace(a) {
}
function Game() {
this.letterValues = frValues;
this.init();
this.listeningPlayers = [];
this.pendingEvents = [];
@ -264,9 +213,11 @@ function newBoard() {
return res;
}
Game.prototype.init = function () {
Game.prototype.init = function (lang) {
this.board = newBoard();
this.bag = frBag.slice();
this.lang = lang || DEFAULT_BOARD_LANG;
this.bag = boardTilesPerLang[this.lang].bag.slice();
this.letterValues = boardTilesPerLang[this.lang].letterValues;
this.racks = {};
this.scores = {};
this.lastUpdated = new Date();
@ -278,7 +229,9 @@ Game.prototype.init = function () {
Game.prototype.toJSON = function () {
return {
board: this.board,
lang: this.lang,
bag: this.bag,
letterValues: this.letterValues,
racks: this.racks,
scores: this.scores,
lastUpdated: this.lastUpdated.toISOString(),
@ -289,7 +242,9 @@ Game.prototype.toJSON = function () {
Game.fromJSON = function (obj) {
const game = new Game();
game.board = obj.board || newBoard();
game.bag = obj.bag || frBag.slice();
game.lang = obj.lang || DEFAULT_BOARD_LANG;
game.bag = boardTilesPerLang[game.lang].bag.slice();
game.letterValues = boardTilesPerLang[game.lang].letterValues;
game.racks = obj.racks || {};
game.scores = obj.scores || {};
game.lastUpdated = obj.lastUpdated ? new Date(obj.lastUpdated) : new Date();
@ -336,15 +291,13 @@ Game.prototype.playerJoined = function (playerName) {
const players = [];
for (let player in this.racks) {
if (Object.prototype.hasOwnProperty.call(this.racks, player)) {
player = player.slice(1); // '#'
players.push({
player: player,
score: this.getPlayerScore(player),
rackCount: countTiles(this.getPlayerRack(player))
});
}
for (let player of Object.keys(this.racks)) {
player = player.slice(1); // '#'
players.push({
player: player,
score: this.getPlayerScore(player),
rackCount: countTiles(this.getPlayerRack(player))
});
}
this.pendingEvents.push({players: players});
@ -358,7 +311,7 @@ Game.prototype.addListeningPlayer = function (playerName, responseAndType) {
let closed = false;
function close () {
function close() {
if (closed) {
return;
}
@ -446,7 +399,7 @@ Game.prototype.bagPushLetter = function (letter, player) {
};
Game.prototype.reset = function (player) {
this.init();
this.init(this.lang);
this.pendingEvents.push({
player: player,
action: "reset",
@ -523,11 +476,13 @@ function handleCommand(cmdNumber, message, response) {
error: 0,
gameNumber: gameNumber,
playerName: playerName,
boardLang: game.lang,
availableBoardLangs: availableBoardLangs,
currentPlayer: game.currentPlayer,
rack: game.getPlayerRack(playerName),
board: game.board,
remainingLetters: game.bag.length,
letterValues: frValues,
letterletterValues: game.letterValues,
version: VERSION
});
break;
@ -535,7 +490,7 @@ function handleCommand(cmdNumber, message, response) {
case "hello": {
game.playerJoined(playerName);
reply(message, response, cmdNumber, {error: 0, version: VERSION});
reply(message, response, cmdNumber, {error: 0, boardLang: game.lang, version: VERSION});
break;
}
@ -712,6 +667,31 @@ function handleCommand(cmdNumber, message, response) {
break;
}
case "changeBoard": {
game.lang = cmd.lang || DEFAULT_BOARD_LANG;
game.reset();
reply(message, response, cmdNumber, {
error: 0,
boardLang: game.lang,
letterValues: game.letterValues
});
game.pendingEvents.push({
msg: {
sender: playerName,
content: "I changed the language of the board to " + game.lang,
specialMsg: {
type: "changeBoardLang",
newBoardLang: game.lang
}
}
});
break;
}
case "msg": {
game.pendingEvents.push({
msg: {
@ -740,14 +720,16 @@ function handleCommands(message, responseAndType) {
writeMessage(responseAndType,
JSON.stringify({
playerName: message.playerName,
currentPlayer: game.currentPlayer,
gameNumber: gameNumber,
letterValues: game.letterValues,
rack: game.getPlayerRack(message.playerName),
board: game.board,
remainingLetters: game.bag.length,
version: VERSION
playerName: message.playerName,
currentPlayer: game.currentPlayer,
gameNumber: gameNumber,
boardLang: game.lang,
availableBoardLangs: availableBoardLangs,
letterValues: game.letterValues,
rack: game.getPlayerRack(message.playerName),
board: game.board,
remainingLetters: game.bag.length,
version: VERSION
})
);
@ -980,9 +962,10 @@ function handleRequest(request, response) {
debuglog("Serving " + request.url);
fs.exists("../public/" + request.url, function (exists) {
const requestedPath = path.join(__dirname, "..", "public", request.url);
fs.exists(requestedPath, function (exists) {
if (exists) {
fs.readFile("../public/" + request.url, function(err, contents) {
fs.readFile(requestedPath, function (err, contents) {
if (err) {
response.statusCode = 500;
response.setHeader("Content-Type", "text/plain; charset=utf-8");
@ -1056,10 +1039,8 @@ fs.readFile(GAMES_BACKUP, function (err, data) {
} else {
const backup = JSON.parse(data);
for (const gameNumber in backup) {
if (Object.prototype.hasOwnProperty.call(backup, gameNumber)) {
games[gameNumber] = Game.fromJSON(backup[gameNumber]);
}
for (const gameNumber of Object.keys(backup)) {
games[gameNumber] = Game.fromJSON(backup[gameNumber]);
}
}
} catch (e) {