Ver código fonte

[add] homepage
[add] stop polling on grid terminated
[refactor] reviewed js hierarchy
[add] prepared multi-sourced crossword

isundil 9 anos atrás
pai
commit
8598bc84d3

+ 3 - 3
css/grid.less

@@ -52,7 +52,7 @@
 
         &.definition-right-vt, &.definition-right-hz, &.definition-bottom-vt, &.definition-bottom-hz {
             position: relative;
-            &:after {
+            &:after,&:before {
                 position: absolute;
                 height: @cell-size /6;
                 width: @cell-size /6;
@@ -60,7 +60,7 @@
             }
         }
 
-        &.definition-right-vt:after {
+        &.definition-right-vt:before {
             top: 0;
             bottom: 0;
             left: 0;
@@ -69,7 +69,7 @@
             border-top: 1px solid @arrow-color;
             border-right: 1px solid @arrow-color;
         }
-        &.definition-right-hz:after {
+        &.definition-right-hz:before {
             top: 0;
             bottom: 0;
             left: 0;

+ 77 - 0
css/index.less

@@ -0,0 +1,77 @@
+
+body.home {
+    @grid-size: 150px;
+
+    .grids-wrapper {
+        text-align: center;
+    }
+
+    .grids {
+        list-style: none;
+        padding: 0;
+        margin: auto;
+        max-width: 1640px;
+        width: 100%;
+    }
+
+    .grid {
+        display: inline-block;
+        height: @grid-size;
+        width: @grid-size;
+        overflow: hidden;
+        margin: 0.25em;
+        background-size: 125%;
+        background-repeat: no-repeat;
+        background-position: center;
+        border: 1px solid @border-color;
+
+        transition: background-size 0.4s ease-in;
+
+        &:hover {
+            background-size: 150%;
+        }
+
+        &.crossword {
+            background-image: url("img/crosswords.png");
+        }
+
+        & > a {
+            position: relative;
+            display: inline-block;
+            height: @grid-size - 85px;
+            width: 100%;
+            padding-top: 85px;
+
+            text-decoration: none;
+            color: black;
+        }
+    }
+
+    .grid-difficulty {
+        position: absolute;
+        right: -40px;
+        top: 20px;
+        width: 100%;
+        transform: rotate(45deg);
+        background-color: rgba(255, 135, 135, 0.75);
+        color: white;
+        font-weight: 700;
+        text-align: center;
+    }
+
+    .grid-provider,.grid-date {
+        display: block;
+        padding: 0.25em 0;
+        text-align: center;
+        background: rgba(0, 0, 0, 0.5);
+        color: white;
+    }
+
+    .grid-provider {
+    }
+
+    .grid-date {
+        font-weight: 700;
+    }
+}
+

+ 5 - 3
css/main.less

@@ -1,7 +1,4 @@
 
-@import "scoreboard.less";
-@import "grid.less";
-
 @cell-size: 75px;
 @border-color: darkblue;
 @arrow-color: darkblue;
@@ -19,3 +16,8 @@ html,body {
     display: none;
 }
 
+@import "main.less";
+@import "scoreboard.less";
+@import "grid.less";
+@import "index.less";
+

+ 28 - 14
js/Makefile

@@ -1,31 +1,45 @@
 
+CORE_SRC=	\
+			core/ui.js				\
+			core/network.js
+
 SRC=		\
-			resources.js		\
-			ui.js				\
-			uiScoreboard.js		\
-			uiGrid.js			\
-			ui.js				\
-			Grid.js				\
-			Player.js				\
-			polling.js			\
-			workflow.js
+			game/resources.js		\
+			game/uiScoreboard.js	\
+			game/uiGrid.js			\
+			game/Grid.js			\
+			game/Player.js			\
+			game/polling.js			\
+			game/workflow.js
+
+SRC_INDEX=	index/resources.js		\
+			index/ui.js				\
+			index/workflow.js
 
 OUTPUT=		../public/crosswords.min.js
 
+OUTPUT_INDEX=	../public/index.min.js
+
 CLOSURE=	closure-compiler-v20170218.jar
 
-all:
-	java -jar ${CLOSURE} --compilation_level ADVANCED --language_in=ECMASCRIPT5_STRICT --warning_level=VERBOSE --js_output_file ${OUTPUT} ${SRC}
+all: game index
+
+game:
+	java -jar ${CLOSURE} --compilation_level ADVANCED --language_in=ECMASCRIPT5_STRICT --warning_level=VERBOSE --js_output_file ${OUTPUT} ${CORE_SRC} ${SRC}
+
+index:
+	java -jar ${CLOSURE} --compilation_level ADVANCED --language_in=ECMASCRIPT5_STRICT --warning_level=VERBOSE --js_output_file ${OUTPUT_INDEX} ${CORE_SRC} ${SRC_INDEX}
 
 debug:
-	java -jar ${CLOSURE} --compilation_level WHITESPACE_ONLY --language_in=ECMASCRIPT5_STRICT --js_output_file ${OUTPUT} ${SRC}
+	java -jar ${CLOSURE} --compilation_level WHITESPACE_ONLY --language_in=ECMASCRIPT5_STRICT --js_output_file ${OUTPUT} ${CORE_SRC} ${SRC}
+	java -jar ${CLOSURE} --compilation_level WHITESPACE_ONLY --language_in=ECMASCRIPT5_STRICT --js_output_file ${OUTPUT_INDEX} ${CORE_SRC} ${SRC_INDEX}
 
 $OUTPUT: all
 
 clean:
-	$(RM) $(OUTPUT)
+	$(RM) $(OUTPUT) $(OUTPUT_INDEX)
 
 re:	clean all
 
-.PHONY: all debug clean re
+.PHONY: all debug clean re index game
 

+ 22 - 0
js/core/network.js

@@ -0,0 +1,22 @@
+
+function doGet(url, callback) {
+    var xhr = new XMLHttpRequest();
+    xhr.onreadystatechange = function(e) {
+        if (xhr.readyState === 4) {
+            var resp = null;
+
+            if (xhr.status === 200) {
+                resp = xhr.response;
+                try {
+                    resp = JSON.parse(/** @type {string} */ (resp));
+                } catch (e) {
+                    resp = null;
+                }
+            }
+            callback(xhr.status, resp);
+        }
+    };
+    xhr.open('GET', url, true);
+    xhr.send(null);
+}
+

+ 1 - 0
js/ui.js → js/core/ui.js

@@ -1,4 +1,5 @@
 
 function dCreate(domName) { return document.createElement(domName); }
 function dGet(id) { return document.getElementById(id); }
+function dFrag() { return document.createDocumentFragment(); }
 

+ 0 - 0
js/Grid.js → js/game/Grid.js


+ 0 - 0
js/Player.js → js/game/Player.js


+ 0 - 22
js/polling.js → js/game/polling.js

@@ -2,27 +2,6 @@
 /** @const @type {number} */
 var POLL_INTERVAL = 5000;
 
-function doGet(url, callback) {
-    var xhr = new XMLHttpRequest();
-    xhr.onreadystatechange = function(e) {
-        if (xhr.readyState === 4) {
-            var resp = null;
-
-            if (xhr.status === 200) {
-                resp = xhr.response;
-                try {
-                    resp = JSON.parse(/** @type {string} */ (resp));
-                } catch (e) {
-                    resp = null;
-                }
-            }
-            callback(xhr.status, resp);
-        }
-    };
-    xhr.open('GET', url, true);
-    xhr.send(null);
-}
-
 function initPolling() {
     lazyGetPseudonyme(function(pseudo) {
         if (pseudo) {
@@ -77,7 +56,6 @@ function stopPolling() {
         clearTimeout(pollNow.pollSchedule);
         pollNow.pollSchedule = 0;
     }
-    console.log("stop polling");
 }
 
 function scheduleNextPoll() {

+ 0 - 0
js/resources.js → js/game/resources.js


+ 1 - 3
js/uiGrid.js → js/game/uiGrid.js

@@ -29,7 +29,7 @@ function uiCreateCell(cellData, x, y) {
 }
 
 function uiCreateGrid() {
-    var frag = document.createDocumentFragment();
+    var frag = dFrag();
 
     dGet(R.id.scoreboard.header.title).textContent = GRID.title;
     dGet(R.id.scoreboard.header.difficulty).textContent = GRID.difficulty;
@@ -127,8 +127,6 @@ function inputCell(x, y) {
 
     // Display mobile / tablet keyboard
     moveInput(CURRENTINPUT);
-
-    console.log("Current index: ", SELECTED_INDEX);
 }
 
 function uiSelectWritableCell(direction) {

+ 0 - 0
js/uiScoreboard.js → js/game/uiScoreboard.js


+ 1 - 1
js/workflow.js → js/game/workflow.js

@@ -108,7 +108,7 @@ document.addEventListener('DOMContentLoaded', function() {
                     onGridUpdated();
                     uiSelectWritableCell(1);
                 }
-            }
+            } // TODO catch arrow keys
         }
     });
     initPolling();

+ 19 - 0
js/index/resources.js

@@ -0,0 +1,19 @@
+
+var R = {
+    id: {
+        gridList: "gridList"
+    }
+    ,klass: {
+        grid: {
+            item: 'grid'
+            ,level: 'grid-difficulty'
+            ,provider: 'grid-provider'
+            ,date: 'grid-date'
+
+            ,type: {
+                crossword: "crossword"
+            }
+        }
+    }
+};
+

+ 36 - 0
js/index/ui.js

@@ -0,0 +1,36 @@
+
+function yymmddToText(yymmdd) {
+    var months = [ "janvier", "fevrier", "mars", "avril", "mai", "juin", "juillet", "aout", "septembre", "octobre", "novembre", "decembre" ];
+    return '' +parseInt(yymmdd.substr(4, 2), 10) +' ' +months[parseInt(yymmdd.substr(2, 2), 10)] +' 20' +yymmdd.substr(0, 2);
+}
+
+/**
+ * @param {*} gridObj
+ * @return {Element}
+**/
+function makeGridListItem(gridObj) {
+    var dom = dCreate("li")
+        ,link = dCreate("a")
+        ,difficulty = dCreate("span")
+        ,provider = dCreate("span")
+        ,day = dCreate("span");
+
+    dom.className = R.klass.grid.item +' ' +R.klass.grid.type.crossword;
+    link.href = "api/create?gridProvider=" +gridObj["providerId"] +"&gridId=" +gridObj["id"];
+
+    difficulty.className = R.klass.grid.level;
+    difficulty.textContent = gridObj["level"];
+    link.appendChild(difficulty);
+
+    provider.className = R.klass.grid.provider;
+    provider.textContent = gridObj["provider"];
+    link.appendChild(provider);
+
+    day.className = R.klass.grid.date;
+    day.textContent = yymmddToText('' +gridObj["date"]);
+    link.appendChild(day);
+
+    dom.appendChild(link);
+    return dom;
+};
+

+ 29 - 0
js/index/workflow.js

@@ -0,0 +1,29 @@
+
+var
+/** @const @type {string} */
+GRID_LIST_ENDPOINT = '/api/list'
+;
+
+document.addEventListener('DOMContentLoaded', function() {
+    doGet(GRID_LIST_ENDPOINT, function(st, resp) {
+        if (resp) {
+            resp.sort(function(a, b) {
+                if (a["date"] !== b["date"])
+                    return b["date"] -a["date"];
+                // Same date, sort by difficulty
+                if (a["level"] !== b["level"])
+                    return a["level"] -b["level"];
+                // Same date & difficulty, sort by provider name
+                return a["provider"].localeCompare(b["provider"]);
+            });
+            var frag = dFrag();
+            resp.forEach(function(i) {
+                frag.appendChild(makeGridListItem(i));
+            });
+            var gridList = dGet(R.id.gridList);
+            gridList.textContent = "";
+            gridList.appendChild(frag);
+        }
+    });
+});
+

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
public/crosswords.min.css


+ 16 - 16
public/crosswords.min.js

@@ -1,16 +1,16 @@
-var e={};function h(a){var b=document.createElement("li"),d=document.createElement("span");d.className="player-name";d.textContent=a.name;b.appendChild(d);b.j=document.createElement("span");b.j.className="player-score";b.appendChild(b.j);b.style.color=a.color;return b}
-function k(){var a,b=[],d;for(d in l.g){var c=e[d];c||(c=e[d]=h(l.g[d]),d===l.l.id&&c.classList.add("player-self"),a||(a=document.getElementById("scoreboardPlayers")),a.appendChild(c));c.j.textContent=l.g[d].j;b.push(d)}b.sort(function(a,b){return l.g[b].j-l.g[a].j});d=0;for(a=b.length;d<a;d++)e[b[d]].style.order=d}
-(function(){var a=setInterval(function(){if(l){var b=(null!==l.o?l.o:Date.now()-l.F)/1E3,d=Math.floor(b%60),b=(b-d)/60,c=Math.floor(b%60),b=Math.floor((b-c)/60),b=(b?b+":":"")+((10>c?"0":"")+c+":")+((10>d?"0":"")+d);document.getElementById("gridTime").textContent=b;l.o&&(clearInterval(a),a=null)}},1E3)})();var n=[],p=null;function r(a,b,d){var c=document.createElement("div");c.className="cell";if(a.s)c.classList.add("cell-disabled");else if(null!==a.f){c.classList.add("cell-definition");for(var f=document.createElement("span"),g=0,m=a.f.length;g<m;g++){var q=a.f[g],t=document.createElement("span");t.dataset.x=b;t.dataset.y=d;t.dataset.definition=g;t.className="definition";t.innerHTML=q.text.join("<br/>");f.appendChild(t)}c.appendChild(f)}else c.classList.add("cell-letter");return c}
-function u(){var a=document.createDocumentFragment();document.getElementById("gridTitle").textContent=l.G;document.getElementById("gridDifficulty").textContent=l.H;for(var b=0;b<l.u;b++){var d=document.createElement("div");d.className="crossword-line";a.appendChild(d);for(var c=0;c<l.a;c++){var f=r(l.c[c][b],c,b);f.dataset.x=c;f.dataset.y=b;d.appendChild(f);n.push({x:c,y:b,b:f,data:l.c[c][b]})}}for(b=0;b<l.u;b++)for(c=0;c<l.a;c++)f=n[b*l.a+c],f.data.f&&f.data.f.forEach(function(a){switch(a.direction){case 2:n[b*
-l.a+c+1].b.classList.add("definition-right-vt");break;case 1:n[b*l.a+c+1].b.classList.add("definition-right-hz");break;case 4:n[(b+1)*l.a+c].b.classList.add("definition-bottom-vt");break;case 3:n[(b+1)*l.a+c].b.classList.add("definition-bottom-hz")}});v();d=document.getElementById("grid");d.textContent="";d.appendChild(a)}
-function v(){n.forEach(function(a){a.data.f||a.data.s||(a.data.m?(a.b.textContent=a.data.m,a.data.i&&(a.b.classList.add("cell-letter-correct"),a.b.classList.remove("cell-letter-wrong"),a.b.style.color=a.data.i.color)):a.b.textContent="")})}function w(a){for(var b=0,d=a.length;b<d;b++)if(!n[a[b][0]+a[b][1]*l.a].data.complete)return!1;return!0}
-function x(a,b){y&&y.b.classList.remove("cell-input");y=n[a+b*l.a];y.b.classList.add("cell-input");var d=y,c=document.getElementById("input");c.style.top=d.b.offsetTop+"px";c.style.left=d.b.offsetLeft+"px";c.focus();console.log("Current index: ",p)}function z(a){for(var b=p+a,d=A.length;b<d&&0<=b;b+=a)if(!n[A[b].x+A[b].y*l.a].data.i){x(A[b].x,A[b].y);p=b;break}}
-function B(a){for(a=a.target;a&&a.dataset&&!a.dataset.x;)a=a.parentElement;if(a&&a.dataset&&a.dataset.x&&a.dataset.y){var b=n[parseInt(a.dataset.x,10)+parseInt(a.dataset.y,10)*l.a];if(b.data.s)C();else if(b.data.f){var d=!0;C();if(b.data.f[a.dataset.definition]){var c=0;b.data.f[a.dataset.definition].C.forEach(function(a){D(a[0],a[1]);d&&!n[a[0]+a[1]*l.a].data.i&&(p=c,x(a[0],a[1]));d=!1;c++})}}else{var f=E(b.x,b.y);C();for(var c=0,g=f.length;c<g;c++)for(var m=0,q=f[c].length;m<q;m++)if(!n[f[c][m][0]+
-f[c][m][1]*l.a].data.i){if(f[c][m][0]==b.x&&f[c][m][1]==b.y){f[c].forEach(function(a){D(a[0],a[1])});p=m;x(b.x,b.y);return}break}c=0;for(q=f.length;c<q;c++)if(!w(f[c])){g=0;for(q=f[0].length;g<q;g++)m=f[0][g],D(m[0],m[1]),m[0]==b.x&&m[1]==b.y&&(p=g);a.classList.add("cell-input");x(b.x,b.y);break}}}};function F(a){this.text=a.text;this.direction=a.pos;this.C=null}function G(){this.s=!1;this.m=this.i=this.f=null}G.prototype.update=function(a,b){if(null===a.type)this.s=!0;else if(void 0!==a.definitions)this.f=[],a.definitions.forEach(function(a){this.f.push(new F(a))}.bind(this));else if(a.letter)return this.m=a.letter,this.i=b[a.found],a.v;return 0};
-function H(a,b){this.G=a.title||"";this.H=a.difficulty;this.a=a.w;this.u=a.h;this.F=a.startTime||0;this.g={};this.l=null;this.A=b;this.B=[];this.c=[];for(var d=0;d<this.a;d++){this.c[d]=[];for(var c=0;c<this.u;c++)this.c[d][c]=new G}this.o=null}function I(a,b,d,c,f){if(!a.c[b+c]||!a.c[b+c][d+f]||a.c[b+c][d+f].f||a.c[b+c][d+f].s)return[[b,d]];a=I(a,b+c,d+f,c,f);a.unshift([b,d]);return a}
-function E(a,b){var d=[];l.B.forEach(function(c){for(var f=0,g=c.length;f<g;f++)if(c[f][0]==a&&c[f][1]==b){d.push(c);break}});return d}function J(a){var b=0;a.forEach(function(a){var c=this.g[a.name];c||(c=this.g[a.name]=new K(a));b=Math.max(b,c.update(a))}.bind(l))}
-H.prototype.update=function(a){var b=null,d=!1;a.forEach(function(a){a=this.c[a.x][a.y].update(a,this.g);b=Math.max(b||0,a);0===a&&(d=!0)}.bind(this));if(d){for(var c=[],f=0;f<this.a;f++)for(var g=0;g<this.u;g++)this.c[f][g].f&&this.c[f][g].f.forEach(function(a){var b;switch(a.direction){case 2:b=I(this,f+1,g,0,1);break;case 1:b=I(this,f+1,g,1,0);break;case 4:b=I(this,f,g+1,0,1);break;case 3:b=I(this,f,g+1,1,0)}c.push(b);a.C=b}.bind(this));this.B=c}return b};function K(a){this.id=a.name;this.D=encodeURIComponent(a.name);this.j=a.score;var b=a.name.indexOf("|");this.name=a.name.substr(0,b);this.color="#"+a.name.substr(b+1)}K.prototype.update=function(a){this.j=a.score;return a.v};function L(a,b){var d=new XMLHttpRequest;d.onreadystatechange=function(){if(4===d.readyState){var a=null;if(200===d.status){a=d.response;try{a=JSON.parse(a)}catch(f){a=null}}b(d.status,a)}};d.open("GET",a,!0);d.send(null)}function M(){N(function(a){a&&L("/api/poll?grid="+O+"&v="+P,function(b,d){d&&(l=new H(d,a),d.players&&(J(d.players),l.l||(l.l=l.g[l.A]),k()),d.grid&&(l.update(d.grid),u()),d.gridTime?l.o=parseInt(d.gridTime,10):Q(),P=Math.max(P,d.v||0))})})}
-function R(){!0!==R.c&&(R.a&&(clearTimeout(R.a),R.a=0),R.c=!0,L("/api/poll?grid="+O+"&v="+P,function(a,b){!0!==R.g&&(R.c=!1,b&&(b.players&&(J(b.players),l.l||(l.l=l.g[l.A]),k()),b.grid&&(l.update(b.grid),v()),b.gridTime&&(l.o=parseInt(b.gridTime,10),R.g=!0,R.a&&(clearTimeout(R.a),R.a=0),console.log("stop polling")),P=Math.max(P,b.v||0)),Q())}))}function Q(){R.g=!1;R.a||(R.a=setInterval(R,5E3))};var O,P=0,l,A=[],y=null;function N(a){var b=window.sessionStorage.getItem("pseudonyme_"+O);b?a(b):L("/api/register?grid="+O,function(b,c){b&&c?(window.sessionStorage.setItem("pseudonyme_"+O,c),a(c)):a(null)})}function S(a){var b=y;b.b.classList.add("cell-letter-pending");L("/api/put?grid="+O+"&key="+a+"&x="+b.x+"&y="+b.y+"&me="+l.l.D,function(a){b.b.classList.remove("cell-letter-pending");403!==a||b.data.i?R():b.b.classList.add("cell-letter-wrong")})}
-function C(){A.forEach(function(a){a.b.classList.remove("cell-selected")});y&&(y.b.classList.remove("cell-input"),y=null);A=[]}function D(a,b){var d=n[a+b*l.a];A.push(d);d.b.classList.add("cell-selected")}
-document.addEventListener("DOMContentLoaded",function(){O=document.location.hash.substr(1);""==O?document.location.href="/":(document.addEventListener("click",B),document.addEventListener("keydown",function(a){"BACKSPACE"===a.key.toUpperCase()&&a.preventDefault()}),document.addEventListener("keyup",function(a){y&&!y.data.i&&("DELETE"==a.key.toUpperCase()?(y.b.classList.remove("cell-letter-wrong"),y.data.m=null,v()):"BACKSPACE"==a.key.toUpperCase()?(y.b.classList.remove("cell-letter-wrong"),y.data.m=
-null,z(-1),v()):1===a.key.length&&(a=a.key.charAt(0).toUpperCase(),a.match(/[A-Z]/)&&(y.data.m=a,S(a),v(),z(1))))}),M())});
+function e(a,b){var c=new XMLHttpRequest;c.onreadystatechange=function(){if(4===c.readyState){var a=null;if(200===c.status){a=c.response;try{a=JSON.parse(a)}catch(f){a=null}}b(c.status,a)}};c.open("GET",a,!0);c.send(null)};var h={};function k(a){var b=document.createElement("li"),c=document.createElement("span");c.className="player-name";c.textContent=a.name;b.appendChild(c);b.j=document.createElement("span");b.j.className="player-score";b.appendChild(b.j);b.style.color=a.color;return b}
+function l(){var a,b=[],c;for(c in n.g){var d=h[c];d||(d=h[c]=k(n.g[c]),c===n.l.id&&d.classList.add("player-self"),a||(a=document.getElementById("scoreboardPlayers")),a.appendChild(d));d.j.textContent=n.g[c].j;b.push(c)}b.sort(function(a,b){return n.g[b].j-n.g[a].j});c=0;for(a=b.length;c<a;c++)h[b[c]].style.order=c}
+(function(){var a=setInterval(function(){if(n){var b=(null!==n.o?n.o:Date.now()-n.F)/1E3,c=Math.floor(b%60),b=(b-c)/60,d=Math.floor(b%60),b=Math.floor((b-d)/60),b=(b?b+":":"")+((10>d?"0":"")+d+":")+((10>c?"0":"")+c);document.getElementById("gridTime").textContent=b;n.o&&(clearInterval(a),a=null)}},1E3)})();var p=[],t=null;function u(a,b,c){var d=document.createElement("div");d.className="cell";if(a.s)d.classList.add("cell-disabled");else if(null!==a.f){d.classList.add("cell-definition");for(var f=document.createElement("span"),g=0,m=a.f.length;g<m;g++){var q=a.f[g],r=document.createElement("span");r.dataset.x=b;r.dataset.y=c;r.dataset.definition=g;r.className="definition";r.innerHTML=q.text.join("<br/>");f.appendChild(r)}d.appendChild(f)}else d.classList.add("cell-letter");return d}
+function v(){var a=document.createDocumentFragment();document.getElementById("gridTitle").textContent=n.G;document.getElementById("gridDifficulty").textContent=n.H;for(var b=0;b<n.u;b++){var c=document.createElement("div");c.className="crossword-line";a.appendChild(c);for(var d=0;d<n.a;d++){var f=u(n.c[d][b],d,b);f.dataset.x=d;f.dataset.y=b;c.appendChild(f);p.push({x:d,y:b,b:f,data:n.c[d][b]})}}for(b=0;b<n.u;b++)for(d=0;d<n.a;d++)f=p[b*n.a+d],f.data.f&&f.data.f.forEach(function(a){switch(a.direction){case 2:p[b*
+n.a+d+1].b.classList.add("definition-right-vt");break;case 1:p[b*n.a+d+1].b.classList.add("definition-right-hz");break;case 4:p[(b+1)*n.a+d].b.classList.add("definition-bottom-vt");break;case 3:p[(b+1)*n.a+d].b.classList.add("definition-bottom-hz")}});w();c=document.getElementById("grid");c.textContent="";c.appendChild(a)}
+function w(){p.forEach(function(a){a.data.f||a.data.s||(a.data.m?(a.b.textContent=a.data.m,a.data.i&&(a.b.classList.add("cell-letter-correct"),a.b.classList.remove("cell-letter-wrong"),a.b.style.color=a.data.i.color)):a.b.textContent="")})}function x(a){for(var b=0,c=a.length;b<c;b++)if(!p[a[b][0]+a[b][1]*n.a].data.complete)return!1;return!0}
+function y(a,b){z&&z.b.classList.remove("cell-input");z=p[a+b*n.a];z.b.classList.add("cell-input");var c=z,d=document.getElementById("input");d.style.top=c.b.offsetTop+"px";d.style.left=c.b.offsetLeft+"px";d.focus()}function A(a){for(var b=t+a,c=B.length;b<c&&0<=b;b+=a)if(!p[B[b].x+B[b].y*n.a].data.i){y(B[b].x,B[b].y);t=b;break}}
+function C(a){for(a=a.target;a&&a.dataset&&!a.dataset.x;)a=a.parentElement;if(a&&a.dataset&&a.dataset.x&&a.dataset.y){var b=p[parseInt(a.dataset.x,10)+parseInt(a.dataset.y,10)*n.a];if(b.data.s)D();else if(b.data.f){var c=!0;D();if(b.data.f[a.dataset.definition]){var d=0;b.data.f[a.dataset.definition].C.forEach(function(a){E(a[0],a[1]);c&&!p[a[0]+a[1]*n.a].data.i&&(t=d,y(a[0],a[1]));c=!1;d++})}}else{var f=F(b.x,b.y);D();for(var d=0,g=f.length;d<g;d++)for(var m=0,q=f[d].length;m<q;m++)if(!p[f[d][m][0]+
+f[d][m][1]*n.a].data.i){if(f[d][m][0]==b.x&&f[d][m][1]==b.y){f[d].forEach(function(a){E(a[0],a[1])});t=m;y(b.x,b.y);return}break}d=0;for(q=f.length;d<q;d++)if(!x(f[d])){g=0;for(q=f[0].length;g<q;g++)m=f[0][g],E(m[0],m[1]),m[0]==b.x&&m[1]==b.y&&(t=g);a.classList.add("cell-input");y(b.x,b.y);break}}}};function G(a){this.text=a.text;this.direction=a.pos;this.C=null}function H(){this.s=!1;this.m=this.i=this.f=null}H.prototype.update=function(a,b){if(null===a.type)this.s=!0;else if(void 0!==a.definitions)this.f=[],a.definitions.forEach(function(a){this.f.push(new G(a))}.bind(this));else if(a.letter)return this.m=a.letter,this.i=b[a.found],a.v;return 0};
+function I(a,b){this.G=a.title||"";this.H=a.difficulty;this.a=a.w;this.u=a.h;this.F=a.startTime||0;this.g={};this.l=null;this.A=b;this.B=[];this.c=[];for(var c=0;c<this.a;c++){this.c[c]=[];for(var d=0;d<this.u;d++)this.c[c][d]=new H}this.o=null}function J(a,b,c,d,f){if(!a.c[b+d]||!a.c[b+d][c+f]||a.c[b+d][c+f].f||a.c[b+d][c+f].s)return[[b,c]];a=J(a,b+d,c+f,d,f);a.unshift([b,c]);return a}
+function F(a,b){var c=[];n.B.forEach(function(d){for(var f=0,g=d.length;f<g;f++)if(d[f][0]==a&&d[f][1]==b){c.push(d);break}});return c}function K(a){var b=0;a.forEach(function(a){var c=this.g[a.name];c||(c=this.g[a.name]=new L(a));b=Math.max(b,c.update(a))}.bind(n))}
+I.prototype.update=function(a){var b=null,c=!1;a.forEach(function(a){a=this.c[a.x][a.y].update(a,this.g);b=Math.max(b||0,a);0===a&&(c=!0)}.bind(this));if(c){for(var d=[],f=0;f<this.a;f++)for(var g=0;g<this.u;g++)this.c[f][g].f&&this.c[f][g].f.forEach(function(a){var b;switch(a.direction){case 2:b=J(this,f+1,g,0,1);break;case 1:b=J(this,f+1,g,1,0);break;case 4:b=J(this,f,g+1,0,1);break;case 3:b=J(this,f,g+1,1,0)}d.push(b);a.C=b}.bind(this));this.B=d}return b};function L(a){this.id=a.name;this.D=encodeURIComponent(a.name);this.j=a.score;var b=a.name.indexOf("|");this.name=a.name.substr(0,b);this.color="#"+a.name.substr(b+1)}L.prototype.update=function(a){this.j=a.score;return a.v};function M(){N(function(a){a&&e("/api/poll?grid="+O+"&v="+P,function(b,c){c&&(n=new I(c,a),c.players&&(K(c.players),n.l||(n.l=n.g[n.A]),l()),c.grid&&(n.update(c.grid),v()),c.gridTime?n.o=parseInt(c.gridTime,10):Q(),P=Math.max(P,c.v||0))})})}
+function R(){!0!==R.c&&(R.a&&(clearTimeout(R.a),R.a=0),R.c=!0,e("/api/poll?grid="+O+"&v="+P,function(a,b){!0!==R.g&&(R.c=!1,b&&(b.players&&(K(b.players),n.l||(n.l=n.g[n.A]),l()),b.grid&&(n.update(b.grid),w()),b.gridTime&&(n.o=parseInt(b.gridTime,10),R.g=!0,R.a&&(clearTimeout(R.a),R.a=0)),P=Math.max(P,b.v||0)),Q())}))}function Q(){R.g=!1;R.a||(R.a=setInterval(R,5E3))};var O,P=0,n,B=[],z=null;function N(a){var b=window.sessionStorage.getItem("pseudonyme_"+O);b?a(b):e("/api/register?grid="+O,function(b,d){b&&d?(window.sessionStorage.setItem("pseudonyme_"+O,d),a(d)):a(null)})}function S(a){var b=z;b.b.classList.add("cell-letter-pending");e("/api/put?grid="+O+"&key="+a+"&x="+b.x+"&y="+b.y+"&me="+n.l.D,function(a){b.b.classList.remove("cell-letter-pending");403!==a||b.data.i?R():b.b.classList.add("cell-letter-wrong")})}
+function D(){B.forEach(function(a){a.b.classList.remove("cell-selected")});z&&(z.b.classList.remove("cell-input"),z=null);B=[]}function E(a,b){var c=p[a+b*n.a];B.push(c);c.b.classList.add("cell-selected")}
+document.addEventListener("DOMContentLoaded",function(){O=document.location.hash.substr(1);""==O?document.location.href="/":(document.addEventListener("click",C),document.addEventListener("keydown",function(a){"BACKSPACE"===a.key.toUpperCase()&&a.preventDefault()}),document.addEventListener("keyup",function(a){z&&!z.data.i&&("DELETE"==a.key.toUpperCase()?(z.b.classList.remove("cell-letter-wrong"),z.data.m=null,w()):"BACKSPACE"==a.key.toUpperCase()?(z.b.classList.remove("cell-letter-wrong"),z.data.m=
+null,A(-1),w()):1===a.key.length&&(a=a.key.charAt(0).toUpperCase(),a.match(/[A-Z]/)&&(z.data.m=a,S(a),w(),A(1))))}),M())});

+ 2 - 1
public/game.html

@@ -12,10 +12,11 @@
                 <h2 id="gridDifficulty"></h2>
                 <div id="gridTime">00:00</div>
             </header>
+            <a class="scoreboard-share"></a>
             <ul id="scoreboardPlayers" class="scoreboard-players"></ul>
         </div>
         <div id="grid" class="crossword"></div>
-        <script src="crosswords.min.js"></script>
         <input id="input" type="text" class="input-hidden" />
+        <script src="crosswords.min.js"></script>
     </body>
 </html>

BIN
public/img/crosswords.png


+ 12 - 7
public/index.html

@@ -1,10 +1,15 @@
 <!DOCTYPE html5>
 <html>
-	<head>
-		<title>Crosswords</title>
-		<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"/>
-		<link href="crosswords.min.css" rel="stylesheet"/>
-	</head>
-	<body>
-	</body>
+    <head>
+        <title>Crosswords</title>
+        <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"/>
+        <link href="crosswords.min.css" rel="stylesheet"/>
+    </head>
+    <body class="home">
+        <h1>Grilles</h1>
+        <div class="grids-wrapper">
+            <ul id="gridList" class="grids"/>
+        </div>
+        <script src="index.min.js"></script>
+    </body>
 </html>

+ 2 - 0
public/index.min.js

@@ -0,0 +1,2 @@
+function g(c){var a=new XMLHttpRequest;a.onreadystatechange=function(){if(4===a.readyState){var b=null;if(200===a.status){b=a.response;try{b=JSON.parse(b)}catch(d){b=null}}c(a.status,b)}};a.open("GET","/api/list",!0);a.send(null)};function h(c){var a=document.createElement("li"),b=document.createElement("a"),d=document.createElement("span"),e=document.createElement("span"),f=document.createElement("span");a.className="grid crossword";b.href="api/create?gridProvider="+c.providerId+"&gridId="+c.id;d.className="grid-difficulty";d.textContent=c.level;b.appendChild(d);e.className="grid-provider";e.textContent=c.provider;b.appendChild(e);f.className="grid-date";c=""+c.date;f.textContent=""+parseInt(c.substr(4,2),10)+" "+"janvier fevrier mars avril mai juin juillet aout septembre octobre novembre decembre".split(" ")[parseInt(c.substr(2,
+2),10)]+" 20"+c.substr(0,2);b.appendChild(f);a.appendChild(b);return a};document.addEventListener("DOMContentLoaded",function(){g(function(c,a){if(a){a.sort(function(a,b){return a.date!==b.date?b.date-a.date:a.level!==b.level?a.level-b.level:a.provider.localeCompare(b.provider)});var b=document.createDocumentFragment();a.forEach(function(a){b.appendChild(h(a))});var d=document.getElementById("gridList");d.textContent="";d.appendChild(b)}})});

+ 4 - 1
src/Grid.js

@@ -48,7 +48,10 @@ LetterCell.prototype.setFound = function(playerId, t) {
 function Definition(position, alignment, text) {
     this.position = position;
     this.alignment = alignment;
-    this.textArray = text;
+    this.textArray = [];
+    text.forEach(function(i) {
+        this.textArray.push(i.replace(/[\%]/g, ''));
+    }.bind(this));
 }
 
 Definition.POSITION_BOTTOM = {};

+ 55 - 74
src/GridManager.js

@@ -1,93 +1,74 @@
 
-const http = require('http')
-    ,util = require('util')
-    ,crypto = require('crypto')
-    ,Grid = require('./Grid.js').Grid;
+const 
+    crypto = require('crypto')
+    ,GridProvider = {
+        "20minutes": require('./GridProvider_20min.js')
+    };
 
-const RCILIST = 'http://rcijeux.fr/drupal_game/20minutes/menu/js/jeux_mfleches.js?date=1495635261516'
-    ,RCIGETGRID = 'http://rcijeux.fr/drupal_game/20minutes/grids/%s.mfj'
-    ,DEBUG_QUERY = true
+const DEBUG_QUERY = false
     ,CACHE_EXPIRACY = 86400000; // 1 day
 
 const GRID_CACHE = {
-    data: {},
-    expireAt: 0
-}
-    ,GRID_DATA = {}; // TODO expiracy
+        data: {},
+        expireAt: 0
+    }
+    ,GRID_DATA = {};
 
-function extractVar(data) {
-    var first = data.indexOf('{')
-        ,last = data.indexOf('}');
+setInterval(function() {
+    const now = Date.now()
+        ,nowExpire = now -CACHE_EXPIRACY;
 
-    if (first == -1 || last == -1 || last < first)
-        return null;
-    try {
-        var dataObj;
-        eval("dataObj="+data.substr(first, last));
-        return dataObj;
-    } catch (e) {
-        return null;
+    for (let i in GRID_DATA) {
+        if (GRID_DATA[i].minVersion < nowExpire) {
+            console.log("[GridManager] cleaning " +i);
+            delete GRID_DATA[i];
+        }
     }
-    return null; // unreachable
-}
-
-function getVar(url, cb) {
-    http.get(url, (res) => {
-        let chunks = null;
-        res.on('data', (chunk) => {
-            if (chunks)
-                chunks = Buffer.concat([chunks, chunk], chunks.length +chunk.length);
-            else
-                chunks = Buffer.from(chunk);
-        });
-        res.once('end', () => {
-            cb(extractVar(chunks.toString("utf8")));
-        });
-    });
-}
+    if (GRID_CACHE.expireAt < now) {
+        GRID_CACHE.data = {};
+        GRID_CACHE.expireAt = 0;
+    }
+}, 60 * 1000); // Every minutes clean expired grids
 
-// TODO multi-sources grids
 module.exports.listGrids = function(cb) {
-    if (DEBUG_QUERY) {
-        console.warn("[gridManager] debug skip list grids");
-        cb({"240517":{ level: '1' }});
-    } else {
-        const now = Date.now();
+    const now = Date.now();
 
-        if (GRID_CACHE.expireAt < now) {
-            console.log("[gridManager] list grids");
-            getVar(RCILIST, (grids) => {
-                if (grids) {
-                    let gridObjects = {};
-                    for (let gridId in grids) {
-                        gridObjects[gridId] = {
-                            "level": grids[gridId][1]
-                        };
-                    }
-                    GRID_CACHE.data = gridObjects;
-                    GRID_CACHE.expireAt = now +CACHE_EXPIRACY;
-                    cb(gridObjects);
-                } else {
-                    cb(null);
-                }
-            });
-        } else {
-            cb(GRID_CACHE.data);
-        }
+    if (GRID_CACHE.expireAt < now) {
+        console.log("[gridManager] list grids");
+        const providers = Object.keys(GridProvider);
+        var allGrids = [];
+        var fetchProvider = function(i) {
+            if (i < providers.length) {
+                var providerId = providers[i];
+                GridProvider[providerId].listGrids((grids) => {
+                    grids.forEach((grid) => {
+                        grid.providerId = providerId;
+                        allGrids.push(grid);
+                    });
+                    fetchProvider(++i);
+                });
+            } else {
+                GRID_CACHE.data = allGrids;
+                GRID_CACHE.expireAt = now +CACHE_EXPIRACY;
+                cb(allGrids);
+            }
+        };
+        fetchProvider(0);
+    } else {
+        cb(GRID_CACHE.data);
     }
 };
 
-// TODO multi-sources grids
-module.exports.createGrid = function(gridPublicId, gridId, cb) {
+module.exports.createGrid = function(gridPublicId, gridProvider, gridId, cb) {
     console.log("[gridManager] get grid " +gridId);
-    getVar(util.format(RCIGETGRID, gridId), (data) => {
-        if (data) {
-            var grid = GRID_DATA[gridPublicId] = new Grid(gridPublicId, data);
+    if (GridProvider[gridProvider]) {
+        GridProvider[gridProvider].createGrid(gridPublicId, gridId, (grid) => {
+            GRID_DATA[gridPublicId] = grid;
             cb(grid);
-        } else {
-            cb(null);
-        }
-    });
+        });
+    } else {
+        cb(null);
+    }
 };
 
 module.exports.get = function(gridPublicId) {

+ 79 - 0
src/GridProvider_20min.js

@@ -0,0 +1,79 @@
+
+const http = require('http')
+    ,util = require('util')
+    ,Grid = require('./Grid.js').Grid
+    ;
+
+const RCILIST = 'http://rcijeux.fr/drupal_game/20minutes/menu/js/jeux_mfleches.js?date=1495635261516'
+    ,RCIGETGRID = 'http://rcijeux.fr/drupal_game/20minutes/grids/%s.mfj'
+
+function extractVar(data) {
+    var first = data.indexOf('{')
+        ,last = data.indexOf('}');
+
+    if (first == -1 || last == -1 || last < first)
+        return null;
+    try {
+        var dataObj;
+        eval("dataObj="+data.substr(first, last));
+        return dataObj;
+    } catch (e) {
+        return null;
+    }
+    return null; // unreachable
+}
+
+function getVar(url, cb) {
+    http.get(url, (res) => {
+        let chunks = null;
+        res.on('data', (chunk) => {
+            if (chunks)
+                chunks = Buffer.concat([chunks, chunk], chunks.length +chunk.length);
+            else
+                chunks = Buffer.from(chunk);
+        });
+        res.once('end', () => {
+            cb(extractVar(chunks.toString("utf8")));
+        });
+    });
+}
+
+module.exports.listGrids = function(cb) {
+    const todayymmdd = (function() {
+        const now = new Date()
+            ,month = now.getMonth() +1
+            ,day = now.getDate();
+        return parseInt('' +(now.getFullYear() - 2000) +(month < 10 ? '0' : '') +month +(day < 10 ? '0' : '') +day);
+    })();
+    getVar(RCILIST, (grids) => {
+        if (grids) {
+            let gridObjects = [];
+            for (let gridId in grids) {
+                let gridDate = parseInt(gridId.substr(4, 2) +gridId.substr(2, 2) +gridId.substr(0, 2), 10);
+                if (gridDate <= todayymmdd) {
+                    gridObjects.push({
+                        "id": gridId
+                        ,"date": gridDate
+                        ,"provider": "20 minutes"
+                        ,"level": parseInt(grids[gridId][1], 10)
+                    });
+                }
+            }
+            cb(gridObjects);
+        } else {
+            cb(null);
+        }
+    });
+};
+
+module.exports.createGrid = function(gridPublicId, gridId, cb) {
+    console.log("[gridManager] get grid " +gridId);
+    getVar(util.format(RCIGETGRID, gridId), (data) => {
+        if (data) {
+            cb(new Grid(gridPublicId, data));
+        } else {
+            cb(null);
+        }
+    });
+};
+

+ 29 - 11
src/httpServer.js

@@ -8,6 +8,12 @@ const http = require('http')
 const SCORE_ON_FAIL = -1
     ,SCORE_ON_SUCCESS = 3;
 
+var requestingIp = {};
+
+setInterval(function() {
+    requestingIp = {};
+}, 60 * 1000); // Every minutes clean expired grids
+
 function HttpServer(config) {
     var ctx = this;
 
@@ -49,7 +55,9 @@ HttpServer.prototype.logRequest = function(req, res) {
             +" "
             +req.socket.remoteAddress
             +" "
-            +res.statusCode);
+            +res.statusCode
+            +" "
+            +req.url);
 };
 
 HttpServer.prototype.isRequestPublic = function(url) {
@@ -118,16 +126,26 @@ HttpServer.prototype.serveApi = function(req, url, res) {
         urlToken = HttpServer.parseUrlParams(url.substr(pos +1));
         url = url.substr(0, pos);
     }
-    if (url === "create") {
-        // TODO limit grid creation 1 minute by ip
-        var gridId = urlToken["gridId"] ? urlToken["gridId"][0] : null;
-        if (!gridId) {
-            var dd = req.reqT.getDate()
-                ,mm = req.reqT.getMonth() +1
-                ,yy = req.reqT.getFullYear() -2000;
-            gridId = (dd < 10 ? '0' +dd :dd) +(mm < 10 ? '0' +mm : mm) +yy;
-        }
-        GridManager.createGrid(GridManager.hash('' +req.socket.remoteAddress +req.reqT.getTime() +gridId), gridId, (grid)=>{
+    if (url === "list") {
+        GridManager.listGrids((grids) => {
+            res.end(JSON.stringify(grids));
+        });
+    } else if (url === "create") {
+        const ipAddr = req.socket.remoteAddress;
+        if (requestingIp[ipAddr]) {
+            res.writeHeader("403");
+            res.end('"Please wait a little bit before requesting a new grid"');
+            return;
+        }
+        requestingIp[ipAddr] = true;
+        var gridId = urlToken["gridId"] ? urlToken["gridId"][0] : null
+            ,gridProvider = urlToken["gridProvider"] ? urlToken["gridProvider"][0] : null;
+        if (!gridId || !gridProvider) {
+            res.writeHeader("400", "Bad request");
+            res.end();
+            return;
+        }
+        GridManager.createGrid(GridManager.hash('' +req.socket.remoteAddress +req.reqT.getTime() +gridId), gridProvider, gridId, (grid)=>{
             if (grid) {
                 grid.minVersion = req.reqT.getTime();
                 res.setHeader("Location", "/game.html#" +grid.publicId);

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff