Here comes the Content

This commit is contained in:
Raphaël Jakse 2016-02-28 20:23:41 +01:00
parent 69e36fe19d
commit a3a8384096
21 changed files with 3069 additions and 0 deletions

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# Trivabble
Trivabble is a network Scrabble® game.
This Readme is yet to be written, however, everything is documented in French at [https://trivabble.1s.fr/](https://trivabble.1s.fr/). English is coming soon!

74
alert.css Normal file
View File

@ -0,0 +1,74 @@
.alert {
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background:rgba(0,0,0,0.2);
display:-webkit-flex;
display:flex;
-webkit-flex-direction:column;
flex-direction:column;
-webkit-justify-content:center;
justify-content:center;
line-height:1.5em;
text-align:center
}
.alert-outer {
background:white;
color:black;
border-radius:4px;
display:-webkit-flex;
display:flex;
-webkit-flex-direction:column;
flex-direction:column;
-webkit-align-self:center;
align-self:center;
text-align:center;
box-shadow:0 0 8px black;
max-width:75%;
padding:4px;
}
.alert-content {
padding:1ex
}
.alert input[type=text], .alert input[type=password] {
border-radius:none;
box-sizing:border-box;
min-height:1.5rem;
outline:none;
width:95%;
min-width:10ex;
margin:1ex;
border:1px solid silver;
}
.alert input[type=text]:focus, .alert input[type=password]:focus {
border:1px solid #5CF;
box-shadow:0 0 1px #5CF
}
.alert input[type=button], .alert button {
margin:1px;
border-radius:0;
min-height:1.5rem;
border:1px solid silver;
padding:1ex 2ex;
background:white
}
.alert input[type=button]:active, .alert button:active {
background:#D5D5D5
}
.alert button {
margin:0.5ex;
border:1px solid gray;
background:#F1F1F1;
color:black;
border-radius:3px
}

139
alert.js Normal file
View File

@ -0,0 +1,139 @@
(function (that) {
var divAlert, divAlertInput, divAlertConfirm, divAlertButton, alertButtonOK,
divAlertCallback, divAlertCallbackYes, divAlertCallbackNo, alertInput;
var _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) { return s; };
function promptOK() {
divAlert.style.display = "none";
divAlertCallback && divAlertCallback(alertInput.value);
}
function promptCancel() {
divAlert.style.display = "none";
divAlertCallback && divAlertCallback(null);
}
function confirmYes() {
divAlert.style.display = "none";
divAlertCallbackYes && divAlertCallbackYes();
}
function confirmNo() {
divAlert.style.display = "none";
divAlertCallbackNo && divAlertCallbackNo();
}
function alertOK() {
divAlert.style.display = "none";
divAlertCallback && divAlertCallback();
}
function prepare() {
divAlert = document.createElement("div");
divAlert.className = "alert";
divAlert.style.display = "none";
divAlertContent = document.createElement("div");
divAlertContent.className = "alert-content";
divAlertContent.style.whiteSpace = "pre-wrap";
divAlertInput = document.createElement("div");
divAlertInput.className = "alert-prompt";
alertInput = document.createElement("input");
alertInput.type = "text";
alertInput.onkeydown = function (e) {
if (e.keyCode === 13) {
e.preventDefault();
promptOK();
}
};
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;
divAlertConfirm = document.createElement("div");
divAlertConfirm.className = _("alert-confirm");
divAlertConfirm.appendChild(document.createElement("button"));
divAlertConfirm.lastChild.textContent = _("Oui");
divAlertConfirm.lastChild.onclick = confirmYes;
divAlertConfirm.appendChild(document.createElement("button"));
divAlertConfirm.lastChild.textContent = _("Non");
divAlertConfirm.lastChild.onclick = confirmNo;
divAlertButton = document.createElement("div");
alertButtonOK = document.createElement("button");
divAlertButton.appendChild(alertButtonOK);
alertButtonOK.textContent = _("OK");
alertButtonOK.onclick = alertOK;
var divAlertOuter = document.createElement("div");
divAlertOuter.className = "alert-outer";
divAlertOuter.appendChild(divAlertContent);
divAlertOuter.appendChild(divAlertInput);
divAlertOuter.appendChild(divAlertConfirm);
divAlertOuter.appendChild(divAlertButton);
divAlert.appendChild(divAlertOuter);
document.body.appendChild(divAlert);
}
that.myAlert = function (msg, callback) {
if (!divAlert) {
prepare();
}
divAlertContent.textContent = msg;
divAlertInput.style.display = "none";
divAlertConfirm.style.display = "none";
divAlertButton.style.display = "";
divAlertCallback = callback;
divAlert.style.display = "";
divAlertButton.getElementsByTagName("button")[0].focus();
};
that.myAlert.l10n = _;
that.myPrompt = function (msg, callback, defaultText) {
if (!divAlert) {
prepare();
}
divAlertContent.textContent = msg;
divAlertInput.style.display = "";
divAlertInput.firstChild.value = defaultText || "";
divAlertConfirm.style.display = "none";
divAlertButton.style.display = "none";
divAlertCallback = callback;
divAlert.style.display = "";
if (!/iPad|iPhone|iPod/g.test(navigator.userAgent)) {
alertInput.focus();
alertInput.setSelectionRange(0, alertInput.value.length);
alertInput.select();
}
};
that.myConfirm = function (msg, callbackYes, callbackNo) {
if (!divAlert) {
prepare();
}
divAlertContent.textContent = msg;
divAlertInput.style.display = "none";
divAlertConfirm.style.display = "";
divAlertButton.style.display = "none";
divAlertCallbackYes = callbackYes;
divAlertCallbackNo = callbackNo;
divAlert.style.display = "";
divAlertConfirm.getElementsByTagName("button")[0].focus();
};
}(this));

23
bag.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" height="319mm" width="331mm" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1172.8346 1130.315" xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs id="defs4">
<linearGradient id="linearGradient5620" y2="482.82" gradientUnits="userSpaceOnUse" x2="1120.7" gradientTransform="matrix(1.21 0 0 1.21 -122.4 -73.147)" y1="482.82" x1="56.525">
<stop id="stop5616" offset="0"/>
<stop id="stop5618" stop-color="#33333f" stop-opacity=".94118" offset="1"/>
</linearGradient>
</defs>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" transform="translate(0 77.953)">
<path id="path3348" d="m510.26 107.75-273.12 245.46-127.91 449.43 490.91 207.46 456.36-183.26-159.04-487.46-221.26-224.71 190.18-148.75c-88.433-9.4796-177.32-14.756-266.25-15.804-96.89-1.1424-193.84 2.7337-290.33 11.607l200.46 146.03z" fill-rule="evenodd" stroke="#000" stroke-width="1.21px" fill="url(#linearGradient5620)"/>
<path id="path4164" stroke-linejoin="round" d="m684.88 431.89c-6.34-58.38-23.22-115.59-49.58-168.06-29.85-59.42-71.88-112.69-122.72-155.55 22.722 2.9086 45.556 4.9435 68.435 6.0986 34.007 1.717 68.112 1.489 102.09-0.68242" stroke="#b47f58" stroke-linecap="round" stroke-width="12.1" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

7
hammer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

92
index.html Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title> Trivabble </title>
<link rel="stylesheet" href="trivabble.css" />
<link rel="stylesheet" href="alert.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<script>
window.libD = {
l10n: function () {
"use strict";
var t = [];
var f = function (lang, orig, translated) {
if (!orig) {
return (
(libD.lang && t[libD.lang] && t[libD.lang][lang])
? t[libD.lang][lang]
: lang // lang is the default string
);
}
if (!t[lang]) {
t[lang] = [];
}
t[lang][orig] = translated;
};
return f;
},
lang: (
window.navigator && (
(navigator.language || navigator.userLanguage)
? (navigator.language || navigator.userLanguage).split("-")[0].toLowerCase()
: "en"
)
) || "en"
};
</script>
<script src="alert.js"></script>
<script src="touch2click.js"></script>
<script src="trivabble.js"></script>
<script>
(function () {
var script = document.createElement("script");
script.src = "l10n/js/" + libD.lang + ".js";
script.onerror = trivabble.l10nError;
document.getElementsByTagName("head")[0].appendChild(script);
})();
</script>
</head>
<body>
<div id="menu">
<button data-l10n="text-content" id="join-game">Join adversaries</button>
<p id="p-name"> <span data-l10n="text-content">You are:</span> <span id="name" data-l10n="text-content">(name to give)</span><br /><button class="minibutton" id="change-name" data-l10n="text-content">Change</button></p>
<button id="clear-game" data-l10n="text-content">Put back all the tiles&#10;in the bag</button>
<p id="p-name"><span data-l10n="text-content">Number of your game:</span><br /><span id="number" data-l10n="text-content">(pending)</span></p>
<div id="chat-messages"></div>
<textarea id="chat-ta" placeholder="Write a message to your adversaries here" data-l10n="placeholder"></textarea>
<button id="chat-btn" class="minibutton" data-l10n="text-content">Send message</button>
<div id="prefs-sound">
<p><label><input type="checkbox" id="tiles-sound" checked="checked" /><span data-l10n="text-content">Sound of the tiles</span></label></p>
<p><label><input type="checkbox" id="msg-sound" checked="checked" /><span data-l10n="text-content">Sound of messages</span></label></p>
</div>
</div>
<div id="content">
<table id="board">
<tr><td class="corner"></td></tr>
</table>
<div id="rack-outer">
<div id="rack"></div>
</div>
</div>
<div id="panel">
<div id="bag"></div>
<p><span data-l10n="text-content">Number of tiles in the bag:</span><br /><span id="remaining-letters">102</span><br /><span id="help-bag" data-l10n="text-content">Click on it to take one.</span><a href="#" id="help-clear" style="display:none"></a></span></p>
<table id="participants">
<tr><th data-l10n="text-content">Participant</th><th data-l10n="text-content">Rack</th><th data-l10n="text-content">Score</th></tr>
<tr id="participants-placeholder"><td colspan="3" data-l10n="text-content">Waiting for other participants…</td></tr>
</table>
<p id="help-scores" data-l10n="text-content">Click on someone's score&#10;to change it.</p>
<button id="clear-rack" class="minibutton" data-l10n="text-content">Put back all the tiles of&#10;your rack in the bag</button>
</div>
</body>
</html>

3
l10n/Makefile Normal file
View File

@ -0,0 +1,3 @@
all:
nodejs makejs.js

1
l10n/js/fr.js Normal file
View File

@ -0,0 +1 @@
(function(){var _=myAlert.l10n;_("fr","OK","OK");_("fr","Yes","Oui");_("fr","No","Non");_("fr","Cancel","Annuler");_=trivabble.l10n;_("fr","Your game is not over. Are you sure you want to leave now?","Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?");_("fr","You are about to leave the current game. To recover it, please note its number: {0}","Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}");_("fr","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.","Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page.");_("fr","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}.","Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}.");_("fr","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.","Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie ».");_("fr","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.","Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom.");_("fr","You cannot take another tile: your rack is full.","Vous ne pouvez pas tirer de pièce : votre chevalet est complet.");_("fr","Are you sure you want to put all the tiles back in the bag (un order to play another game)?","Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?");_("fr","Are you sure you want to put all your tiles back in the bag?","Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?");_("fr","It seems your did not give your name. You need to do it for the game to run properly.","Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu.");_("fr","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux.");_("fr","Double\nLetter","Lettre\nDouble");_("fr","Double\nWord","Mot\nDouble");_("fr","Triple\nLetter","Lettre\nTriple");_("fr","Triple\nWord","Mot\nTriple");_("fr","Join adversaries","Rejoindre des adversaires");_("fr","You are:","Vous êtes :");_("fr","Change","Changer");_("fr","(name to give)","(nom à renseigner)");_("fr","Put back all the tiles\nin the bag","Remettre toutes les pièces\ndans le sac");_("fr","Number of your game:","Numéro de votre partie :");_("fr","(pending)","(en attente)");_("fr","Write a message to your adversaries here","Écrivez un message à vos adversaires ici");_("fr","Send message","Envoyer le message");_("fr","Sound of the tiles","Bruit des pièces");_("fr","Sound of messages","Bruit des messages");_("fr","Number of tiles in the bag:","Nombre de pièces dans le sac :");_("fr","Click on it to take one.","Cliquez dessus pour en prendre une.");_("fr","Put back all the tiles in the bag","Ranger toutes les pièces dans le sac");_("fr","Participant","Participant·e");_("fr","Rack","Chevalet");_("fr","Score","Score");_("fr","Waiting for other participants…","En attente des autres participants…");_("fr","Click on someone's score\nto change it.","Cliquez sur le score de quelqu'un\npour le changer.");_("fr","Put back all the tiles of\nyour rack in the bag","Remettre toutes les pièces du\nchevalet dans le sac");if(trivabble.applyL10n){trivabble.applyL10n();}})();

98
l10n/makejs.js Executable file
View File

@ -0,0 +1,98 @@
#!/usr/bin/env node
/* Builds translation files.*/
var fs = require('fs');
var langs = fs.readdirSync("po");
var po, i, len;
function skipLine() {
while (i < len && po[i] !== '\n') {
++i;
}
++i;
}
function skipSpaces() {
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;
}
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("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;
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();
}
}
fs.writeSync(jsFile, "if(" + translationFunction + ".applyL10n){" + translationFunction + ".applyL10n();}})();");
fs.close(jsFile);
}

11
l10n/po/fr/alert.po Normal file
View File

@ -0,0 +1,11 @@
msgid "OK"
msgstr "OK"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
msgid "Cancel"
msgstr "Annuler"

107
l10n/po/fr/trivabble.po Normal file
View File

@ -0,0 +1,107 @@
msgid "Your game is not over. Are you sure you want to leave now?"
msgstr "Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?"
msgid "You are about to leave the current game. To recover it, please note its number: {0}"
msgstr "Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}"
msgid "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."
msgstr "Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page."
msgid "Enter the new score of {0}:"
"Saississez le nouveau score de {0} :"
msgid "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}."
msgstr "Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}."
msgid "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."
msgstr "Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie »."
msgid "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."
msgstr "Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom."
msgid "You cannot take another tile: your rack is full."
msgstr "Vous ne pouvez pas tirer de pièce : votre chevalet est complet."
msgid "Are you sure you want to put all the tiles back in the bag (un order to play another game)?"
msgstr "Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?"
msgid "Are you sure you want to put all your tiles back in the bag?"
msgstr "Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?"
msgid "It seems your did not give your name. You need to do it for the game to run properly."
msgstr "Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu."
msgid "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
msgstr "Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux."
msgid "Double\nLetter"
msgstr "Lettre\nDouble"
msgid "Double\nWord"
msgstr "Mot\nDouble"
msgid "Triple\nLetter"
msgstr "Lettre\nTriple"
msgid "Triple\nWord"
msgstr "Mot\nTriple"
msgid "Join adversaries"
msgstr "Rejoindre des adversaires"
msgid "You are:"
msgstr "Vous êtes :"
msgid "Change"
msgstr "Changer"
msgid "(name to give)"
msgstr "(nom à renseigner)"
msgid "Put back all the tiles\nin the bag"
msgstr "Remettre toutes les pièces\ndans le sac"
msgid "Number of your game:"
msgstr "Numéro de votre partie :"
msgid "(pending)"
msgstr "(en attente)"
msgid "Write a message to your adversaries here"
msgstr "Écrivez un message à vos adversaires ici"
msgid "Send message"
msgstr "Envoyer le message"
msgid "Sound of the tiles"
msgstr "Bruit des pièces"
msgid "Sound of messages"
msgstr "Bruit des messages"
msgid "Number of tiles in the bag:"
msgstr "Nombre de pièces dans le sac :"
msgid "Click on it to take one."
msgstr "Cliquez dessus pour en prendre une."
msgid "Put back all the tiles in the bag"
msgstr "Ranger toutes les pièces dans le sac"
msgid "Participant"
msgstr "Participant·e"
msgid "Rack"
msgstr "Chevalet"
msgid "Score"
msgstr "Score"
msgid "Waiting for other participants…"
msgstr "En attente des autres participants…"
msgid "Click on someone's score\nto change it."
msgstr "Cliquez sur le score de quelqu'un\npour le changer."
msgid "Put back all the tiles of\nyour rack in the bag"
msgstr "Remettre toutes les pièces du\nchevalet dans le sac"

13
l10n/pot/alert.pot Normal file
View File

@ -0,0 +1,13 @@
#TranslationFunction myAlert
msgid "OK"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
msgid "Cancel"
msgstr ""

113
l10n/pot/trivabble.pot Normal file
View File

@ -0,0 +1,113 @@
#TranslationFunction trivabble
msgid "Your game is not over. Are you sure you want to leave now?"
msgstr ""
msgid "You are about to leave the current game. To recover it, please note its number: {0}"
msgstr ""
msgid "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."
msgstr ""
msgid "Enter the new score of {0}:"
"Saississez le nouveau score de {0} :"
msgid "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}."
msgstr ""
msgid "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."
msgstr ""
msgid "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."
msgstr ""
msgid "You cannot take another tile: your rack is full."
msgstr ""
msgid "Are you sure you want to put all the tiles back in the bag (un order to play another game)?"
msgstr ""
msgid "Are you sure you want to put all your tiles back in the bag?"
msgstr ""
msgid "It seems your did not give your name. You need to do it for the game to run properly."
msgstr ""
msgid "Hello! To begin, enter your name. Your adversaries will see this name when you play with them."
msgstr ""
msgid "Double\nLetter"
msgstr ""
msgid "Double\nWord"
msgstr ""
msgid "Triple\nLetter"
msgstr ""
msgid "Triple\nWord"
msgstr ""
msgid "Join adversaries"
msgstr ""
msgid "You are:"
msgstr ""
msgid "Change"
msgstr ""
msgid "(name to give)"
msgstr ""
msgid "Put back all the tiles\nin the bag"
msgstr ""
msgid "Number of your game:"
msgstr ""
msgid "(pending)"
msgstr ""
msgid "Write a message to your adversaries here"
msgstr ""
msgid "Send message"
msgstr ""
msgid "Sound of the tiles"
msgstr ""
msgid "Sound of messages"
msgstr ""
msgid "Number of tiles in the bag:"
msgstr ""
msgid "Click on it to take one."
msgstr ""
msgid "Put back all the tiles in the bag"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Rack"
msgstr ""
msgid "Score"
msgstr ""
msgid "Waiting for other participants…"
msgstr ""
msgid "Click on someone's score\nto change it."
msgstr ""
#. The part of the sentence before and after line break should not
#. be longer than the original message.
#. It is okay to put line breaks anywhere to split the sentence so it is not
#. wider than the original sentence.
msgid "Put back all the tiles of\nyour rack in the bag"
msgstr ""

BIN
notification.mp3 Normal file

Binary file not shown.

BIN
notification.ogg Normal file

Binary file not shown.

BIN
receive.mp3 Normal file

Binary file not shown.

BIN
receive.ogg Normal file

Binary file not shown.

210
touch2click.js Normal file
View File

@ -0,0 +1,210 @@
//thx http://stackoverflow.com/questions/1517924/javascript-mapping-touch-events-to-mouse-events#1781750
function touchHandler(event) {
var 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 = "";
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;
}
//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();
}
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
// default dblclick distance to within 40x40 pixel area
distance = Math.abs(+distance) || 40;//px
// Date.now() polyfill
var now = Date.now || function() {
return +new Date();
};
var cancelEvent = function(e) {
e = (e || window.event);
if (e) {
if (e.preventDefault) {
e.stopPropagation();
e.preventDefault();
} else {
try {
e.cancelBubble = true;
e.returnValue = false;
} catch (ex) {
// IE6
}
}
}
return false;
};
var taps = 0,
last = 0,
// NaN will always test false
x = NaN,
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;
if ((last + speed) > time &&
Math.abs(nextX - x) < distance &&
Math.abs(nextY - y) < distance) {
// continue series
taps++;
} else {
// reset series if too slow or moved
taps = 1;
}
// update starting stats
last = time;
x = nextX;
y = nextY;
// fire tap event
if (document.createEvent) {
e2 = document.createEvent('MouseEvents');
e2.initMouseEvent(
'tap',
true, // click bubbles
true, // click cancelable
e.view, // copy view
taps, // click count
touch.screenX, // copy coordinates
touch.screenY,
touch.clientX,
touch.clientY,
e.ctrlKey, // copy key modifiers
e.altKey,
e.shiftKey,
e.metaKey,
e.button, // copy button 0: left, 1: middle, 2: right
e.relatedTarget); // copy relatedTarget
if (!target.dispatchEvent(e2)) {
// pass on cancel
cancelEvent(e);
}
} else {
e.detail = taps;
// manually bubble up
parent = target;
while (parent && !parent.tap && !parent.ontap) {
parent = parent.parentNode || parent.parent;
}
if (parent && parent.tap) {
// DOM Level 0
parent.tap(e);
} else if (parent && parent.ontap) {
// DOM Level 0, IE
parent.ontap(e);
} else if (typeof jQuery !== 'undefined') {
// cop out and patch IE6-8 with jQuery
jQuery(this).trigger('tap', e);
}
}
if (taps === 2) {
// fire dbltap event only for 2nd click
if (document.createEvent) {
e2 = document.createEvent('MouseEvents');
e2.initMouseEvent(
'dbltap',
true, // dblclick bubbles
true, // dblclick cancelable
e.view, // copy view
taps, // click count
touch.screenX, // copy coordinates
touch.screenY,
touch.clientX,
touch.clientY,
e.ctrlKey, // copy key modifiers
e.altKey,
e.shiftKey,
e.metaKey,
e.button, // copy button 0: left, 1: middle, 2: right
e.relatedTarget); // copy relatedTarget
if (!target.dispatchEvent(e2)) {
// pass on cancel
cancelEvent(e);
}
} else {
e.detail = taps;
// manually bubble up
parent = target;
while (parent && !parent.dbltap && !parent.ondbltap) {
parent = parent.parentNode || parent.parent;
}
if (parent && parent.dbltap) {
// DOM Level 0
parent.dbltap(e);
} else if (parent && parent.ondbltap) {
// DOM Level 0, IE
parent.ondbltap(e);
} else if (typeof jQuery !== 'undefined') {
// cop out and patch IE6-8 with jQuery
jQuery(this).trigger('dbltap', e);
}
}
}
};
}()), false);
document.addEventListener("tap", touchHandler, true);
// document.addEventListener("dbltap", touchHandler, true);
document.addEventListener("touchstart", touchHandler, true);
document.addEventListener("touchmove", touchHandler, true);
document.addEventListener("touchend", touchHandler, true);
document.addEventListener("touchcancel", touchHandler, true);

705
trivabble-server.js Normal file
View File

@ -0,0 +1,705 @@
/**
* Copyright (C) 2016 Raphaël Jakse <raphael.jakse@gmail.com>
*
* @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/
*/
const port = 3000;
const SAVE_TIMEOUT = 5000;
var http = require("http");
var fs = require("fs");
var 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"
];
var 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
}
var games = {};
var dateNow = 0;
var saveTo = null;
function saveGames() {
fs.writeFile('games.backup.json', JSON.stringify(games), function (err) {
if (err) {
console.error('ERROR: Cannot save games!');
}
});
saveTo = null;
}
function Game() {
this.letterValues = frValues;
this.init();
this.listeningPlayers = [];
this.pendingEvents = [];
}
function writeResponse(response, data, terminate) {
response[terminate ? "end" : "write"](data.length + data);
}
function newBoard() {
var res = new Array(15 * 15);
for (var i = 0; i < 15 * 15; i++) {
res[i] = "";
}
return res;
}
Game.prototype.init = function () {
this.board = newBoard();
this.bag = frBag.slice();
this.remainingLetters = this.bag.length;
this.racks = {};
this.scores = {};
}
Game.prototype.toJSON = function () {
return {
remainingLetters: this.remainingLetters,
board: this.board,
bag: this.bag,
racks: this.racks,
scores: this.scores
};
};
Game.fromJSON = function (obj) {
var game = new Game();
game.board = obj.board || newBoard();
game.bag = obj.bag || frBag.slice();
game.remainingLetters = obj.remainingLetters || game.bag.length;
game.racks = obj.racks || {};
game.scores = obj.scores || {}
return game;
};
Game.prototype.getPlayerRack = function (player) {
var playerID = "#" + player;
return (this.racks[playerID] || (this.racks[playerID] = []));
};
Game.prototype.getPlayerScore = function (player) {
var playerID = "#" + player;
return (this.scores[playerID] || (this.scores[playerID] = 0));
};
Game.prototype.setPlayerScore = function (player, score) {
var playerID = "#" + player;
if (!this.racks.hasOwnProperty(playerID) || typeof score !== "number") {
return;
}
this.scores[playerID] = score;
this.pendingEvents.push(
{
players: [{
player: player,
score: score
}],
date: dateNow
}
);
};
Game.prototype.playerJoined = function (playerName) {
if (playerName) {
this.getPlayerRack(playerName); // create the player's rack
}
var players = [];
for (var player in this.racks) {
if (this.racks.hasOwnProperty(player)) {
player = player.slice(1); // '#'
players.push(
{
player: player,
score: this.getPlayerScore(player),
rackCount: countTiles(this.getPlayerRack(player))
}
);
}
}
this.pendingEvents.push(
{
players: players,
date: dateNow
}
);
};
Game.prototype.addListeningPlayer = function (playerName, response) {
var that = this;
that.listeningPlayers.push(response);
response.on("close", function () {
var index = that.listeningPlayers.indexOf(response);
if (index !== -1) {
that.listeningPlayers[index] = that.listeningPlayers[that.listeningPlayers.length - 1];
that.listeningPlayers.pop();
}
});
this.playerJoined(playerName);
this.commit();
};
Game.prototype.commit = function () {
var pendingEvents = this.pendingEvents;
this.pendingEvents = [];
msg = JSON.stringify(pendingEvents);
for (var i = 0; i < this.listeningPlayers.length; i++) {
while (i < this.listeningPlayers.length && !this.listeningPlayers[i]) {
this.listeningPlayers[i] = this.listeningPlayers[this.listeningPlayers.length - 1];
that.listeningPlayers.pop();
}
if (this.listeningPlayers[i]) {
writeResponse(this.listeningPlayers[i], msg);
}
}
if (saveTo === null) {
saveTo = setTimeout(saveGames, SAVE_TIMEOUT);
}
};
Game.prototype.bagPopLetter = function (player) {
if (this.remainingLetters) {
var letter = "";
var index
while (letter === "") {
index = Math.floor(Math.random() * this.bag.length);
letter = this.bag[index];
}
this.bag[index] = "";
this.remainingLetters--;
this.pendingEvents.push(
{
player: player,
action: "popBag",
remainingLetters: this.remainingLetters,
date: dateNow
}
);
return letter;
}
return "";
};
Game.prototype.getCell = function (index) {
return this.board[index];
}
Game.prototype.setCell = function (index, letter, player) {
this.board[index] = letter;
this.pendingEvents.push(
{
player:player,
action:"setCell",
indexTo:index,
letter:letter,
date: dateNow
}
);
};
Game.prototype.bagPushLetter = function (letter, player) {
if (letter) {
var index = 0;
while (this.bag[index] !== "") {
index++;
}
this.bag[index] = letter;
this.remainingLetters++;
this.pendingEvents.push(
{
player: player,
action: "pushBag",
remainingLetters: this.remainingLetters,
date: dateNow
}
);
}
};
Game.prototype.reset = function (player) {
this.init();
this.pendingEvents.push(
{
player: player,
action: "reset",
board: this.board,
remainingLetters: this.remainingLetters,
rack: [],
date: dateNow
}
);
this.playerJoined();
};
function newGameId() {
var number;
var k = 10000;
var retries = 0;
do {
number = Math.floor(Math.random() * k).toString();
if (retries > 10) {
retries = 0;
k *= 10;
} else {
retries++;
}
} while (games[number]);
return number.toString();
}
destToAction = {
"rack": "setRackCell",
"board": "setCell",
"bag": "pushBag"
}
function countTiles(rack) {
var count = 0;
for (var i = 0; i < rack.length; i++) {
if (rack[i]) {
count++;
}
}
return count;
}
function handleCommand(cmd, gameNumber, playerName, response) {
var game = games[gameNumber];
if (!game && (cmd.cmd !== "joinGame")) {
response.write('{"error":1,"reason":"Missing or bad game number"}');
return;
}
var rack = null;
switch (cmd.cmd) {
case "joinGame":
if (!gameNumber) {
gameNumber = newGameId();
}
if (!game) {
game = games[gameNumber] = new Game();
}
game.playerJoined(playerName);
response.write(
JSON.stringify(
{
"error":0,
"gameNumber": gameNumber,
"playerName": playerName,
"rack": game.getPlayerRack(playerName),
"board": game.board,
"remainingLetters": game.remainingLetters,
"letterValues": frValues,
"date": dateNow,
"sync": false
}
)
);
break;
case "hello":
game.playerJoined(playerName);
break;
case "sync":
response.write(
JSON.stringify(
{
"error":0,
"gameNumber": gameNumber,
"playerName": playerName,
"rack": game.getPlayerRack(playerName),
"board": game.board,
"remainingLetters": game.remainingLetters,
"letterValues": frValues,
"date": dateNow,
"sync": true
}
)
);
break;
case "score":
game.setPlayerScore(cmd.player, cmd.score);
response.write('{"error":0}');
break;
case "moveLetter":
var letter = "";
switch (cmd.from) {
case "rack":
rack = game.getPlayerRack(playerName);
letter = rack[cmd.indexFrom];
rack[cmd.indexFrom] = "";
updateRack = true;
break;
case "board":
if (cmd.indexFrom < 15 * 15) {
letter = game.getCell(cmd.indexFrom);
game.setCell(cmd.indexFrom, "", playerName);
}
break;
case "bag":
letter = game.bagPopLetter(playerName);
break;
default:
response.write('{"error":1, "reason":"Moving letter from an unknown place"}');
return;
}
switch (cmd.to) {
case "rack":
if (cmd.indexTo > 7) {
response.write('{"error":1, "reason":"Moving letter to a index which is too high"}');
return;
}
rack = rack || game.getPlayerRack(playerName);
rack[cmd.indexTo] = letter;
break;
case "board":
if (cmd.indexTo < 15 * 15) {
game.setCell(cmd.indexTo, letter, playerName);
}
break;
case "bag":
game.bagPushLetter(letter, playerName);
break;
default:
switch (cmd.from) {
case "rack":
rack[cmd.indexFrom] = letter;
break;
case "board":
game.setCell(cmd.indexFrom, letter, playerName);
break;
case "bag":
game.bagPushLetter(letter, playerName);
break;
default:
console.error("BUG: code should not have been reached.");
}
response.write('{"error":1, "reason":"Moving letter to an unknown place"}');
return;
}
response.write(
JSON.stringify(
{
error:0,
letter: letter,
action: "moveLetter",
indexTo: typeof cmd.indexTo === "number" ? cmd.indexTo : -1,
from: cmd.from,
to: cmd.to,
indexFrom: typeof cmd.indexFrom === "number" ? cmd.indexFrom : -1,
remainingLetters: game.remainingLetters,
date: dateNow
}
)
);
if (rack) {
game.pendingEvents.push(
{
players: [{
player: playerName,
rackCount: countTiles(rack)
}],
date: dateNow
}
);
}
break;
case "setRack":
cmd.rack.length = 8;
var rack = game.getPlayerRack(playerName);
var initialRackCount = countTiles(rack);
var rackCount = countTiles(cmd.rack);
console.log(rack, cmd.rack);
if (initialRackCount !== rackCount) {
response.write(
JSON.stringify({
error: 1,
rack: rack,
reason: "the new rack doesn't contain the same number of letters",
date: dateNow
})
);
break;
}
var oldRack = rack.slice();
for (var i = 0; i < 8; i++) {
cmd.rack[i] = (cmd.rack[i] || "")[0] || "";
if (cmd.rack[i]) {
var indexLetter = oldRack.indexOf(cmd.rack[i]);
if (indexLetter === -1) {
response.write(
JSON.stringify({
error: 1,
rack: rack,
reason: "the new rack is not a permutation of the old rack",
date: dateNow
})
);
break;
}
oldRack[indexLetter] = false;
}
}
for (var i = 0; i < 8; i++) {
rack[i] = cmd.rack[i];
}
response.write(
JSON.stringify({
error: 0,
rack: rack,
date: dateNow
})
);
break;
case "resetGame":
game.reset();
response.write('{"error":0}');
break;
case "msg":
game.pendingEvents.push(
{
msg: {
sender: playerName,
content: cmd.msg
},
date: dateNow
}
);
response.write('{"error":0}');
break;
default:
response.write('{"error":1,"reason":"Unknown command"}');
break;
}
}
function handleCommands(cmds, response) {
if (!cmds.cmds || !cmds.cmds.length) {
var game = games[cmds.gameNumber];
if (!game) {
writeResponse(response, '{"error":1,"reason":"Missing or bad game number"}', true);
return;
}
writeResponse(response,
JSON.stringify(
{
"playerName": cmds.playerName,
"gameNumber": cmds.gameNumber,
"letterValues": game.letterValues,
"rack": game.getPlayerRack(cmds.playerName),
"board": game.board,
"remainingLetters": game.remainingLetters,
"date": dateNow
}
)
);
game.addListeningPlayer(cmds.playerName, response);
return;
}
response.write("[");
for (var i = 0; i < cmds.cmds.length; i++) {
if (i) {
response.write(",");
}
handleCommand(cmds.cmds[i], cmds.gameNumber, cmds.playerName, response);
}
response.end("]");
if (games[cmds.gameNumber]) {
console.log("pendingEvents COMMIT", games[cmds.gameNumber].pendingEvents);
games[cmds.gameNumber].commit();
}
}
function handleRequest(request, response) {
var post = '';
// thx http://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js
request.on("data", function (data) {
post += data;
// Too much POST data, kill the connection!
if (post.length > 1e6) {
request.connection.destroy();
}
});
request.on("end", function () {
response.setHeader("Content-Type", "text/plain");
response.setHeader("Transfer-Encoding", "chunked");
dateNow = Date.now();
console.log("RECEIVED", post);
handleCommands(post && JSON.parse(post), response);
});
}
fs.readFile('games.backup.json', function (err, data) {
try {
if (err) {
console.error("WARNING: Could not restore previous backup of the games");
} else {
var backup = JSON.parse(data);
for (var gameNumber in backup) {
games[gameNumber] = Game.fromJSON(backup[gameNumber]);
}
}
} catch (e) {
console.error("WARNING: Could not restore previous backup of the games: file is broken:");
console.error("WARNING: ", e);
}
http.createServer(handleRequest).listen(port, function() {
console.log("Server listening on: http://localhost:%s", port);
});
});

380
trivabble.css Normal file
View File

@ -0,0 +1,380 @@
#board {
border-collapse:collapse;
}
a {
text-decoration:none;
color:blue;
}
html {
font-family: "Droid Sans", sans-serif;
}
html, #board, [draggable], .tile {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
user-select: none;
}
#chat-messages, #number, #name {
-webkit-touch-callout: default;
-webkit-user-select: element;
-khtml-user-select: element;
-moz-user-select: element;
-ms-user-select: element;
user-select: element;
-moz-user-select: element;
-khtml-user-select: element;
-webkit-user-select: element;
user-select: element;
}
#clear-game, #help-scores, #clear-rack {
white-space:pre-wrap;
}
[draggable] {
/* Required to make elements draggable in old WebKit */
-khtml-user-drag: element;
-webkit-user-drag: element;
}
#help-clear, #help-bag, #help-scores {
font-size:small
}
#board td {
text-transform:uppercase;
border:1px solid #220;
background:#FFC;
vertical-align:middle;
text-align:center;
}
#board td .tile-placeholder {
position: relative;
height: 100%;
width: 100%;
top: 0px;
left: 0px;
display: flex;
justify-content: center;
flex-direction: column;
}
#board th {
font-size:small;
color:gray;
font-weight:normal
}
.corner {
visibility:hidden;
border:none !important
}
.special-cell-tripleWord {
background:#F77 !important;
box-shadow: 0 0 1px #F77;
}
.special-cell-tripleLetter {
background:#77F !important;
box-shadow: 0 0 1px #77F;
}
.special-cell-doubleLetter {
background:#7CF !important;
box-shadow: 0 0 1px #7CF;
}
.special-cell-doubleWord {
background:#FBB !important;
box-shadow: 0 0 1px #FBB;
}
.special-cell-label {
display:inline-block;
white-space:pre-wrap;
font-size:7px;
}
#center-cell .special-cell-label {
font-weight:bold;
color:black;
font-size:large
}
body {
display:-webkit-flex;
display:flex;
-webkit-flex-direction:row;
flex-direction:row
}
#menu, #panel {
display:-webkit-flex;
display:flex;
-webkit-flex-direction:column;
flex-direction:column
}
button {
margin: 0.25em 0;
min-height:3em;
background:#AFA;
border:1px solid gray;
}
button:hover {
background:#FD7;
cursor:pointer
}
#rack {
background:#EEC;
font-size:0;
border-radius:4px;
display:inline-block;
padding:2px 5px;
white-space:pre;
box-shadow:0 0 2px gray
}
#board td {
height:32px;
width: 32px;
}
#rack .tile-placeholder {
height:32px;
width: 32px;
}
.tile {
background-color: #EE8;
transition:background-color 1s, border 1s;
display: inline-block;
width: 95%;
height: 95%;
left: 2.5%;
top: 2.5%;
position: absolute;
border: 1px solid #AA5;
box-sizing: border-box;
text-align: center;
font-size:1rem;
cursor:pointer
}
.tile.tile-highlight {
background-color: #FFF;
font-weight: bold;
animation: highlight 4s ease-in;
border: 1px solid black;
}
@keyframes highlight {
0% {
background:red;
color:white;
}
100% {
background:white
}
}
.tile-score {
font-size: 0.5em;
position: absolute;
bottom: 1px;
right: 2px;
}
#chat-messages.blink {
animation:chat-blink 1s
}
td.blink {
animation:td-blink 1s
}
@keyframes chat-blink {
0% {
background:rgba(255, 255, 150, 0);
}
50% {
background:rgba(255, 255, 150, 1);
}
100% {
background:rgba(255, 255, 150, 0);
}
}
@keyframes td-blink {
0% {
background:rgba(100, 200, 255, 0);
}
50% {
background:rgba(100, 200, 255, 1);
}
100% {
background:rgba(100, 200, 255, 0);
}
}
.tile-target {
background:rgba(255,255,255, 0.9);
/* } */
/* #bag.tile-target, td .tile-target { */
outline: 1px solid #F33
}
#rack .tile-placeholder {
position:relative;
margin-left:1px;
margin-right:1px;
display:inline-block;
vertical-align:middle;
background:#CCA;
}
#panel {
-webkit-justify-content:center;
justify-content:center;
text-align:center
}
#bag, #rack-outer {
-webkit-align-self:center;
align-self:center;
text-align:center;
}
#p-name {
text-align:center;
font-size:small
}
.minibutton {
padding:0;
min-height:0;
font-size:small
}
.msg-sender {
color:blue;
}
#chat-messages {
overflow:auto
}
#bag {
width:165px;
height:160px;
background:url(bag.svg) no-repeat;
background-size:100% 100%
}
#bag:hover {
cursor:pointer;
opacity:0.8
}
#chat-messages {
border:1px solid silver;
box-sizing:border-box;
width: 100%;
margin-bottom:1px;
height:15em;
}
#chat-ta {
width:100%;
box-sizing:border-box;
height:3em
}
#participants {
font-size:small;
border-collapse:collapse;
margin:0 auto 1ex auto;
}
#participants th, #participants td {
border:1px solid gray;
padding:0.33ex 1ex
}
#participants td:nth-child(3) {
min-width:4em;
}
#participants td:nth-child(3):hover {
cursor:pointer;
background:#FFA
}
#participants th {
font-weight:normal;
background:#EEE;
font-size:smaller
}
#participants-placeholder {
font-style:italic;
}
@media screen and (max-height:650px) {
#board td , #rack .tile-placeholder {
height:28px;
width: 28px;
}
}
@media screen and (max-height:600px) {
#board td , #rack .tile-placeholder {
height:24px;
width: 24px;
}
}
@media screen and (max-height:550px) {
#chat-messages {
height:7em;
}
}
#prefs-sound {
margin:1ex 0 0 0;
}
#prefs-sound p {
margin:0;
padding:0;
font-size:small
}
#prefs-sound input, #prefs-sound label {
vertical-align:middle
}

1087
trivabble.js Normal file

File diff suppressed because it is too large Load Diff