Merge branch 'develop' into timer

This commit is contained in:
Laurent Mazet 2020-12-06 17:59:03 +01:00
commit b059eaf9e9
16 changed files with 503 additions and 159 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
/server/games.backup.json
/public/config.js
/public/dict/*.dict
/public/dict/
/public/l10n
/.vscode

View File

@ -10,7 +10,7 @@ endif
.PHONY: all help lang start-dev-server
all: lang
all: lang emptydictlist
help:
@echo make extract-lang-dists: extract the distributions per language from Wikipedia
@ -26,6 +26,11 @@ extract-lang-dists:
lang: public/l10n/js/fr.js
emptydictlist: public/dict/list.js
public/dict/list.js:
mkdir -p public/dict/ && touch public/dict/list.js
eslint:
-${ESLINT} **/*.js

View File

@ -135,7 +135,20 @@ We will assume www-data is the UNIX user of your Web server. Adapt if necessary.
exec sudo -u www-data DEBUG_LOG=true nodejs trivabble-server.js
```
8. Configure your web server. In the next two sections, we describe the configuration for Apache 2 and Nginx.
8. Configure your web server.
#### Content-Security Policy Header
Trivabble loads 5 static javascript files, 2 static css files and a sound from its origin, and connects to the same server by SSE or websockets.
Here is the recommended HTTP CSP header for Trivabble:
```
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; media-src 'self'
```
We advise you to set this up to improve the security of your setup.
In the next two sections, we describe the configuration for Apache 2 and Nginx.
#### Apache 2
@ -193,7 +206,7 @@ server {
## Enable the spell checker
This is an experimental feature. The process of building the dictionaries takes a lot of disk space.
The process of building the dictionaries takes a lot of disk space and require huge amount of memory (around 1GB).
Install `wget` and `aspell` (to have the `word-list-compress` command), and run:
@ -202,4 +215,65 @@ cd dict-dists-extractor
make
```
Then, set `ENABLE_CHECK_SPELLING` to `true` in `public/config.js`.
If you want to limit spelling to a subset of languages, you can define the list by the `LANGS` variable before executing the `make` command such as:
```sh
cd dict-dists-extractor
LANGS="de en fr" make
```
This way, you only build English, French and German dictionaries.
To deactivate spell checker at system level, you need to empty dictionary list contained into file `public/dict/list.js`. Such command can be used:
```sh
echo > public/dict/list.js
```
## Technical appendices
### Board extractor
Wikipedia provides tile distributions (content of the bag and points) for various languages. Board languages are built from a Wikipedia page.
#### General parsing
Most board languages are extracted from the French version of the Wikipedia page which has a simple document structure. Parsing is done by a JS script named `make_board.js`:
- each new language board is proceeded by a level 2 header containing language name,
- tile points is followed by `point(s)`
- tile letter and number of tiles are easy extracted by a regex which looks like `<b>(letter)</b> <small>x(times)</small>`
#### Special languages
Some languages are only defined into the English version. So we copy them into separated text files and parsing is done by an AWK named `make_board.awk`.
### Dictionary building process
The spell checker in Trivabble is based on Aspell dictionaries. However, as Trivabble only uses a subset of letters (most accentuated characters have to be replaced by standard ones), we can't use Aspell engine straightforward; we need to build lists of words that only use playable characters.
#### Overall concept
In order to build dictionaries for various languages, we first need to retrieve Aspell dictionary lists. Then we retrieve the last versions of dictionaries for every languages and we build them. Thanks to two Aspell commands, we build the full list of words. Based on the distribution of tiles, we translate every derived (most of the time accentuated) character by the appropriate one, and finally we remove duplicated entries.
#### Details on Makefile rules
Rule '$(OBJ_DIR)/src.mk' retrieves on Aspell site the last version of the dictionary list, extract dictionary archive URL and build a sub-makefile with rules to retrieve every needed dictionary.
Rule '%.dir' unpacks dictionary archives and build Aspell dictionary. Result directory is rename as LANG.dir
Rule '%.low' expands a Aspell dictionary into a list of words (low means "List Of Words"". Phrases are split into words. Finally the list is ordered and clean from duplicated words.
Rule '$(ROOT_DICT)/%.dict':
- excludes words with numbers, dashes, apostrophes and other forbidden signs,
- translates accentuated characters into standard ones,
- up-case all words,
- and remove duplicated words.
This file can be used by Trivabble for spell checking.
Last rule 'check-%' checks that file %.dict contains only words with characters base on language tile bag.
#### Remarks
Some lists of words are too huge (for example Hungarian or Turkish are agglutinative languages), so Trivabble will not be able to check spelling for those languages.
Some languages don't have any Aspell dictionary.

1
dict-dists-extractor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/obj

View File

@ -9,7 +9,7 @@ SMALL = br ca cy da de en es fr ga hr hy is lv nl no pt sv
LARGE = bg el eo it pl ro ru sk sl uk
# 50MB < dictionary size < 500MB
VERY_LARGE = ar cs et fi
LANGS = $(SMALL) $(LARGE)
LANGS ?= $(SMALL) $(LARGE)
DICT_RULE_PRELUDE = sortuniq() { cat > "$$1"; sort -u -o "$$1" "$$1"; }; \
@ -24,13 +24,14 @@ LOWS = $(addsuffix .low,$(LANGS))
DICTS = $(addsuffix .dict,$(LANGS))
DEPEND = Makefile
#DEPEND = Makefile
#MAKEFLAGS = -s
.PHONY: all required count clean low
.PHONY: all required count clean low list
all: required
make $(addprefix check-,$(LANGS))
make list
$(ROOT_DICT):
mkdir $(ROOT_DICT)
@ -45,6 +46,15 @@ low: $(LOWS)
required: ${OBJ_DIR} ${OBJ_DIR}/src.mk ${ROOT_DICT}
list: $(ROOT_DICT)/list.js
$(ROOT_DICT)/list.js: $(wildcard $(ROOT_DICT)/*.dict)
ls -s ${ROOT_DICT}/*.dict| \
awk 'BEGIN { printf "window.DictionaryList = {\n" } \
{ $$0 = gensub(/(.+) .*\/(.+)\.dict/, "\\1 \\2", "g"); \
printf "\"%s\": %d,\n", $$2, $$1 } \
END { printf "\"none\": 0\n};" }' > $@
$(OBJ_DIR)/src.mk:
echo Creation language table
wget $(ASPDICT) -q -O - | \
@ -73,8 +83,9 @@ include $(wildcard ${OBJ_DIR}/src.mk)
export DICT_NAME="$$(basename "$<" ".dir")"; \
aspell --dict-dir="$$(realpath $<)" -d "$$DICT_NAME" dump master "$$DICT_NAME" | \
aspell --dict-dir="$$(realpath $<)" -l "$$DICT_NAME" expand | \
tr -s '[:space:]' '\n' > "$@" && \
LC_ALL=C sort -S28G -u -o "$@" "$@"
tr -s '[:space:]' '\n' > "$@~" && \
LC_ALL=C sort -S28G -u -o "$@~" "$@~" && \
mv "$@~" "$@"
$(OBJ_DIR)/no.low:
make $(OBJ_DIR)/nb.low

View File

@ -164,7 +164,7 @@ msgid "Are you sure you want to change board to '{0}'? This will put all the ti
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 :"
msgstr "Langue du plateau :"
msgid "You changed the language of the board to {0}"
msgstr "Vous avez changé la langue du plateau en {0}"
@ -199,6 +199,12 @@ msgstr "La vérification orthographique nécessite que Trivabble télécharge un
msgid "Spell checking is based on:"
msgstr "La vérification orthographique est basée sur :"
msgid "{0} points will be added to:"
msgstr "{0} points vont être ajoutés à:"
msgid "There are no points to add."
msgstr "Il ny a pas de point à ajouter."
msgid "Settings"
msgstr "Paramètres"
@ -229,6 +235,15 @@ msgstr "Astuce !"
msgid "Next tip"
msgstr "Astuce suivante"
msgid "Disable the spell checker"
msgstr "Désactiver la vérification orthographique"
msgid "Spell checking is not available for this language."
msgstr "La vérification orthographique n'est pas disponible dans cette langue."
msgid "Score new words"
msgstr "Compter les points"
msgid "Turn timer:"
msgstr "Temps du tour :"

View File

@ -142,6 +142,12 @@ msgstr ""
msgid "Who's turn? Click on the Turn button!"
msgstr ""
msgid "Click on (+) to increase someone's score."
msgstr ""
msgid "Show a cell to everyone by double-clicking on it."
msgstr ""
msgid "Show my rack to other players"
msgstr ""
@ -199,6 +205,12 @@ msgstr ""
msgid "Spell checking is based on:"
msgstr ""
msgid "{0} points will be added to:"
msgstr ""
msgid "There are no points to add."
msgstr: ""
msgid "Settings"
msgstr ""
@ -222,18 +234,21 @@ msgstr ""
msgid "Flash light color"
msgstr ""
msgid "Click on (+) to increase someone's score."
msgstr ""
msgid "Show a cell to everyone by double-clicking on it."
msgstr ""
msgid "Tip!"
msgstr ""
msgid "Next tip"
msgstr ""
msgid "Disable the spell checker"
msgstr ""
msgid "Spell checking is not available for this language."
msgstr ""
msgid "Score new words"
msgstr ""
msgid "Turn timer:"
msgstr ""

View File

@ -15,7 +15,7 @@ $1 == "-" { sub(/- /, "") }
gsub(/[×,]/, "")
for (i = 1; i<= NF; i+=2) {
lettre = $(i)
if (lettre == "blank") {
if ((lettre == "blank") || (lettre ~ /[Jj]oker/)) {
lettre = " "
}
nombre = $(i+1)

View File

@ -40,8 +40,8 @@
margin-top:1em;
}
.alert-content {
padding:1ex
.alert-content-and-input {
padding:1ex;
}
.alert input[type=text], .alert input[type=number], .alert input[type=password] {
@ -81,3 +81,18 @@
border-radius:3px
}
.alert-prompt-buttons {
display: none;
}
.alert.choice .alert-prompt-buttons, .alert.prompt .alert-prompt-buttons {
display: block;
}
.alert.choice.choice-inline .alert-content, .choice.choice-inline .alert-prompt {
display:inline-block;
}
.alert.choice.choice-inline .alert-prompt {
padding-left: 1ex;
}

View File

@ -12,6 +12,7 @@
let divAlertCallbackYes;
let divAlertCallbackNo;
let alertInput;
let alertSelect;
let divAlertContent;
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
@ -85,14 +86,26 @@
divAlertInput.appendChild(alertInput);
divAlertInput.appendChild(document.createElement("div"));
divAlertInput.lastChild.className = "alert-prompt-buttons";
divAlertInput.lastChild.appendChild(document.createElement("button"));
divAlertInput.lastChild.lastChild.textContent = _("OK");
divAlertInput.lastChild.lastChild.onclick = promptOK;
divAlertInput.lastChild.appendChild(document.createElement("button"));
divAlertInput.lastChild.lastChild.textContent = _("Annuler");
divAlertInput.lastChild.lastChild.onclick = promptCancel;
alertSelect = document.createElement("select");
alertSelect.onchange = function () {
alertInput.value = alertSelect.value;
};
divAlertInput.appendChild(alertSelect);
const divAlertContentAndInput = document.createElement("div");
divAlertContentAndInput.className = "alert-content-and-input";
divAlertContentAndInput.appendChild(divAlertContent);
divAlertContentAndInput.appendChild(divAlertInput);
const divAlerPromptButton = document.createElement("div");
divAlerPromptButton.className = "alert-prompt-buttons";
divAlerPromptButton.appendChild(document.createElement("button"));
divAlerPromptButton.lastChild.textContent = _("OK");
divAlerPromptButton.lastChild.onclick = promptOK;
divAlerPromptButton.appendChild(document.createElement("button"));
divAlerPromptButton.lastChild.textContent = _("Cancel");
divAlerPromptButton.lastChild.onclick = promptCancel;
divAlertConfirm = document.createElement("div");
divAlertConfirm.className = _("alert-confirm");
@ -111,8 +124,8 @@
const divAlertOuter = document.createElement("div");
divAlertOuter.className = "alert-outer";
divAlertOuter.appendChild(divAlertContent);
divAlertOuter.appendChild(divAlertInput);
divAlertOuter.appendChild(divAlertContentAndInput);
divAlertOuter.appendChild(divAlerPromptButton);
divAlertOuter.appendChild(divAlertConfirm);
divAlertOuter.appendChild(divAlertButton);
divAlert.appendChild(divAlertOuter);
@ -131,6 +144,8 @@
}
divAlert.classList.remove("prompt");
divAlert.classList.remove("choice");
divAlert.classList.remove("choice-inline");
divAlertContent.textContent = msg;
divAlertInput.style.display = "none";
@ -149,11 +164,15 @@
}
divAlert.classList.add("prompt");
divAlert.classList.remove("choice");
divAlert.classList.remove("choice-inline");
divAlertContent.textContent = msg;
divAlertInput.style.display = "";
alertInput.style.display = "";
alertInput.value = defaultText || "";
alertInput.type = (options && options.type) || "text";
alertSelect.style.display = "none";
divAlertConfirm.style.display = "none";
divAlertButton.style.display = "none";
divAlertCallback = callback;
@ -170,6 +189,8 @@
}
divAlert.classList.remove("prompt");
divAlert.classList.remove("choice");
divAlert.classList.remove("choice-inline");
divAlertContent.textContent = msg;
divAlertInput.style.display = "none";
@ -180,4 +201,35 @@
divAlert.style.display = "";
divAlertConfirm.getElementsByTagName("button")[0].focus();
};
global.myChoice = function (msg, options, callback, defaultValue) {
if (!divAlert) {
prepare();
}
divAlert.classList.remove("prompt");
divAlert.classList.add("choice");
if (options.dispositionInline) {
divAlert.classList.add("choice-inline");
} else {
divAlert.classList.remove("choice-inline");
}
alertSelect.options.length = 0;
for (const key of options.choices) {
[].push.call(alertSelect.options, new Option(key, key));
}
alertSelect.value = alertInput.value = defaultValue;
divAlertContent.textContent = msg;
divAlertInput.style.display = "";
alertInput.style.display = "none";
alertSelect.style.display = "";
divAlertConfirm.style.display = "none";
divAlertButton.style.display = "none";
divAlertCallback = callback;
divAlert.style.display = "";
divAlertConfirm.getElementsByTagName("button")[0].focus();
};
}(this));

View File

@ -17,8 +17,8 @@ window.TrivabbleConf = {
// To tweak only if your webserver is shared with other conflicting resources at / (e.g. Yunohost integration)
APP_PATH: "",
// Whether the spell checker is enabled (dictionaries must be downloaded on the server before enabling this option)
ENABLE_SPELL_CHECKER: false,
// The API entry point. Default value: APP_PATH + '/:trivabble'
API_ENTRY_POINT: "/:trivabble",
// The color of the flash light when double clicking on a cell
FLASH_LIGHT_COLOR: "#EE6633",
@ -35,6 +35,11 @@ window.TrivabbleConf = {
// The defaut double tap duration. If not set, the value at the middle of the previous array is used.
DOUBLE_TAP_DURATION: 1800,
// The default premium for playing seven tiles on a turn
PREMIUM_SEVEN_TILES: 50,
// Score is automically affected to last player. If false, score is automatically affected to the player who pressed the Score button
SCORE_LAST_PLAYER: true,
// I don't like trailing commas, here is a nice message for you reading this file :-)
HAVE_FUN: true

View File

@ -9,6 +9,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="format-detection" content="telephone=no" />
<script src="config.js"></script>
<script src="dict/list.js"></script>
<script src="l10n.js"></script>
<script src="alert.js"></script>
<script src="touch2click.js"></script>
@ -58,6 +59,17 @@
<p><label><input type="checkbox" id="tiles-sound" /><span data-l10n="text-content">Sound of the tiles</span></label></p>
<p><label><input type="checkbox" id="msg-sound" /><span data-l10n="text-content">Sound of messages</span></label></p>
</div>
<div>
<p id="disable-spell-checker-p" hide="true">
<label>
<input type="checkbox" id="disable-spell-checker" />
<span data-l10n="text-content">Disable the spell checker</span>
</label>
</p>
<p id="no-spell-checker-p" hide="false">
<span data-l10n="text-content">Spell checking is not available for this language.</span>
</p>
</div>
<div>
<p>
<label for="double-tap-duration" data-l10n="text-content">Double tap duration:</label>
@ -122,6 +134,7 @@
<button id="clear-rack" class="minibutton" data-l10n="text-content">Put back all the tiles of&#10;your rack in the bag</button>
<button id="show-rack" class="minibutton" data-l10n="text-content">Show my rack to other players</button>
<button id="check-spelling" class="minibutton" data-l10n="text-content" hidden="true">Check spelling for new words</button>
<button id="score-words" class="minibutton" data-l10n="text-content">Score new words</button>
</div>
<div id="help-box">
<p id="help-box-title" data-l10n="text-content">Tip!</p>

View File

@ -122,6 +122,8 @@ html, #board, [draggable], .tile {
display:inline-block;
white-space:pre-wrap;
font-size:7px;
text-overflow:ellipsis;
overflow:hidden;
}
#center-cell .special-cell-label {
@ -186,8 +188,10 @@ button {
}
#board td {
width:32px;
height:32px;
width: 32px;
max-width:32px;
max-height:32px;
}
#rack .tile-placeholder {

View File

@ -22,7 +22,7 @@
* @source: https://gitlab.com/raphj/trivabble/
*/
/*global libD, myConfirm, myAlert, myPrompt*/
/*global libD, myConfirm, myAlert, myPrompt, myChoice*/
(function () {
"use strict";
@ -31,6 +31,8 @@
const Conf = window.TrivabbleConf || {};
const DictionaryList = window.DictionaryList || {};
function setConf(parameterName, defaultValue) {
if (typeof Conf[parameterName] === "undefined") {
Conf[parameterName] = defaultValue;
@ -52,12 +54,14 @@
setConf("APP_PATH", "");
setConf("ENABLE_MSG_SOUND", true);
setConf("ENABLE_TILE_SOUND", true);
setConf("ENABLE_SPELL_CHECKER", false);
setConf("DOUBLE_TAP_DURATIONS", [650, 1100, 1800, 3000, 5000]);
setConf("DOUBLE_TAP_DURATION", middle("DOUBLE_TAP_DURATIONS"));
setConf("FLASH_LIGHT_DURATIONS", [800, 1600, 3200]);
setConf("FLASH_LIGHT_DURATION", middle("FLASH_LIGHT_DURATIONS"));
setConf("FLASH_LIGHT_COLOR", "#ee6633");
setConf("API_ENTRY_POINT", Conf.APP_PATH + "/:trivabble");
setConf("PREMIUM_SEVEN_TILES", 50);
setConf("SCORE_LAST_PLAYER", true);
setConf("ENABLE_TIMER", true);
function isSetting(key) {
@ -69,23 +73,21 @@
let type;
let value;
/* get default value from configuration */
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, key)) {
value = localStorage.getItem(key);
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") {
if (Object.prototype.hasOwnProperty.call(localStorage, key + "_type")) {
type = localStorage.getItem(key + "_type");
} else {
type = "string";
}
type = "string";
}
/* cast from string to type */
@ -105,6 +107,10 @@
return value;
}
function unsetSetting(key) {
localStorage.removeItem("trivabble" + key);
}
function setSetting(key, value) {
if (getSetting(key) === value) {
return;
@ -113,22 +119,12 @@
let type;
/* try to retrieve type from configuration */
if (Object.prototype.hasOwnProperty.call(Conf, key)) {
if (Object.prototype.hasOwnProperty.call(SettingsTypes, key)) {
type = SettingsTypes[key];
} else if (Object.prototype.hasOwnProperty.call(Conf, key)) {
type = typeof Conf[key];
}
/* try to retrieve type from localstorage */
if (typeof type === "undefined") {
if (Object.prototype.hasOwnProperty.call(localStorage, key)) {
type = localStorage.getItem(key + "_type");
}
}
/* if not set type is defined from value */
if (typeof type === "undefined") {
type = typeof value;
}
/* storage value in localstorage */
if (type === typeof value) {
if (type === "object") {
@ -138,32 +134,23 @@
(type === "number") ||
(type === "object") ||
(type === "string")) {
localStorage.setItem(key, value);
/* store type into localstorage if no default value in configuration */
if (!Object.prototype.hasOwnProperty.call(Conf, key)) {
localStorage.setItem(key + "_type", type);
}
localStorage.setItem("trivabble" + key, value);
} else {
console.error("Unsupported type");
}
} else {
console.error("incoherent type");
console.error("Incoherent or missing type. See the SettingsTypes object.");
}
}
function migrateSetting(key, type) {
if (Object.prototype.hasOwnProperty.call(localStorage, key)) {
localStorage.setItem(key + "_type", type);
}
}
migrateSetting("spellCheckerEnabled", "boolean");
migrateSetting("trivabbleGameNumber", "number");
migrateSetting("trivabbleBoardLang", "string");
migrateSetting("trivabbleGameNumber", "number");
migrateSetting("trivabbleLang", "string");
migrateSetting("trivabblePlayerName", "string");
const SettingsTypes = {
SpellCheckerEnabledOnce: "boolean",
DisableSpellChecker: "boolean",
GameNumber: "number",
BoardLang: "string",
Lang: "string",
PlayerName: "string"
};
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
return s;
@ -214,12 +201,9 @@
let currentMessageId = 1;
const waitingMsgs = [];
let serverVersion = 0;
let lastPlayer = null;
// HTTP path url prefix for any socket (xhr,ess or ws) from browser to server access
function getApiEntryPoint() {
return Conf.APP_PATH + "/:trivabble";
}
let serverVersion = 0;
function mouseDown(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
@ -270,6 +254,21 @@
}
}
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) {
@ -283,11 +282,11 @@
return;
}
if (!force && !getSetting("spellCheckerEnabled")) {
if (!force && !getSetting("SpellCheckerEnabledOnce")) {
myConfirm(
_("Spell checking requires Trivabble to download a dictionary. Do you confirm?"),
function () {
setSetting("spellCheckerEnabled", true);
setSetting("SpellCheckerEnabledOnce", true);
getDictionary(code, callback, true);
}
);
@ -635,33 +634,32 @@
rackBCR = rack.getBoundingClientRect();
bagBCR = bag.getBoundingClientRect();
let from;
let from = null;
let index;
let p = movingTile.parentNode;
let oldP = movingTile;
let oldOldP = null;
let p = movingTile;
while (p) {
if (p === board) {
while (p && !from) {
index = boardCells.indexOf(p);
if (index !== -1) {
from = "board";
index = boardCells.indexOf(oldOldP);
break;
}
if (p === rack) {
index = playerLetters.indexOf(p);
if (index !== -1) {
from = "rack";
index = playerLetters.indexOf(oldP);
break;
}
oldOldP = oldP;
oldP = p;
p = p.parentNode;
}
if (!from) {
fatalError(new Error("Error: did not find the parent of the moving tile"));
return;
}
moveCMD = {
cmd: "moveLetter",
from: from,
@ -732,6 +730,14 @@
}
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);
}
@ -745,15 +751,16 @@
switch (key) {
case "playerName":
name.textContent = value;
setSetting("trivabblePlayerName", value);
setSetting("PlayerName", value);
break;
case "gameNumber":
document.getElementById("number").textContent = value;
setSetting("trivabbleGameNumber", value);
setSetting("GameNumber", value);
break;
case "boardLang":
document.getElementById("board-lang").value = value;
setSetting("trivabbleBoardLang", value);
setSetting("BoardLang", value);
checkDictionaryExistance(value);
break;
}
}
@ -769,7 +776,7 @@
myAlert(
format(
_("You are about to leave the current game. To recover it, please note its number: {0}"),
getSetting("trivabbleGameNumber")
getSetting("GameNumber")
),
f
);
@ -850,7 +857,7 @@
chatMessages.scrollTop = chatMessages.scrollHeight;
if (sender && sender !== getSetting("trivabblePlayerName")) {
if (sender && sender !== getSetting("PlayerName")) {
if (getSetting("ENABLE_MSG_SOUND")) {
audioChat.play();
}
@ -890,7 +897,7 @@
case "changeBoardLang": {
const newLang = boardLangSelect.querySelector("[value=" + msg.specialMsg.newBoardLang + "]").textContent;
infoMessage(
(msg.sender === getSetting("trivabblePlayerName"))
(msg.sender === getSetting("PlayerName"))
? format(_("You changed the language of the board to {0}"), newLang)
: format(_("{0} changed the language of the board to {1}"), msg.sender, newLang)
);
@ -923,11 +930,14 @@
localStorage.timerTurnDate = timerDate();
}
currentTilePlayed = {};
currentPlayer = player;
refreshCurrentPlayer();
}
function getScoreCell(playerName) {
return tablePlayers[playerName].childNodes[2].childNodes[0];
}
function setPlayers(players) {
if (participantPlaceholder) {
participantPlaceholder.parentNode.removeChild(participantPlaceholder);
@ -1039,7 +1049,10 @@
}
if (Object.prototype.hasOwnProperty.call(player, "score")) {
const scoreCell = tablePlayers[playerName].childNodes[2].childNodes[0];
const scoreCell = getScoreCell(playerName);
if (parseInt(scoreCell.textContent) !== player.score) {
currentTilePlayed = {};
}
scoreCell.textContent = player.score;
blink(scoreCell);
}
@ -1054,6 +1067,15 @@
refreshCurrentPlayer();
}
function handleLastPlayer(data) {
if (data.player) {
if (lastPlayer !== data.player) {
currentTilePlayed = {};
}
lastPlayer = data.player;
}
}
function applyAction(data) {
switch (data.action) {
case "pushBag": //TODO
@ -1068,9 +1090,14 @@
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") {
@ -1078,17 +1105,32 @@
}
if (data.to === "board") {
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== getSetting("trivabblePlayerName"));
setCell(
data.indexTo,
data.letter,
data.player && data.player !== getSetting("PlayerName")
);
} 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 !== getSetting("trivabblePlayerName"));
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);
}
@ -1315,7 +1357,7 @@
closeConnections();
pollingServer = true;
eventSource = new EventSource(getApiEntryPoint() + "/sse/" + JSON.stringify(cmdsWithContext()));
eventSource = new EventSource(getSetting("API_ENTRY_POINT") + "/sse/" + JSON.stringify(cmdsWithContext()));
bindConnectionEvents(eventSource);
return;
}
@ -1331,7 +1373,7 @@
webSocket = new WebSocket(
(window.location.protocol === "http:" ? "ws://" : "wss://") +
window.location.host +
getApiEntryPoint() + "/ws/" +
Conf.API_ENTRY_POINT + "/ws/" +
JSON.stringify(cmdsWithContext())
);
@ -1345,7 +1387,7 @@
function xhrRequest(data, onreadystatechange) {
const xhr = new XMLHttpRequest();
xhr.open("POST", getApiEntryPoint(), true);
xhr.open("POST", Conf.API_ENTRY_POINT, true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(JSON.stringify(data));
@ -1443,9 +1485,9 @@
function cmdsWithContext(cmds) {
return {
gameNumber: getSetting("trivabbleGameNumber") || "",
playerName: getSetting("trivabblePlayerName"),
boardLang: getSetting("trivabbleBoardLang"),
gameNumber: getSetting("GameNumber") || "",
playerName: getSetting("PlayerName"),
boardLang: getSetting("BoardLang"),
version: VERSION,
cmds: cmds
};
@ -1477,13 +1519,13 @@
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("trivabbleGameNumber")),
_("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("trivabbleGameNumber", n);
setSetting("GameNumber", n);
location.reload();
}
}
@ -1503,7 +1545,7 @@
cell.firstChild.appendChild(document.createElement("span"));
cell.classList.add("special-cell");
cell.classList.add("special-cell-" + type);
cell.lastChild.lastChild.textContent = _(specialTypesText[type]);
cell.lastChild.lastChild.textContent = cell.title = _(specialTypesText[type]);
cell.lastChild.lastChild.className = "special-cell-label";
}
@ -1512,17 +1554,17 @@
_("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("trivabblePlayerName", newName.trim());
name.textContent = getSetting("trivabblePlayerName");
setSetting("PlayerName", newName.trim());
name.textContent = getSetting("PlayerName");
}
},
getSetting("trivabblePlayerNamer")
getSetting("PlayerNamer")
);
}
function startGame(number) {
if (number) {
setSetting("trivabbleGameNumber", number);
setSetting("GameNumber", number);
}
startConnection();
}
@ -1591,7 +1633,7 @@
sendCmds([{cmd: "changeBoard", lang: code}]);
},
function () {
boardLangSelect.value = getSetting("trivabbleBoardLang");
boardLangSelect.value = getSetting("BoardLang");
}
);
}
@ -1781,11 +1823,11 @@
}
function checkSpellingClicked() {
if (getSetting("spellCheckerEnabled") === "false") {
if (getSetting("SpellCheckerEnabledOnce") === "false") {
return;
}
getDictionary(getSetting("trivabbleBoardLang"), checkSpelling);
getDictionary(getSetting("BoardLang"), checkSpelling);
}
function checkSpelling(dictionary) {
@ -1837,6 +1879,84 @@
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)) * 15) + 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 === 7) {
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;
@ -1976,7 +2096,7 @@
if (tilesSound) {
/* migration of old settings for tiles sound */
const oldSetting = getSetting("trivabbleTilesSound");
const oldSetting = getSetting("TilesSound");
if (oldSetting) {
setSetting("ENABLE_TILE_SOUND", oldSetting === "true");
delete localStorage.trivabbleTilesSound;
@ -1992,7 +2112,7 @@
if (msgSound) {
/* migration of old settings for message sound */
const oldSetting = getSetting("trivabbleMsgSound");
const oldSetting = getSetting("MsgSound");
if (oldSetting) {
setSetting("ENABLE_MSG_SOUND", oldSetting === "true");
delete localStorage.trivabbleMsgSound;
@ -2005,14 +2125,22 @@
}
}
function initSpellChecker() {
if (getSetting("ENABLE_SPELL_CHECKER")) {
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 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) {
@ -2147,14 +2275,14 @@
}
function repromptName(f) {
if (getSetting("trivabblePlayerName") && getSetting("trivabblePlayerName").trim()) {
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 (name) {
if (name && name.trim()) {
setSetting("trivabblePlayerName", name.trim());
function (newName) {
if (newName && newName.trim()) {
setSetting("PlayerName", newName.trim());
}
repromptName(f);
@ -2203,11 +2331,11 @@
);
});
boardLangSelect.value = getSetting("trivabbleBoardLang");
boardLangSelect.value = getSetting("BoardLang");
}
function langSelectionChange(e) {
setSetting("trivabbleLang", e.target.value);
setSetting("Lang", e.target.value);
location.reload();
}
@ -2245,6 +2373,8 @@
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");
@ -2253,12 +2383,12 @@
}
function initGame() {
if (!getSetting("trivabblePlayerName")) {
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("trivabblePlayerName", name);
setSetting("PlayerName", name);
}
repromptName(initGame);
}
@ -2267,7 +2397,7 @@
return;
}
name.textContent = getSetting("trivabblePlayerName");
name.textContent = getSetting("PlayerName");
const letters = "ABCDEFGHIJKLMNO";
@ -2286,6 +2416,7 @@
};
let cell;
let row;
for (let i = 0; i < 7; i++) {
const span = document.createElement("span");
@ -2298,7 +2429,7 @@
board.rows[0].appendChild(document.createElement("th"));
board.rows[0].lastChild.textContent = i + 1;
const row = board.insertRow(-1);
row = board.insertRow(-1);
row.appendChild(document.createElement("th"));
row.lastChild.textContent = letters[i];
@ -2311,18 +2442,18 @@
cell.lastChild.className = "tile-placeholder";
if (i === j && i === 7) {
specialCell("doubleWord", board.lastChild.lastChild);
cell = board.lastChild.lastChild.getElementsByClassName("special-cell-label")[0];
specialCell("doubleWord", row.lastChild);
cell = row.lastChild.getElementsByClassName("special-cell-label")[0];
cell.textContent = "★";
row.lastChild.id = "center-cell";
} else if (i % 7 === 0 && j % 7 === 0) {
specialCell("tripleWord", board.lastChild.lastChild);
specialCell("tripleWord", row.lastChild);
} else if ((i === j || i + j === 14) && (i < 5 || i > 9)) {
specialCell("doubleWord", board.lastChild.lastChild);
specialCell("doubleWord", row.lastChild);
} else if ((i % 4 === 1) && (j % 4 === 1)) {
specialCell("tripleLetter", board.lastChild.lastChild);
specialCell("tripleLetter", row.lastChild);
} else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14 - i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
specialCell("doubleLetter", board.lastChild.lastChild);
specialCell("doubleLetter", row.lastChild);
}
}
@ -2332,7 +2463,7 @@
board.rows[0].appendChild(board.rows[0].cells[0].cloneNode(false));
const row = board.insertRow(-1);
row = board.insertRow(-1);
row.appendChild(board.rows[0].cells[0].cloneNode(false));
for (let i = 0; i < 15; i++) {
@ -2342,19 +2473,19 @@
row.appendChild(board.rows[0].cells[0].cloneNode(false));
if (getSetting("trivabbleGameNumber")) {
document.getElementById("number").textContent = getSetting("trivabbleGameNumber");
if (getSetting("GameNumber")) {
document.getElementById("number").textContent = getSetting("GameNumber");
}
if (getSetting("trivabbleBoardLang")) {
document.getElementById("board-lang").value = getSetting("trivabbleBoardLang");
if (getSetting("BoardLang")) {
document.getElementById("board-lang").value = getSetting("BoardLang");
}
startGame(getSetting("trivabbleGameNumber"));
startGame(getSetting("GameNumber"));
}
function initLang() {
const lang = libD.lang = getSetting("trivabbleLang") || libD.lang;
const lang = libD.lang = getSetting("Lang") || libD.lang;
const langSel = document.getElementById("select-lang");
langSel.value = lang;

View File

@ -3,7 +3,7 @@
"name": "Ukrainian",
"bag": [
"Jokers", "Jokers",
" ", " ",
"О", "О", "О", "О", "О", "О", "О", "О", "О",
"А", "А", "А", "А", "А", "А", "А", "А",
"И", "И", "И", "И", "И", "И",
@ -41,7 +41,7 @@
],
"letterValues": {
"Jokers": 0,
" ": 0,
"О": 1,
"А": 1,
"И": 1,

View File

@ -446,9 +446,12 @@ function countTiles(rack) {
return count;
}
function joinGame(gameNumber) {
function joinGame(gameNumber, message) {
if (!gameNumber) {
gameNumber = newGameId();
if (message) {
message.gameNumber = gameNumber;
}
}
const game = games[gameNumber] || (games[gameNumber] = new Game());
@ -719,7 +722,7 @@ function handleCommand(cmdNumber, message, response) {
function handleCommands(message, responseAndType) {
if (!message.cmds || !message.cmds.length) {
const {gameNumber, game} = joinGame(message.gameNumber);
const {gameNumber, game} = joinGame(message.gameNumber, message);
writeMessage(responseAndType,
JSON.stringify({
@ -1006,8 +1009,8 @@ function handleRequest(request, response) {
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self'; " +
(disableCSP ? "" : "connect-src 'self' " + (
// uzbl (like Safari 9 / iPad 2) does not like unsecure websockets on the
(disableCSP ? "connect-src *; " : "connect-src 'self' " + (
// uzbl (like Safari 9 / iPad 2) does not like insecure websockets on the
// same port with connect-src 'self'
// See https://github.com/w3c/webappsec-csp/issues/7
"ws://" + host + ":" + port + " " +