Jelajahi Sumber

[add]css
[add] input

isundil 9 tahun lalu
induk
melakukan
b0f98c6a8d
9 mengubah file dengan 335 tambahan dan 17 penghapusan
  1. 76 2
      css/main.less
  2. 65 2
      js/grid.js
  3. 11 0
      js/resources.js
  4. 72 1
      js/ui.js
  5. 64 7
      js/workflow.js
  6. 1 1
      public/crosswords.min.css
  7. 7 3
      public/crosswords.min.js
  8. 10 0
      src/Grid.js
  9. 29 1
      src/httpServer.js

+ 76 - 2
css/main.less

@@ -1,7 +1,8 @@
 
-@cell-size: 60px;
+@cell-size: 75px;
 @border-color: darkblue;
-@disabled-color: #030405;
+@arrow-color: darkblue;
+@disabled-color: #263340;
 
 .crossword-line {
     display: block;
@@ -13,6 +14,7 @@
         height: 100%;
         border-left: 1px solid @border-color;
         border-top: 1px solid @border-color;
+        overflow: hidden;
 
         &:last-child {
             border-right: 1px solid @border-color;
@@ -22,6 +24,78 @@
             background: @disabled-color;
             border-color: @disabled-color;
         }
+
+        &.cell-definition > * {
+            display: flex;
+            justify-content: center;
+            flex-direction: column;
+            height: 100%;
+
+            .definition {
+                display: block;
+                font-size: .75em;
+                text-align: center;
+
+                & > * {
+                    display: inline-block;
+                    width: @cell-size;
+                }
+
+                &:not(:only-child):not(:first-child) {
+                    border-top: 1px solid @border-color;
+                }
+
+            }
+        }
+
+        &.definition-right-vt, &.definition-right-hz, &.definition-bottom-vt, &.definition-bottom-hz {
+            position: relative;
+            &:after {
+                position: absolute;
+                height: @cell-size /6;
+                width: @cell-size /6;
+                content: " ";
+            }
+        }
+
+        &.definition-right-vt:after {
+            top: 0;
+            bottom: 0;
+            margin: auto;
+            width: @cell-size /8;
+            border-top: 1px solid @arrow-color;
+            border-right: 1px solid @arrow-color;
+        }
+        &.definition-right-hz:after {
+            top: 0;
+            bottom: 0;
+            width: @cell-size /8;
+            margin: auto;
+            border-top: 1px solid @arrow-color;
+        }
+        &.definition-bottom-vt:after {
+            left: 0;
+            right: 0;
+            margin: auto;
+            height: @cell-size /8;
+            border-left: 1px solid @arrow-color;
+        }
+        &.definition-bottom-hz:after {
+            left: 0;
+            right: 0;
+            margin: auto;
+            height: @cell-size /8;
+            border-left: 1px solid @arrow-color;
+            border-bottom: 1px solid @arrow-color;
+        }
+
+        &.cell-selected {
+            background: #e1e8ff;
+        }
+
+        &.cell-input {
+            background: #b0c2ff;
+        }
     }
 
     &:last-child .cell {

+ 65 - 2
js/grid.js

@@ -7,6 +7,8 @@ function Definition(data) {
     this.text = data["text"];
     /** @type {number} */
     this.direction = data["pos"];
+    /** @type {Array.<Array.<number>>|null} */
+    this.word = null;
 }
 
 /** @const */
@@ -56,6 +58,7 @@ function Grid(data) {
     /** @type {number} */
     this.height = data["h"];
 
+    this.words = [];
     this.grid = [];
     for (var i =0; i < this.width; i++) {
         this.grid[i] = [];
@@ -65,16 +68,76 @@ function Grid(data) {
     }
 }
 
+Grid.prototype.computeWord = function(x, y, dx, dy) {
+    if (!this.grid[x +dx] || !this.grid[x +dx][y +dy] || this.grid[x +dx][y +dy].definitions || this.grid[x +dx][y +dy].isBlack) {
+        return [[x, y]];
+    }
+    var word = this.computeWord(x +dx, y +dy, dx, dy);
+    word.unshift([x, y]);
+    return word;
+};
+
+Grid.prototype.getWord = function(x, y) {
+    var words = [];
+    this.words.forEach(function(word) {
+        for (var i =0, nbLetters =word.length; i < nbLetters; i++) {
+            if (word[i][0] == x && word[i][1] == y) {
+                words.push(word);
+                break;
+            }
+        }
+    });
+    return words;
+};
+
 /**
  * @return {number|null}
 **/
 Grid.prototype.update = function(data) {
     var maxVersion = null
-        ,grid = this.grid;
+        ,grid = this.grid
+        ,topologyUpdated = false;
 
     data.forEach(function(cellData) {
-        maxVersion = Math.max(maxVersion || 0, grid[cellData["x"]][cellData["y"]].update(cellData));
+        var updateResult = grid[cellData["x"]][cellData["y"]].update(cellData);
+        maxVersion = Math.max(maxVersion || 0, updateResult);
+        if (updateResult === 0) {
+            topologyUpdated = true;
+        }
     });
+    if (topologyUpdated) {
+        var words = [];
+        for (var i =0; i < this.width; i++) {
+            for (var j =0; j < this.height; j++) {
+                if (grid[i][j].definitions) {
+                    grid[i][j].definitions.forEach(function(definition) {
+                        var word;
+                        switch (definition.direction) {
+                            case Definition.RIGHT_VERTICAL:
+                                word = this.computeWord(i +1, j, 0, 1);
+                                break;
+
+                            case Definition.RIGHT_HORIZONTAL:
+                                word = this.computeWord(i +1, j, 1, 0);
+                                break;
+
+                            case Definition.BOTTOM_VERTICAL:
+                                word = this.computeWord(i, j +1, 0, 1);
+                                break;
+
+                            case Definition.BOTTOM_HORIZONTAL:
+                                word = this.computeWord(i, j +1, 1, 0);
+                                break;
+
+                        }
+                        words.push(word);
+                        definition.word = word;
+                    }.bind(this));
+                }
+            }
+        }
+        this.words = words;
+    }
     return maxVersion;
 };
 

+ 11 - 0
js/resources.js

@@ -7,6 +7,17 @@ var R = {
             ,black: "cell-disabled"
             ,definition: "cell-definition"
             ,letter: "cell-letter"
+
+            ,definitions: {
+                item: "definition"
+                ,rightVertical: "definition-right-vt"
+                ,rightHorizontal: "definition-right-hz"
+                ,bottomVertical: "definition-bottom-vt"
+                ,bottomHorizontal: "definition-bottom-hz"
+            }
+
+            ,selected: "cell-selected"
+            ,currentInput: "cell-input"
         }
     }
 };

+ 72 - 1
js/ui.js

@@ -1,6 +1,7 @@
 
 var UI_CELLS = [];
 
+
 function dCreate(domName) {
     return document.createElement(domName);
 }
@@ -13,9 +14,16 @@ function uiCreateCell(cellData) {
         cell.classList.add(R.klass.cell.black);
     } else if (cellData.definitions !== null) {
         cell.classList.add(R.klass.cell.definition);
+        var cellContent =dCreate("span");
+        cellData.definitions.forEach(function(definition) {
+            var domDefinition = dCreate("span");
+            domDefinition.className = R.klass.cell.definitions.item;
+            domDefinition.innerHTML = definition.text.join("<br/>");
+            cellContent.appendChild(domDefinition);
+        });
+        cell.appendChild(cellContent);
     } else {
         cell.classList.add(R.klass.cell.letter);
-        console.log(cellData);
     }
     return cell;
 }
@@ -30,6 +38,8 @@ function uiCreateGrid() {
         frag.appendChild(line);
         for (var j =0; j < GRID.width; j++) {
             var cell = uiCreateCell(GRID.grid[j][i]);
+            cell.dataset.x = j;
+            cell.dataset.y = i;
             line.appendChild(cell);
             UI_CELLS.push({
                 x: j
@@ -39,6 +49,32 @@ function uiCreateGrid() {
             });
         }
     }
+    for (var i =0; i < GRID.height; i++) {
+        for (var j =0; j < GRID.width; j++) {
+            var cell = UI_CELLS[i *GRID.width +j];
+            if (cell.data.definitions) {
+                cell.data.definitions.forEach(function(d) {
+                    switch (d.direction) {
+                        case Definition.RIGHT_VERTICAL:
+                            UI_CELLS[i *GRID.width +j +1].dom.classList.add(R.klass.cell.definitions.rightVertical);
+                        break;
+
+                        case Definition.RIGHT_HORIZONTAL:
+                            UI_CELLS[i *GRID.width +j +1].dom.classList.add(R.klass.cell.definitions.rightHorizontal);
+                        break;
+
+                        case Definition.BOTTOM_VERTICAL:
+                            UI_CELLS[(i +1) *GRID.width +j].dom.classList.add(R.klass.cell.definitions.bottomVertical);
+                        break;
+
+                        case Definition.BOTTOM_HORIZONTAL:
+                            UI_CELLS[(i +1) *GRID.width +j].dom.classList.add(R.klass.cell.definitions.bottomHorizontal);
+                        break;
+                    }
+                });
+            }
+        }
+    }
     document.body.textContent = "";
     document.body.appendChild(frag);
 }
@@ -51,3 +87,38 @@ function onGridUpdated() {
     });
 }
 
+function gridClickDelegate(e) {
+    var target = e.target;
+    while (target && !target.classList.contains(R.klass.cell.item))
+        target = target.parentElement;
+    if (target && target.dataset && target.dataset.x && target.dataset.y) {
+        var clickedCell = UI_CELLS[parseInt(target.dataset.x, 10) +parseInt(target.dataset.y, 10) *GRID.width];
+        if (clickedCell.data.isBlack) {
+            unselect();
+        } else if (clickedCell.data.definitions) {
+            var first = true;
+            unselect();
+            if (clickedCell.data.definitions.length) {
+                clickedCell.data.definitions[0].word.forEach(function(coordinates) {
+                    select(coordinates[0], coordinates[1]);
+                    if (first) {
+                        CURRENTINPUT = UI_CELLS[coordinates[0] +coordinates[1] *GRID.width];
+                        CURRENTINPUT.dom.classList.add(R.klass.cell.currentInput);
+                        first = false;
+                    }
+                });
+            }
+        } else {
+            var words = GRID.getWord(clickedCell.x, clickedCell.y);
+            unselect();
+            words.forEach(function (word) {
+                word.forEach(function (coordinates) {
+                    select(coordinates[0], coordinates[1]);
+                });
+            });
+            target.classList.add(R.klass.cell.currentInput);
+            CURRENTINPUT = clickedCell;
+        }
+    }
+}
+

+ 64 - 7
js/workflow.js

@@ -1,7 +1,10 @@
 
 var GRID_PUBLIC_ID
     ,KNOWN_VERSION = 0
-    ,GRID;
+    ,GRID
+
+    ,SELECTED = []
+    ,CURRENTINPUT = null;
 
 function doGet(url, callback) {
     var xhr = new XMLHttpRequest();
@@ -17,7 +20,7 @@ function doGet(url, callback) {
                     resp = null;
                 }
             }
-            callback(resp);
+            callback(xhr.status, resp);
         }
     };
     xhr.open('GET', url, true);
@@ -25,15 +28,59 @@ function doGet(url, callback) {
 }
 
 function initPolling() {
-    doGet("/api/poll?grid=" +GRID_PUBLIC_ID +"&v=" +KNOWN_VERSION, function(resp) {
-        GRID = new Grid(resp);
-        if (resp["grid"]) {
-            KNOWN_VERSION = Math.max(GRID.update(resp["grid"]) || 0, KNOWN_VERSION);
-            uiCreateGrid();
+    doGet("/api/poll?grid=" +GRID_PUBLIC_ID +"&v=" +KNOWN_VERSION, function(status, resp) {
+        if (resp) {
+            GRID = new Grid(resp);
+            if (resp["grid"]) {
+                KNOWN_VERSION = Math.max(GRID.update(resp["grid"]) || 0, KNOWN_VERSION);
+                uiCreateGrid();
+            }
+        } // TODO else cannot init party
+    });
+}
+
+function pollNow() {
+    // TODO avoid duplicate call / abort planned poll
+    doGet("/api/poll?grid=" +GRID_PUBLIC_ID +"&v=" +KNOWN_VERSION, function(status, resp) {
+        if (resp && resp["grid"]) {
+            var newVersion = Math.max(GRID.update(resp["grid"]) || 0, KNOWN_VERSION);
+            if (newVersion !== KNOWN_VERSION) {
+                onGridUpdated();
+                KNOWN_VERSION = newVersion;
+            }
+        }
+        // TODO plan next poll
+    });
+}
+
+function keyPressHandler(cell, key) {
+    doGet("/api/put?grid=" +GRID_PUBLIC_ID +"&key=" +key +"&x=" +cell.x +"&y=" +cell.y, function(status, resp) {
+        if (status === 403) {
+            // TODO wrong
+        } else if (status === 204) {
+            // TODO good
+            pollNow();
         }
     });
 }
 
+function unselect() {
+    SELECTED.forEach(function (cell) {
+        cell.dom.classList.remove(R.klass.cell.selected);
+    });
+    if (CURRENTINPUT) {
+        CURRENTINPUT.dom.classList.remove(R.klass.cell.currentInput);
+        CURRENTINPUT = null;
+    }
+    SELECTED = [];
+}
+
+function select(x, y) {
+    var cell = UI_CELLS[x +y *GRID.width];
+    SELECTED.push(cell);
+    cell.dom.classList.add(R.klass.cell.selected);
+}
+
 document.addEventListener('DOMContentLoaded', function() {
     GRID_PUBLIC_ID = (document.location.hash.substr(1));
 
@@ -41,6 +88,16 @@ document.addEventListener('DOMContentLoaded', function() {
         document.location.href = "/";
         return;
     }
+
+    document.addEventListener('click', gridClickDelegate);
+    document.addEventListener('keypress', function(e) {
+        if (CURRENTINPUT) {
+            var key = e.key.charAt(0).toUpperCase();
+            if (key.match(/[A-Z]/)) {
+                keyPressHandler(CURRENTINPUT, key);
+            }
+        }
+    });
     initPolling();
 });
 

+ 1 - 1
public/crosswords.min.css

@@ -1 +1 @@
-.crossword-line{display:block;height:60px}.crossword-line .cell{display:inline-block;width:60px;height:100%;border-left:1px solid #00008b;border-top:1px solid #00008b}.crossword-line .cell:last-child{border-right:1px solid #00008b}.crossword-line .cell.cell-disabled{background:#030405;border-color:#030405}.crossword-line:last-child .cell{border-bottom:1px solid #00008b}
+.crossword-line{display:block;height:75px}.crossword-line .cell{display:inline-block;width:75px;height:100%;border-left:1px solid #00008b;border-top:1px solid #00008b;overflow:hidden}.crossword-line .cell:last-child{border-right:1px solid #00008b}.crossword-line .cell.cell-disabled{background:#263340;border-color:#263340}.crossword-line .cell.cell-definition>*{display:flex;justify-content:center;flex-direction:column;height:100%}.crossword-line .cell.cell-definition>* .definition{display:block;font-size:.75em;text-align:center}.crossword-line .cell.cell-definition>* .definition>*{display:inline-block;width:75px}.crossword-line .cell.cell-definition>* .definition:not(:only-child):not(:first-child){border-top:1px solid #00008b}.crossword-line .cell.definition-bottom-hz,.crossword-line .cell.definition-bottom-vt,.crossword-line .cell.definition-right-hz,.crossword-line .cell.definition-right-vt{position:relative}.crossword-line .cell.definition-bottom-hz:after,.crossword-line .cell.definition-bottom-vt:after,.crossword-line .cell.definition-right-hz:after,.crossword-line .cell.definition-right-vt:after{position:absolute;height:12.5px;width:12.5px;content:" "}.crossword-line .cell.definition-right-vt:after{top:0;bottom:0;margin:auto;width:9.38px;border-top:1px solid #00008b;border-right:1px solid #00008b}.crossword-line .cell.definition-right-hz:after{top:0;bottom:0;width:9.38px;margin:auto;border-top:1px solid #00008b}.crossword-line .cell.definition-bottom-vt:after{left:0;right:0;margin:auto;height:9.38px;border-left:1px solid #00008b}.crossword-line .cell.definition-bottom-hz:after{left:0;right:0;margin:auto;height:9.38px;border-left:1px solid #00008b;border-bottom:1px solid #00008b}.crossword-line .cell.cell-selected{background:#e1e8ff}.crossword-line .cell.cell-input{background:#b0c2ff}.crossword-line:last-child .cell{border-bottom:1px solid #00008b}

+ 7 - 3
public/crosswords.min.js

@@ -1,3 +1,7 @@
-var e=[];function h(){}function k(){this.g=!1;this.b=null}k.prototype.update=function(a){null===a.type?this.g=!0:void 0!==a.definitions&&(this.b=[],a.definitions.forEach(function(){this.b.push(new h)}.bind(this)));return 0};function l(a){this.f=a.w;this.c=a.h;this.a=[];for(a=0;a<this.f;a++){this.a[a]=[];for(var b=0;b<this.c;b++)this.a[a][b]=new k}}l.prototype.update=function(a){var b=null,c=this.a;a.forEach(function(a){b=Math.max(b||0,c[a.x][a.y].update(a))});return b};var m,n=0,p;function q(a){var b="/api/poll?grid="+m+"&v="+n,c=new XMLHttpRequest;c.onreadystatechange=function(){if(4===c.readyState){var b=null;if(200===c.status){b=c.response;try{b=JSON.parse(b)}catch(d){b=null}}a(b)}};c.open("GET",b,!0);c.send(null)}
-function r(){q(function(a){p=new l(a);if(a.grid){n=Math.max(p.update(a.grid)||0,n);a=document.createDocumentFragment();for(var b=0;b<p.c;b++){var c=document.createElement("div");c.className="crossword-line";a.appendChild(c);for(var f=0;f<p.f;f++){var d;d=p.a[f][b];var g=document.createElement("div");g.className="cell";d.g?g.classList.add("cell-disabled"):null!==d.b?g.classList.add("cell-definition"):(g.classList.add("cell-letter"),console.log(d));d=g;c.appendChild(d);e.push({x:f,y:b,i:d,data:p.a[f][b]})}}document.body.textContent=
-"";document.body.appendChild(a)}})}document.addEventListener("DOMContentLoaded",function(){m=document.location.hash.substr(1);""==m?document.location.href="/":r()});
+var f=[];function h(a){var b=document.createElement("div");b.className="cell";if(a.i)b.classList.add("cell-disabled");else if(null!==a.b){b.classList.add("cell-definition");var c=document.createElement("span");a.b.forEach(function(a){var b=document.createElement("span");b.className="definition";b.innerHTML=a.text.join("<br/>");c.appendChild(b)});b.appendChild(c)}else b.classList.add("cell-letter");return b}
+function k(){for(var a=document.createDocumentFragment(),b=0;b<m.g;b++){var c=document.createElement("div");c.className="crossword-line";a.appendChild(c);for(var d=0;d<m.a;d++){var e=h(m.c[d][b]);e.dataset.x=d;e.dataset.y=b;c.appendChild(e);f.push({x:d,y:b,f:e,data:m.c[d][b]})}}for(b=0;b<m.g;b++)for(d=0;d<m.a;d++)e=f[b*m.a+d],e.data.b&&e.data.b.forEach(function(a){switch(a.direction){case 2:f[b*m.a+d+1].f.classList.add("definition-right-vt");break;case 1:f[b*m.a+d+1].f.classList.add("definition-right-hz");
+break;case 4:f[(b+1)*m.a+d].f.classList.add("definition-bottom-vt");break;case 3:f[(b+1)*m.a+d].f.classList.add("definition-bottom-hz")}});document.body.textContent="";document.body.appendChild(a)}
+function n(a){for(a=a.target;a&&!a.classList.contains("cell");)a=a.parentElement;if(a&&a.dataset&&a.dataset.x&&a.dataset.y){var b=f[parseInt(a.dataset.x,10)+parseInt(a.dataset.y,10)*m.a];if(b.data.i)p();else if(b.data.b){var c=!0;p();b.data.b.length&&b.data.b[0].l.forEach(function(a){q(a[0],a[1]);c&&(r=f[a[0]+a[1]*m.a],r.f.classList.add("cell-input"),c=!1)})}else{var d=t(b.x,b.y);p();d.forEach(function(a){a.forEach(function(a){q(a[0],a[1])})});a.classList.add("cell-input");r=b}}};function u(a){this.text=a.text;this.direction=a.pos;this.l=null}function v(){this.i=!1;this.b=null}v.prototype.update=function(a){null===a.type?this.i=!0:void 0!==a.definitions&&(this.b=[],a.definitions.forEach(function(a){this.b.push(new u(a))}.bind(this)));return 0};function w(a){this.a=a.w;this.g=a.h;this.j=[];this.c=[];for(a=0;a<this.a;a++){this.c[a]=[];for(var b=0;b<this.g;b++)this.c[a][b]=new v}}
+function x(a,b,c,d,e){if(!a.c[b+d]||!a.c[b+d][c+e]||a.c[b+d][c+e].b||a.c[b+d][c+e].i)return[[b,c]];a=x(a,b+d,c+e,d,e);a.unshift([b,c]);return a}function t(a,b){var c=[];m.j.forEach(function(d){for(var e=0,g=d.length;e<g;e++)if(d[e][0]==a&&d[e][1]==b){c.push(d);break}});return c}
+w.prototype.update=function(a){var b=null,c=this.c,d=!1;a.forEach(function(a){a=c[a.x][a.y].update(a);b=Math.max(b||0,a);0===a&&(d=!0)});if(d){for(var e=[],g=0;g<this.a;g++)for(var l=0;l<this.g;l++)c[g][l].b&&c[g][l].b.forEach(function(a){var b;switch(a.direction){case 2:b=x(this,g+1,l,0,1);break;case 1:b=x(this,g+1,l,1,0);break;case 4:b=x(this,g,l+1,0,1);break;case 3:b=x(this,g,l+1,1,0)}e.push(b);a.l=b}.bind(this));this.j=e}return b};var y,z=0,m,A=[],r=null;function B(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(e){a=null}}b(a)}};c.open("GET",a,!0);c.send(null)}function C(){B("/api/poll?grid="+y+"&v="+z,function(a){a&&(m=new w(a),a.grid&&(z=Math.max(m.update(a.grid)||0,z),k()))})}function D(a){var b=r;B("/api/put?grid="+y+"&key="+a+"&x="+b.x+"&y="+b.y,function(a){console.log(a)})}
+function p(){A.forEach(function(a){a.f.classList.remove("cell-selected")});r&&(r.f.classList.remove("cell-input"),r=null);A=[]}function q(a,b){var c=f[a+b*m.a];A.push(c);c.f.classList.add("cell-selected")}document.addEventListener("DOMContentLoaded",function(){y=document.location.hash.substr(1);""==y?document.location.href="/":(document.addEventListener("click",n),document.addEventListener("keypress",function(a){r&&(a=a.key.charAt(0).toUpperCase(),a.match(/[A-Z]/)&&D(a))}),C())});

+ 10 - 0
src/Grid.js

@@ -168,6 +168,15 @@ function Grid(publicId, data) {
     this.publicId = publicId;
 };
 
+Grid.prototype.getCell = function(x, y) {
+    for (var i =0, nbCells = this.grid.length; i < nbCells; i++) {
+        var cell = this.grid[i];
+        if (cell.px === x && cell.py === y)
+            return cell;
+    }
+    return null;
+};
+
 Grid.prototype.toStatic = function(v) {
     var ret = {};
     if (!v) {
@@ -190,4 +199,5 @@ Grid.prototype.toStatic = function(v) {
 };
 
 module.exports.Grid = Grid;
+module.exports.LetterCell = LetterCell;
 

+ 29 - 1
src/httpServer.js

@@ -2,7 +2,8 @@
 const http = require('http')
     ,fs = require('fs')
 
-    ,GridManager = require('./GridManager.js');
+    ,GridManager = require('./GridManager.js')
+    ,LetterCell = require('./Grid.js').LetterCell;
 
 function HttpServer(config) {
     var ctx = this;
@@ -152,6 +153,33 @@ HttpServer.prototype.serveApi = function(req, url, res) {
             res.end();
         }
 
+    } else if (url === "put") {
+        if (!urlToken["x"] || !urlToken["y"] || !urlToken["key"] || !urlToken["grid"]) {
+            res.writeHeader("400", "Bad request");
+            res.end();
+            return;
+        }
+        var x = parseFloat(urlToken["x"][0])
+            ,y = parseFloat(urlToken["y"][0])
+            ,grid = GridManager.get(urlToken["grid"][0]);
+        if (!grid) {
+            throw new HttpServer.Error404();
+        }
+        var cell = grid.getCell(x, y);
+        if (!cell || !cell instanceof LetterCell || cell.found) {
+            res.writeHeader("400", "Bad request");
+            res.end();
+            return;
+        }
+        if (cell.letter !== urlToken["key"][0]) {
+            res.writeHeader("403");
+            res.end();
+            return;
+        }
+        cell.found = true;
+        cell.version = req.reqT.getTime();
+        res.writeHeader("204");
+        res.end();
     } else {
         throw new HttpServer.Error404(url);
     }