Saturday, April 14, 2012

Zoggle

Welcome to my guide to Zoggle. Zoggle is not affiliated with Boggle.
Zoggle is available...
On my website Zoggle. (Works on iOS - add to home screen)
On Google+.

How to play:
The objective of the game is to score as many points as possible. Points are scored based on the length of the words.
Length : Points
3,4      = 1
5         = 2
6         = 3
7         = 5
8+       = 11

Words are made by connecting letters Horizontally, Vertically, or Diagonally (illustrated below).


Zoggle is a webapp that takes this game to a whole new level by having all players play at the the same time in the same game in real-time. This creates a whole host of tough problems to solve.

Problem #1, Real-Time Gaming Data In The Browser:
This wasn't actually a problem I had because I already had a solution in mind. The key to solving this problem is to use Node.js (with the Express.js web framework) and Websockets (socket.io), hosted on Amazon EC2.
Problem #2, The Board Solving Algorithm:
The computer generates a random board of letters in a 2-dimensional grid. The next step is to figure out all of the words possible for that given grid. For reference I looked at this page. My solution ended up being slower than the python implementation (200ms vs 80ms), but it was still fast enough to be production worthy.
The basic steps to my algorithm:
1. Reduce the size of the dictionary to only contain words possibly made by the grid. This basically consisted of removing all words that contained a letter not on the board. This took my dictionary from 100k, to ~2k.
2. Flood fill the board grid starting at the the first letter of the dictionary words to see if they can be made. Flood fill works by recursively "filling" adjacent grid cells. All this means is that I make sure that for example the 2nd filled grid cell contains the 2nd letter of the dictionary word, otherwise return.
Flood Fill Graphic:


Here is the code for checking to see if a word can fit on the grid:
function fitWord(x, y, cboard, tarWord, cword) {
  if (x >= boardWidth || x < 0 || y >= boardHeight || y < 0)// out of bounds
    return;
  if (cboard[x][y] == "")// visited space
    return;
  if (tarWord.indexOf(cword) == -1)
    return;
  var board = copyTwoDimentionalArray(cboard);
  var let = board[x][y];
  cword += let;
  if (cword == tarWord) {
    wordFit = true;
    return;
  }
  board[x][y] = "";
  fitWord(x + 1, y + 1, board, tarWord, cword);
  fitWord(x + 1, y - 1, board, tarWord, cword);
  fitWord(x - 1, y + 1, board, tarWord, cword);
  fitWord(x - 1, y - 1, board, tarWord, cword);
  fitWord(x, y + 1, board, tarWord, cword);
  fitWord(x, y - 1, board, tarWord, cword);
  fitWord(x + 1, y, board, tarWord, cword);
  fitWord(x - 1, y, board, tarWord, cword);
  return;
}

Problem #3, Cross-Platform CSS:
Since this is a custom webapp, and did not want to re-code (port) the game to iOS and Android, I had to change CSS to accommodate them. There are ways to do this with CSS only, however I found those ways to be inconsistent and unreliable. Instead I opted to detect via JavaScript.
navigator.userAgent.indexOf("Android") != -1
And then add the required CSS
var mobileCss=document.createElement("link");
  mobileCss.setAttribute("rel","stylesheet");
  mobileCss.setAttribute("href","/stylesheets/mobile.css");
  document.body.appendChild(mobileCss);
So far this has worked reliably for me. I did however achieve separate CSS for portrait mode vs landscape mode via CSS.
@media screen and (orientation:portrait)
@media screen and (orientation:landscape)
Problem #4, Touch detection on Android & iPhone:
Turns out that window.onmousemove doesn't work on mobile devices. Instead I had to use window.ontouchmove and re-write my board highlight detection to use elementFromPoint.
document.addEventListener("touchmove",function(e){
  e.preventDefault();
  var j = document.elementFromPoint(e.touches[0].pageX, e.touches[0].pageY);
    currentWord += j.innerText;
    currentWordUsed.push(j);
    j.parentNode.parentNode.setAttribute("class",
        "tileHighlight");
}, false);

Problem #5, CSS3 on Android:
Android, while it claims to support CSS3 in reality does not. It only supports animation of one property at a time. This can be seen at the end of a Zoggle game, where the game will fade out before it gets moved to the side. This is because it would get all choppy otherwise.
Special Notes:
Socket.io Configuration
io.configure('production', function() {
  io.enable('browser client minification'); // send minified client
  io.enable('browser client etag'); // apply etag caching logic based on version
  io.set('connect timeout',2000);//if connection fails, fall back in 2 seconds
  io.enable('browser client gzip'); // gzip the file
  io.set('log level', 1); // reduce logging
  io.set('transports', [ // enable all transports (optional if you want
              // flashsocket)
  'websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling' ]);
});
app.js Caching
var cacheTime = 1000 * 60 * 60 * 1 * 1;// 1 hour
  app.use(express.static(__dirname + '/public', {
    maxAge : cacheTime
  }));

Custom 404
myapp/node_modules/express/node_modules/connect/lib/http.js
It specifies what express is doing with the 404 cases:

res.setHeader('Content-Type', 'text/plain');
res.end('Cannot ' + req.method + ' ' + req.url);
I changed this code to something like this:

res.setHeader('Content-Type', 'text/html');
res.render('errors/404', { title: 'Page not found'});