text:     /*jslint browser: true, undef: true, eqeqeq: true, nomen: true, white: true */
      /*global window: false, document: false */
  
      /*
       * fix looped audio
       * add fruits + levels
       * fix what happens when a ghost is eaten (should go back to base)
       * do proper ghost mechanics (blinky/wimpy etc)
       */
  
      ;(function(window, document, undefined) {
        var NONE        = 4,
        UP          = 3,
        LEFT        = 2,
        DOWN        = 1,
        RIGHT       = 11,
        WAITING     = 5,
        PAUSE       = 6,
        PLAYING     = 7,
        COUNTDOWN   = 8,
        EATEN_PAUSE = 9,
        DYING       = 10,
        Pacman      = {};
  
        Pacman.FPS = 30;
  
        Pacman.Ghost = function (game, map, colour) {
  
          var position  = null,
          direction = null,
          eatable   = null,
          eaten     = null,
          due       = null;
          
          function getNewCoord(dir, current) { 
  
            var speed  = isVunerable() ? 1 : isHidden() ? 4 : 2,
            xSpeed = (dir === LEFT && -speed || dir === RIGHT && speed || 0),
            ySpeed = (dir === DOWN && speed || dir === UP && -speed || 0);
  
            return {
              "x": addBounded(current.x, xSpeed),
              "y": addBounded(current.y, ySpeed)
            };
          };
  
          /* Collision detection(walls) is done when a ghost lands on an
           * exact block, make sure they dont skip over it 
           */
           function addBounded(x1, x2) { 
            var rem    = x1 % 10, 
            result = rem + x2;
            if (rem !== 0 && result > 10) {
              return x1 + (10 - rem);
            } else if(rem > 0 && result < 0) { 
              return x1 - rem;
            }
            return x1 + x2;
          };
          
          function isVunerable() { 
            return eatable !== null;
          };
          
          function isDangerous() {
            return eaten === null;
          };
  
          function isHidden() { 
            return eatable === null && eaten !== null;
          };
          
          function getRandomDirection() {
            var moves = (direction === LEFT || direction === RIGHT) 
            ? [UP, DOWN] : [LEFT, RIGHT];
            return moves[Math.floor(Math.random() * 2)];
          };
          
          function reset() {
            eaten = null;
            eatable = null;
            position = {"x": 90, "y": 80};
            direction = getRandomDirection();
            due = getRandomDirection();
          };
          
          function onWholeSquare(x) {
            return x % 10 === 0;
          };
          
          function oppositeDirection(dir) { 
            return dir === LEFT && RIGHT ||
            dir === RIGHT && LEFT ||
            dir === UP && DOWN || UP;
          };
  
          function makeEatable() {
            direction = oppositeDirection(direction);
            eatable = game.getTick();
          };
  
          function eat() { 
            eatable = null;
            eaten = game.getTick();
          };
  
          function pointToCoord(x) {
            return Math.round(x / 10);
          };
  
          function nextSquare(x, dir) {
            var rem = x % 10;
            if (rem === 0) { 
              return x; 
            } else if (dir === RIGHT || dir === DOWN) { 
              return x + (10 - rem);
            } else {
              return x - rem;
            }
          };
  
          function onGridSquare(pos) {
            return onWholeSquare(pos.y) && onWholeSquare(pos.x);
          };
  
          function secondsAgo(tick) { 
            return (game.getTick() - tick) / Pacman.FPS;
          };
  
          function getColour() { 
            if (eatable) { 
              if (secondsAgo(eatable) > 5) { 
                return game.getTick() % 20 > 10 ? "#FFFFFF" : "#0000BB";
              } else { 
                return "#0000BB";
              }
            } else if(eaten) { 
              return "#222";
            } 
            return colour;
          };
  
          function draw(ctx) {
  
            var s    = map.blockSize, 
            top  = (position.y/10) * s,
            left = (position.x/10) * s;
  
            if (eatable && secondsAgo(eatable) > 8) {
              eatable = null;
            }
  
            if (eaten && secondsAgo(eaten) > 3) { 
              eaten = null;
            }
  
            var tl = left + s;
            var base = top + s - 3;
            var inc = s / 10;
  
            var high = game.getTick() % 10 > 5 ? 3  : -3;
            var low  = game.getTick() % 10 > 5 ? -3 : 3;
  
            ctx.fillStyle = getColour();
            ctx.beginPath();
  
            ctx.moveTo(left, base);
  
            ctx.quadraticCurveTo(left, top, left + (s/2),  top);
            ctx.quadraticCurveTo(left + s, top, left+s,  base);
  
              // Wavy things at the bottom
              ctx.quadraticCurveTo(tl-(inc*1), base+high, tl - (inc * 2),  base);
              ctx.quadraticCurveTo(tl-(inc*3), base+low, tl - (inc * 4),  base);
              ctx.quadraticCurveTo(tl-(inc*5), base+high, tl - (inc * 6),  base);
              ctx.quadraticCurveTo(tl-(inc*7), base+low, tl - (inc * 8),  base); 
              ctx.quadraticCurveTo(tl-(inc*9), base+high, tl - (inc * 10), base); 
  
              ctx.closePath();
              ctx.fill();
  
              ctx.beginPath();
              ctx.fillStyle = "#FFF";
              ctx.arc(left + 6,top + 6, s / 6, 0, 300, false);
              ctx.arc((left + s) - 6,top + 6, s / 6, 0, 300, false);
              ctx.closePath();
              ctx.fill();
  
              var f = s / 12;
              var off = {};
              off[RIGHT] = [f, 0];
              off[LEFT]  = [-f, 0];
              off[UP]    = [0, -f];
              off[DOWN]  = [0, f];
  
              ctx.beginPath();
              ctx.fillStyle = "#000";
              ctx.arc(left+6+off[direction][0], top+6+off[direction][1], 
                s / 15, 0, 300, false);
              ctx.arc((left+s)-6+off[direction][0], top+6+off[direction][1], 
                s / 15, 0, 300, false);
              ctx.closePath();
              ctx.fill();
  
            };
  
            function pane(pos) {
  
              if (pos.y === 100 && pos.x >= 190 && direction === RIGHT) {
                return {"y": 100, "x": -10};
              }
              
              if (pos.y === 100 && pos.x <= -10 && direction === LEFT) {
                return position = {"y": 100, "x": 190};
              }
  
              return false;
            };
  
            function move(ctx) {
  
              var oldPos = position,
              onGrid = onGridSquare(position),
              npos   = null;
              
              if (due !== direction) {
  
                npos = getNewCoord(due, position);
  
                if (onGrid &&
                  map.isFloorSpace({
                    "y":pointToCoord(nextSquare(npos.y, due)),
                    "x":pointToCoord(nextSquare(npos.x, due))})) {
                  direction = due;
              } else {
                npos = null;
              }
            }
  
            if (npos === null) {
              npos = getNewCoord(direction, position);
            }
  
            if (onGrid &&
              map.isWallSpace({
                "y" : pointToCoord(nextSquare(npos.y, direction)),
                "x" : pointToCoord(nextSquare(npos.x, direction))
              })) {
  
              due = getRandomDirection();            
            return move(ctx);
          }
  
          position = npos;        
  
          var tmp = pane(position);
          if (tmp) { 
            position = tmp;
          }
  
          due = getRandomDirection();
  
          return {
            "new" : position,
            "old" : oldPos
          };
        };
  
        return {
          "eat"         : eat,
          "isVunerable" : isVunerable,
          "isDangerous" : isDangerous,
          "makeEatable" : makeEatable,
          "reset"       : reset,
          "move"        : move,
          "draw"        : draw
        };
      };
  
      Pacman.User = function (game, map) {
  
        var position  = null,
        direction = null,
        eaten     = null,
        due       = null, 
        lives     = null,
        score     = 5,
        keyMap    = {};
  
        keyMap[KEY.ARROW_LEFT]  = LEFT;
        keyMap[KEY.ARROW_UP]    = UP;
        keyMap[KEY.ARROW_RIGHT] = RIGHT;
        keyMap[KEY.ARROW_DOWN]  = DOWN;
  
        function addScore(nScore) { 
          score += nScore;
          if (score >= 10000 && score - nScore < 10000) { 
            lives += 1;
          }
        };
  
        function theScore() { 
          return score;
        };
  
        function loseLife() { 
          lives -= 1;
        };
  
        function getLives() {
          return lives;
        };
  
        function initUser() {
          score = 0;
          lives = 3;
          newLevel();
        }
  
        function newLevel() {
          resetPosition();
          eaten = 0;
        };
  
        function resetPosition() {
          position = {"x": 90, "y": 120};
          direction = LEFT;
          due = LEFT;
        };
  
        function reset() {
          initUser();
          resetPosition();
        };        
  
        function keyDown(e) {
          if (typeof keyMap[e.keyCode] !== "undefined") { 
            due = keyMap[e.keyCode];
            e.preventDefault();
            e.stopPropagation();
            return false;
          }
          return true;
        };
  
        function getNewCoord(dir, current) {   
          return {
            "x": current.x + (dir === LEFT && -2 || dir === RIGHT && 2 || 0),
            "y": current.y + (dir === DOWN && 2 || dir === UP    && -2 || 0)
          };
        };
  
        function onWholeSquare(x) {
          return x % 10 === 0;
        };
  
        function pointToCoord(x) {
          return Math.round(x/10);
        };
  
        function nextSquare(x, dir) {
          var rem = x % 10;
          if (rem === 0) { 
            return x; 
          } else if (dir === RIGHT || dir === DOWN) { 
            return x + (10 - rem);
          } else {
            return x - rem;
          }
        };
  
        function next(pos, dir) {
          return {
            "y" : pointToCoord(nextSquare(pos.y, dir)),
            "x" : pointToCoord(nextSquare(pos.x, dir)),
          };                               
        };
  
        function onGridSquare(pos) {
          return onWholeSquare(pos.y) && onWholeSquare(pos.x);
        };
  
        function isOnSamePlane(due, dir) { 
          return ((due === LEFT || due === RIGHT) && 
            (dir === LEFT || dir === RIGHT)) || 
          ((due === UP || due === DOWN) && 
           (dir === UP || dir === DOWN));
        };
  
        function move(ctx) {
  
          var npos        = null, 
          nextWhole   = null, 
          oldPosition = position,
          block       = null;
  
          if (due !== direction) {
            npos = getNewCoord(due, position);
  
            if (isOnSamePlane(due, direction) || 
              (onGridSquare(position) && 
               map.isFloorSpace(next(npos, due)))) {
              direction = due;
          } else {
            npos = null;
          }
        }
  
        if (npos === null) {
          npos = getNewCoord(direction, position);
        }
  
        if (onGridSquare(position) && map.isWallSpace(next(npos, direction))) {
          direction = NONE;
        }
  
        if (direction === NONE) {
          return {"new" : position, "old" : position};
        }
  
        if (npos.y === 100 && npos.x >= 190 && direction === RIGHT) {
          npos = {"y": 100, "x": -10};
        }
  
        if (npos.y === 100 && npos.x <= -12 && direction === LEFT) {
          npos = {"y": 100, "x": 190};
        }
  
        position = npos;        
        nextWhole = next(position, direction);
  
        block = map.block(nextWhole);        
  
        if ((isMidSquare(position.y) || isMidSquare(position.x)) &&
          block === Pacman.BISCUIT || block === Pacman.PILL) {
  
          map.setBlock(nextWhole, Pacman.EMPTY);           
        addScore((block === Pacman.BISCUIT) ? 10 : 50);
        eaten += 1;
  
        if (eaten === 182) {
          game.completedLevel();
        }
  
        if (block === Pacman.PILL) { 
          game.eatenPill();
        }
      }   
  
      return {
        "new" : position,
        "old" : oldPosition
      };
    };
  
    function isMidSquare(x) { 
      var rem = x % 10;
      return rem > 3 || rem < 7;
    };
  
    function calcAngle(dir, pos) { 
      if (dir == RIGHT && (pos.x % 10 < 5)) {
        return {"start":0.25, "end":1.75, "direction": false};
      } else if (dir === DOWN && (pos.y % 10 < 5)) { 
        return {"start":0.75, "end":2.25, "direction": false};
      } else if (dir === UP && (pos.y % 10 < 5)) { 
        return {"start":1.25, "end":1.75, "direction": true};
      } else if (dir === LEFT && (pos.x % 10 < 5)) {             
        return {"start":0.75, "end":1.25, "direction": true};
      }
      return {"start":0, "end":2, "direction": false};
    };
  
    function drawDead(ctx, amount) { 
  
      var size = map.blockSize, 
      half = size / 2;
  
      if (amount >= 1) { 
        return;
      }
  
      ctx.fillStyle = "#FFFF00";
      ctx.beginPath();        
      ctx.moveTo(((position.x/10) * size) + half, 
       ((position.y/10) * size) + half);
  
      ctx.arc(((position.x/10) * size) + half, 
        ((position.y/10) * size) + half,
        half, 0, Math.PI * 2 * amount, true); 
  
      ctx.fill();    
    };
  
    function draw(ctx) { 
  
      var s     = map.blockSize, 
      angle = calcAngle(direction, position);
  
      ctx.fillStyle = "#FFFF00";
  
      ctx.beginPath();        
  
      ctx.moveTo(((position.x/10) * s) + s / 2,
       ((position.y/10) * s) + s / 2);
  
      ctx.arc(((position.x/10) * s) + s / 2,
        ((position.y/10) * s) + s / 2,
        s / 2, Math.PI * angle.start, 
        Math.PI * angle.end, angle.direction); 
  
      ctx.fill();    
    };
  
    initUser();
  
    return {
      "draw"          : draw,
      "drawDead"      : drawDead,
      "loseLife"      : loseLife,
      "getLives"      : getLives,
      "score"         : score,
      "addScore"      : addScore,
      "theScore"      : theScore,
      "keyDown"       : keyDown,
      "move"          : move,
      "newLevel"      : newLevel,
      "reset"         : reset,
      "resetPosition" : resetPosition
    };
  };
  
  Pacman.Map = function (size) {
  
    var height    = null, 
    width     = null, 
    blockSize = size,
    pillSize  = 0,
    map       = null;
  
    function withinBounds(y, x) {
      return y >= 0 && y < height && x >= 0 && x < width;
    }
  
    function isWall(pos) {
      return withinBounds(pos.y, pos.x) && map[pos.y][pos.x] === Pacman.WALL;
    }
  
    function isFloorSpace(pos) {
      if (!withinBounds(pos.y, pos.x)) {
        return false;
      }
      var peice = map[pos.y][pos.x];
      return peice === Pacman.EMPTY || 
      peice === Pacman.BISCUIT ||
      peice === Pacman.PILL;
    }
  
    function drawWall(ctx) {
  
      var i, j, p, line;
  
      ctx.strokeStyle = "#0000FF";
      ctx.lineWidth   = 5;
      ctx.lineCap     = "round";
  
      for (i = 0; i < Pacman.WALLS.length; i += 1) {
        line = Pacman.WALLS[i];
        ctx.beginPath();
  
        for (j = 0; j < line.length; j += 1) {
  
          p = line[j];
  
          if (p.move) {
            ctx.moveTo(p.move[0] * blockSize, p.move[1] * blockSize);
          } else if (p.line) {
            ctx.lineTo(p.line[0] * blockSize, p.line[1] * blockSize);
          } else if (p.curve) {
            ctx.quadraticCurveTo(p.curve[0] * blockSize, 
             p.curve[1] * blockSize,
             p.curve[2] * blockSize, 
             p.curve[3] * blockSize);   
          }
        }
        ctx.stroke();
      }
    }
  
    function reset() {       
      map    = clone(Pacman.MAP);
      height = map.length;
      width  = map[0].length;        
    };
  
    function block(pos) {
      return map[pos.y][pos.x];
    };
  
    function setBlock(pos, type) {
      map[pos.y][pos.x] = type;
    };
  
    function drawPills(ctx) { 
  
      if (++pillSize > 30) {
        pillSize = 0;
      }
  
      for (i = 0; i < height; i += 1) {
        for (j = 0; j < width; j += 1) {
          if (map[i][j] === Pacman.PILL) {
            ctx.beginPath();
  
            ctx.fillStyle = "#000";
            ctx.fillRect((j * blockSize), (i * blockSize), 
             blockSize, blockSize);
  
            ctx.fillStyle = "#FFF";
            ctx.arc((j * blockSize) + blockSize / 2,
              (i * blockSize) + blockSize / 2,
              Math.abs(5 - (pillSize/3)), 
              0, 
              Math.PI * 2, false); 
            ctx.fill();
            ctx.closePath();
          }
        }
      }
    };
  
    function draw(ctx) {
  
      var i, j, size = blockSize;
  
      ctx.fillStyle = "#000";
      ctx.fillRect(0, 0, width * size, height * size);
  
      drawWall(ctx);
  
      for (i = 0; i < height; i += 1) {
        for (j = 0; j < width; j += 1) {
         drawBlock(i, j, ctx);
       }
     }
   };
  
   function drawBlock(y, x, ctx) {
  
    var layout = map[y][x];
  
    if (layout === Pacman.PILL) {
      return;
    }
  
    ctx.beginPath();
  
    if (layout === Pacman.EMPTY || layout === Pacman.BLOCK || 
      layout === Pacman.BISCUIT) {
  
      ctx.fillStyle = "#000";
    ctx.fillRect((x * blockSize), (y * blockSize), 
     blockSize, blockSize);
  
    if (layout === Pacman.BISCUIT) {
      ctx.fillStyle = "#FFF";
      ctx.fillRect((x * blockSize) + (blockSize / 2.5), 
       (y * blockSize) + (blockSize / 2.5), 
       blockSize / 6, blockSize / 6);
    }
  }
  ctx.closePath();	 
  };
  
  reset();
  
  return {
    "draw"         : draw,
    "drawBlock"    : drawBlock,
    "drawPills"    : drawPills,
    "block"        : block,
    "setBlock"     : setBlock,
    "reset"        : reset,
    "isWallSpace"  : isWall,
    "isFloorSpace" : isFloorSpace,
    "height"       : height,
    "width"        : width,
    "blockSize"    : blockSize
  };
  };
  
  Pacman.Audio = function(game) {
  
    var files          = [], 
    endEvents      = [],
    progressEvents = [],
    playing        = [];
  
    function load(name, path, cb) { 
  
      var f = files[name] = document.createElement("audio");
  
      progressEvents[name] = function(event) { progress(event, name, cb); };
  
      f.addEventListener("canplaythrough", progressEvents[name], true);
      f.setAttribute("preload", "true");
      f.setAttribute("autobuffer", "true");
      f.setAttribute("src", path);
      f.pause();        
    };
  
    function progress(event, name, callback) { 
      if (event.loaded === event.total && typeof callback === "function") {
        callback();
        files[name].removeEventListener("canplaythrough", 
          progressEvents[name], true);
      }
    };
  
    function disableSound() {
      for (var i = 0; i < playing.length; i++) {
        files[playing[i]].pause();
        files[playing[i]].currentTime = 0;
      }
      playing = [];
    };
  
    function ended(name) { 
  
      var i, tmp = [], found = false;
  
      files[name].removeEventListener("ended", endEvents[name], true);
  
      for (i = 0; i < playing.length; i++) {
        if (!found && playing[i]) { 
          found = true;
        } else { 
          tmp.push(playing[i]);
        }
      }
      playing = tmp;
    };
  
    function play(name) { 
      if (!game.soundDisabled()) {
        endEvents[name] = function() { ended(name); };
        playing.push(name);
        files[name].addEventListener("ended", endEvents[name], true);
        files[name].play();
      }
    };
  
    function pause() { 
      for (var i = 0; i < playing.length; i++) {
        files[playing[i]].pause();
      }
    };
  
    function resume() { 
      for (var i = 0; i < playing.length; i++) {
        files[playing[i]].play();
      }        
    };
  
    return {
      "disableSound" : disableSound,
      "load"         : load,
      "play"         : play,
      "pause"        : pause,
      "resume"       : resume
    };
  };
  
  var PACMAN = window.PACMAN = function () {
  
    var state        = WAITING;
    var audio        = null;
    var ghosts       = [];
    var ghostSpecs   = ["#00FFDE", "#FF0000", "#FFB8DE", "#FFB847"];
    var eatenCount   = 0;
    var level        = 0;
    var tick         = 0;
    var ghostPos, userPos;
    var stateChanged = true;
    var timerStart   = null;
    var lastTime     = 0;
    var ctx          = null;
    var timer        = null;
    var map          = null;
    var user         = null;
    var stored       = null;
  
    function getTick() { 
      return tick;
    };
  
    function drawScore(text, position) {
      ctx.fillStyle = "#FFFFFF";
      ctx.font      = "12px BDCartoonShoutRegular";
      ctx.fillText(text, 
       (position["new"]["x"] / 10) * map.blockSize, 
       ((position["new"]["y"] + 5) / 10) * map.blockSize);
    }
  
    function dialog(text) {
      ctx.fillStyle = "#FFFF00";
      ctx.font      = "14px BDCartoonShoutRegular";
      var width = ctx.measureText(text).width,
      x     = ((map.width * map.blockSize) - width) / 2;        
      ctx.fillText(text, x, (map.height * 10) + 8);
    }
  
    function soundDisabled() {
      return localStorage["soundDisabled"] === "true";
    };
  
    function startLevel() {        
      user.resetPosition();
      for (var i = 0; i < ghosts.length; i += 1) { 
        ghosts[i].reset();
      }
      audio.play("start");
      timerStart = tick;
      setState(COUNTDOWN);
    }    
  
    function startNewGame() {
      setState(WAITING);
      level = 1;
      user.reset();
      map.reset();
      map.draw(ctx);
      startLevel();
    }
  
    function keyDown(e) {
      if (e.keyCode === KEY.N) {
        startNewGame();
      } else if (e.keyCode === KEY.S) {
        audio.disableSound();
        localStorage["soundDisabled"] = !soundDisabled();
      } else if (e.keyCode === KEY.P && state === PAUSE) {
        audio.resume();
        map.draw(ctx);
        setState(stored);
      } else if (e.keyCode === KEY.P) {
        stored = state;
        setState(PAUSE);
        audio.pause();
        map.draw(ctx);
        dialog("Paused");
      } else if (state !== PAUSE) {   
        return user.keyDown(e);
      }
      return true;
    }    
  
    function loseLife() {        
      setState(WAITING);
      user.loseLife();
      if (user.getLives() > 0) {
        startLevel();
      }
    }
  
    function setState(nState) { 
      state = nState;
      stateChanged = true;
    };
  
    function collided(user, ghost) {
      return (Math.sqrt(Math.pow(ghost.x - user.x, 2) + 
        Math.pow(ghost.y - user.y, 2))) < 10;
    };
  
    function drawFooter() {
  
      var topLeft  = (map.height * map.blockSize),
      textBase = topLeft + 17;
  
      ctx.fillStyle = "#000000";
      ctx.fillRect(0, topLeft, (map.width * map.blockSize), 30);
  
      ctx.fillStyle = "#FFFF00";
  
      for (var i = 0, len = user.getLives(); i < len; i++) {
        ctx.fillStyle = "#FFFF00";
        ctx.beginPath();
        ctx.moveTo(150 + (25 * i) + map.blockSize / 2,
         (topLeft+1) + map.blockSize / 2);
  
        ctx.arc(150 + (25 * i) + map.blockSize / 2,
          (topLeft+1) + map.blockSize / 2,
          map.blockSize / 2, Math.PI * 0.25, Math.PI * 1.75, false);
        ctx.fill();
      }
  
      ctx.fillStyle = !soundDisabled() ? "#00FF00" : "#FF0000";
      ctx.font = "bold 16px sans-serif";
              //ctx.fillText("♪", 10, textBase);
              ctx.fillText("s", 10, textBase);
  
              ctx.fillStyle = "#FFFF00";
              ctx.font      = "14px BDCartoonShoutRegular";
              ctx.fillText("Score: " + user.theScore(), 30, textBase);
              ctx.fillText("Level: " + level, 260, textBase);
            }
  
            function redrawBlock(pos) {
              map.drawBlock(Math.floor(pos.y/10), Math.floor(pos.x/10), ctx);
              map.drawBlock(Math.ceil(pos.y/10), Math.ceil(pos.x/10), ctx);
            }
  
            function mainDraw() { 
  
              var diff, u, i, len, nScore;
              
              ghostPos = [];
  
              for (i = 0, len = ghosts.length; i < len; i += 1) {
                ghostPos.push(ghosts[i].move(ctx));
              }
              u = user.move(ctx);
              
              for (i = 0, len = ghosts.length; i < len; i += 1) {
                redrawBlock(ghostPos[i].old);
              }
              redrawBlock(u.old);
              
              for (i = 0, len = ghosts.length; i < len; i += 1) {
                ghosts[i].draw(ctx);
              }                     
              user.draw(ctx);
              
              userPos = u["new"];
              
              for (i = 0, len = ghosts.length; i < len; i += 1) {
                if (collided(userPos, ghostPos[i]["new"])) {
                  if (ghosts[i].isVunerable()) { 
                    audio.play("eatghost");
                    ghosts[i].eat();
                    eatenCount += 1;
                    nScore = eatenCount * 50;
                    drawScore(nScore, ghostPos[i]);
                    user.addScore(nScore);                    
                    setState(EATEN_PAUSE);
                    timerStart = tick;
                  } else if (ghosts[i].isDangerous()) {
                    audio.play("die");
                    setState(DYING);
                    timerStart = tick;
                  }
                }
              }                             
            };
  
            function mainLoop() {
  
              var diff;
  
              if (state !== PAUSE) { 
                ++tick;
              }
  
              map.drawPills(ctx);
  
              if (state === PLAYING) {
                mainDraw();
              } else if (state === WAITING && stateChanged) {            
                stateChanged = false;
                map.draw(ctx);
                dialog("Press N to start a New game");            
              } else if (state === EATEN_PAUSE && 
               (tick - timerStart) > (Pacman.FPS / 3)) {
                map.draw(ctx);
                setState(PLAYING);
              } else if (state === DYING) {
                if (tick - timerStart > (Pacman.FPS * 2)) { 
                  loseLife();
                } else { 
                  redrawBlock(userPos);
                  for (i = 0, len = ghosts.length; i < len; i += 1) {
                    redrawBlock(ghostPos[i].old);
                    ghostPos.push(ghosts[i].draw(ctx));
                  }                                   
                  user.drawDead(ctx, (tick - timerStart) / (Pacman.FPS * 2));
                }
              } else if (state === COUNTDOWN) {
  
                diff = 5 + Math.floor((timerStart - tick) / Pacman.FPS);
  
                if (diff === 0) {
                  map.draw(ctx);
                  setState(PLAYING);
                } else {
                  if (diff !== lastTime) { 
                    lastTime = diff;
                    map.draw(ctx);
                    dialog("Starting in: " + diff);
                  }
                }
              } 
  
              drawFooter();
            }
  
            function eatenPill() {
              audio.play("eatpill");
              timerStart = tick;
              eatenCount = 0;
              for (i = 0; i < ghosts.length; i += 1) {
                ghosts[i].makeEatable(ctx);
              }        
            };
  
            function completedLevel() {
              setState(WAITING);
              level += 1;
              map.reset();
              user.newLevel();
              startLevel();
            };
  
            function keyPress(e) { 
              if (state !== WAITING && state !== PAUSE) { 
                e.preventDefault();
                e.stopPropagation();
              }
            };
  
            function init(wrapper, root) {
  
              var i, len, ghost,
              blockSize = wrapper.offsetWidth / 19,
              canvas    = document.createElement("canvas");
              
              canvas.setAttribute("width", (blockSize * 19) + "px");
              canvas.setAttribute("height", (blockSize * 22) + 30 + "px");
  
              wrapper.appendChild(canvas);
  
              ctx  = canvas.getContext('2d');
  
              audio = new Pacman.Audio({"soundDisabled":soundDisabled});
              map   = new Pacman.Map(blockSize);
              user  = new Pacman.User({ 
                "completedLevel" : completedLevel, 
                "eatenPill"      : eatenPill 
              }, map);
  
              for (i = 0, len = ghostSpecs.length; i < len; i += 1) {
                ghost = new Pacman.Ghost({"getTick":getTick}, map, ghostSpecs[i]);
                ghosts.push(ghost);
              }
              
              map.draw(ctx);
              dialog("Loading ...");
  
              var extension = Modernizr.audio.ogg ? 'ogg' : 'mp3';
  
              var audio_files = [
              ["start", root + "audio/opening_song." + extension],
              ["die", root + "audio/die." + extension],
              ["eatghost", root + "audio/eatghost." + extension],
              ["eatpill", root + "audio/eatpill." + extension],
              ["eating", root + "audio/eating.short." + extension],
              ["eating2", root + "audio/eating.short." + extension]
              ];
  
              load(audio_files, function() { loaded(); });
            };
  
            function load(arr, callback) { 
  
              if (arr.length === 0) { 
                callback();
              } else { 
                var x = arr.pop();
                audio.load(x[0], x[1], function() { load(arr, callback); });
              }
            };
  
            function loaded() {
  
              dialog("Press N to Start");
              
              document.addEventListener("keydown", keyDown, true);
              document.addEventListener("keypress", keyPress, true); 
              
              timer = window.setInterval(mainLoop, 1000 / Pacman.FPS);
            };
  
            return {
              "init" : init
            };
  
          };
  
      /* Human readable keyCode index */
      var KEY = {'BACKSPACE': 8, 'TAB': 9, 'NUM_PAD_CLEAR': 12, 'ENTER': 13, 'SHIFT': 16, 'CTRL': 17, 'ALT': 18, 'PAUSE': 19, 'CAPS_LOCK': 20, 'ESCAPE': 27, 'SPACEBAR': 32, 'PAGE_UP': 33, 'PAGE_DOWN': 34, 'END': 35, 'HOME': 36, 'ARROW_LEFT': 37, 'ARROW_UP': 38, 'ARROW_RIGHT': 39, 'ARROW_DOWN': 40, 'PRINT_SCREEN': 44, 'INSERT': 45, 'DELETE': 46, 'SEMICOLON': 59, 'WINDOWS_LEFT': 91, 'WINDOWS_RIGHT': 92, 'SELECT': 93, 'NUM_PAD_ASTERISK': 106, 'NUM_PAD_PLUS_SIGN': 107, 'NUM_PAD_HYPHEN-MINUS': 109, 'NUM_PAD_FULL_STOP': 110, 'NUM_PAD_SOLIDUS': 111, 'NUM_LOCK': 144, 'SCROLL_LOCK': 145, 'SEMICOLON': 186, 'EQUALS_SIGN': 187, 'COMMA': 188, 'HYPHEN-MINUS': 189, 'FULL_STOP': 190, 'SOLIDUS': 191, 'GRAVE_ACCENT': 192, 'LEFT_SQUARE_BRACKET': 219, 'REVERSE_SOLIDUS': 220, 'RIGHT_SQUARE_BRACKET': 221, 'APOSTROPHE': 222};
  
      (function () {
      	/* 0 - 9 */
      	for (var i = 48; i <= 57; i++) {
          KEY['' + (i - 48)] = i;
        }
        /* A - Z */
        for (i = 65; i <= 90; i++) {
          KEY['' + String.fromCharCode(i)] = i;
        }
        /* NUM_PAD_0 - NUM_PAD_9 */
        for (i = 96; i <= 105; i++) {
          KEY['NUM_PAD_' + (i - 96)] = i;
        }
        /* F1 - F12 */
        for (i = 112; i <= 123; i++) {
          KEY['F' + (i - 112 + 1)] = i;
        }
      })();
  
      Pacman.WALL    = 0;
      Pacman.BISCUIT = 1;
      Pacman.EMPTY   = 2;
      Pacman.BLOCK   = 3;
      Pacman.PILL    = 4;
  
      Pacman.MAP = [
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
      [0, 4, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 4, 0],
      [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0],
      [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
      [0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0],
      [0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0],
      [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
      [2, 2, 2, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 2, 2, 2],
      [0, 0, 0, 0, 1, 0, 1, 0, 0, 3, 0, 0, 1, 0, 1, 0, 0, 0, 0],
      [2, 2, 2, 2, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 2, 2, 2, 2],
      [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
      [2, 2, 2, 0, 1, 0, 1, 1, 1, 2, 1, 1, 1, 0, 1, 0, 2, 2, 2],
      [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
      [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
      [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0],
      [0, 4, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 4, 0],
      [0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0],
      [0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0],
      [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
      [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
      ];
  
      Pacman.WALLS = [
  
      [{"move": [0, 9.5]}, {"line": [3, 9.5]},
      {"curve": [3.5, 9.5, 3.5, 9]}, {"line": [3.5, 8]},
      {"curve": [3.5, 7.5, 3, 7.5]}, {"line": [1, 7.5]},
      {"curve": [0.5, 7.5, 0.5, 7]}, {"line": [0.5, 1]},
      {"curve": [0.5, 0.5, 1, 0.5]}, {"line": [9, 0.5]},
      {"curve": [9.5, 0.5, 9.5, 1]}, {"line": [9.5, 3.5]}],
  
      [{"move": [9.5, 1]},
      {"curve": [9.5, 0.5, 10, 0.5]}, {"line": [18, 0.5]},
      {"curve": [18.5, 0.5, 18.5, 1]}, {"line": [18.5, 7]},
      {"curve": [18.5, 7.5, 18, 7.5]}, {"line": [16, 7.5]},
      {"curve": [15.5, 7.5, 15.5, 8]}, {"line": [15.5, 9]},
      {"curve": [15.5, 9.5, 16, 9.5]}, {"line": [19, 9.5]}],
  
      [{"move": [2.5, 5.5]}, {"line": [3.5, 5.5]}],
  
      [{"move": [3, 2.5]},
      {"curve": [3.5, 2.5, 3.5, 3]},
      {"curve": [3.5, 3.5, 3, 3.5]},
      {"curve": [2.5, 3.5, 2.5, 3]},
      {"curve": [2.5, 2.5, 3, 2.5]}],
  
      [{"move": [15.5, 5.5]}, {"line": [16.5, 5.5]}],
  
      [{"move": [16, 2.5]}, {"curve": [16.5, 2.5, 16.5, 3]},
      {"curve": [16.5, 3.5, 16, 3.5]}, {"curve": [15.5, 3.5, 15.5, 3]},
      {"curve": [15.5, 2.5, 16, 2.5]}],
  
      [{"move": [6, 2.5]}, {"line": [7, 2.5]}, {"curve": [7.5, 2.5, 7.5, 3]},
      {"curve": [7.5, 3.5, 7, 3.5]}, {"line": [6, 3.5]},
      {"curve": [5.5, 3.5, 5.5, 3]}, {"curve": [5.5, 2.5, 6, 2.5]}],
  
      [{"move": [12, 2.5]}, {"line": [13, 2.5]}, {"curve": [13.5, 2.5, 13.5, 3]},
      {"curve": [13.5, 3.5, 13, 3.5]}, {"line": [12, 3.5]},
      {"curve": [11.5, 3.5, 11.5, 3]}, {"curve": [11.5, 2.5, 12, 2.5]}],
  
      [{"move": [7.5, 5.5]}, {"line": [9, 5.5]}, {"curve": [9.5, 5.5, 9.5, 6]},
      {"line": [9.5, 7.5]}],
      [{"move": [9.5, 6]}, {"curve": [9.5, 5.5, 10.5, 5.5]},
      {"line": [11.5, 5.5]}],
  
  
      [{"move": [5.5, 5.5]}, {"line": [5.5, 7]}, {"curve": [5.5, 7.5, 6, 7.5]},
      {"line": [7.5, 7.5]}],
      [{"move": [6, 7.5]}, {"curve": [5.5, 7.5, 5.5, 8]}, {"line": [5.5, 9.5]}],
  
      [{"move": [13.5, 5.5]}, {"line": [13.5, 7]},
      {"curve": [13.5, 7.5, 13, 7.5]}, {"line": [11.5, 7.5]}],
      [{"move": [13, 7.5]}, {"curve": [13.5, 7.5, 13.5, 8]},
      {"line": [13.5, 9.5]}],
  
      [{"move": [0, 11.5]}, {"line": [3, 11.5]}, {"curve": [3.5, 11.5, 3.5, 12]},
      {"line": [3.5, 13]}, {"curve": [3.5, 13.5, 3, 13.5]}, {"line": [1, 13.5]},
      {"curve": [0.5, 13.5, 0.5, 14]}, {"line": [0.5, 17]},
      {"curve": [0.5, 17.5, 1, 17.5]}, {"line": [1.5, 17.5]}],
      [{"move": [1, 17.5]}, {"curve": [0.5, 17.5, 0.5, 18]}, {"line": [0.5, 21]},
      {"curve": [0.5, 21.5, 1, 21.5]}, {"line": [18, 21.5]},
      {"curve": [18.5, 21.5, 18.5, 21]}, {"line": [18.5, 18]},
      {"curve": [18.5, 17.5, 18, 17.5]}, {"line": [17.5, 17.5]}],
      [{"move": [18, 17.5]}, {"curve": [18.5, 17.5, 18.5, 17]},
      {"line": [18.5, 14]}, {"curve": [18.5, 13.5, 18, 13.5]},
      {"line": [16, 13.5]}, {"curve": [15.5, 13.5, 15.5, 13]},
      {"line": [15.5, 12]}, {"curve": [15.5, 11.5, 16, 11.5]},
      {"line": [19, 11.5]}],
  
      [{"move": [5.5, 11.5]}, {"line": [5.5, 13.5]}],
      [{"move": [13.5, 11.5]}, {"line": [13.5, 13.5]}],
  
      [{"move": [2.5, 15.5]}, {"line": [3, 15.5]},
      {"curve": [3.5, 15.5, 3.5, 16]}, {"line": [3.5, 17.5]}],
      [{"move": [16.5, 15.5]}, {"line": [16, 15.5]},
      {"curve": [15.5, 15.5, 15.5, 16]}, {"line": [15.5, 17.5]}],
  
      [{"move": [5.5, 15.5]}, {"line": [7.5, 15.5]}],
      [{"move": [11.5, 15.5]}, {"line": [13.5, 15.5]}],
  
      [{"move": [2.5, 19.5]}, {"line": [5, 19.5]},
      {"curve": [5.5, 19.5, 5.5, 19]}, {"line": [5.5, 17.5]}],
      [{"move": [5.5, 19]}, {"curve": [5.5, 19.5, 6, 19.5]},
      {"line": [7.5, 19.5]}],
  
      [{"move": [11.5, 19.5]}, {"line": [13, 19.5]},
      {"curve": [13.5, 19.5, 13.5, 19]}, {"line": [13.5, 17.5]}],
      [{"move": [13.5, 19]}, {"curve": [13.5, 19.5, 14, 19.5]},
      {"line": [16.5, 19.5]}],
  
      [{"move": [7.5, 13.5]}, {"line": [9, 13.5]},
      {"curve": [9.5, 13.5, 9.5, 14]}, {"line": [9.5, 15.5]}],
      [{"move": [9.5, 14]}, {"curve": [9.5, 13.5, 10, 13.5]},
      {"line": [11.5, 13.5]}],
  
      [{"move": [7.5, 17.5]}, {"line": [9, 17.5]},
      {"curve": [9.5, 17.5, 9.5, 18]}, {"line": [9.5, 19.5]}],
      [{"move": [9.5, 18]}, {"curve": [9.5, 17.5, 10, 17.5]},
      {"line": [11.5, 17.5]}],
  
      [{"move": [8.5, 9.5]}, {"line": [8, 9.5]}, {"curve": [7.5, 9.5, 7.5, 10]},
      {"line": [7.5, 11]}, {"curve": [7.5, 11.5, 8, 11.5]},
      {"line": [11, 11.5]}, {"curve": [11.5, 11.5, 11.5, 11]},
      {"line": [11.5, 10]}, {"curve": [11.5, 9.5, 11, 9.5]},
      {"line": [10.5, 9.5]}]
      ];
  
      function clone(_this) {
        var i, newObj = (_this instanceof Array) ? [] : {};
        for (i in _this) {
          if (i === 'clone') {
            continue;
          }
          if (_this[i] && typeof _this[i] === "object") {
            newObj[i] = clone(_this[i]);
          } else {
            newObj[i] = _this[i];
          }
        }
        return newObj;
      };
  
    })(window, document);