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)
   */
  
  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      = {};
  
  var imgJB = new Image();
  imgJB.src = 'j4.png';
  
  var strategy = [];
  var strategyPos = [];
  
  Pacman.FPS = 30;
  
  // If game to be played by robot.
  Pacman.autoplay = false;
  
  function makeKey(x, y) {
      return "" + x + "_" + y;
  }
  
  function asPos(pos) {
      return pos.x / 10 + "_" + pos.y /10;
  }
  
  function getRandomInt(max) {
      return Math.floor(Math.random() * Math.floor(max));
  }
  
  function invertDirection(dir) {
      if (dir == UP) return DOWN;
      if (dir == DOWN) return UP;
      if (dir == LEFT) return RIGHT;
      if (dir == RIGHT) return LEFT;
      throw "Error";
  }
  
  function asDirection(dir) {
      if (dir == UP)
          return "UP";
      if (dir == DOWN)
          return "DOWN";
      if (dir == LEFT)
          return "LEFT";
      if (dir == RIGHT)
          return "RIGHT";
      throw "Error";
  }
  
  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
      };
  };
  
  
  var dirs = [UP, DOWN, LEFT, RIGHT]
  var strategyIndex = 0;
  
  function getNextMove(map, pos) {
      if (strategy[strategyIndex] != null) {
          if (asPos(pos) != strategyPos[strategyIndex]) {
              throw "BUG: must be at " + strategyPos[strategyIndex] + " but is at " + asPos(pos)
          }
          var dir = strategy[strategyIndex++];
          return dir;
      }
      return null; // dirs[getRandomInt(3)];
  }
  
  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 = 1;
          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) {
          if (Pacman.autoplay && (/*due == null || */onGridSquare(position))) {
              due = getNextMove(map, position);
          }
  
          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 {
                  console.log("cannot go!")
                  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,
          topo      = 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 piece = map[pos.y][pos.x];
          return piece === Pacman.EMPTY ||
              piece === Pacman.BISCUIT ||
              piece === 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 wallOrIgnored(map, x, y) {
          var what = map[y][x];
          if (what == null || what == Pacman.WALL || ((y == 10) && ((x < 4) || (x >= 15)))) {
              return true;
          } else {
              return false;
          }
  
      }
  
      function testDir(topo, x0, y0, x1, y1, dir) {
          if (map[y1] != null) {
              var what = map[y1][x1];
              if (!wallOrIgnored(map, x1, y1)) {
                  topo[makeKey(x0, y0)].neighbours.push({what: topo[makeKey(x1, y1)], where: dir})
              }
          }
      }
  
      function dijkstra(topo, s) {
          var solutions = {};
          solutions[s] = [];
          solutions[s].dist = 0;
  
          while (true) {
              var parent = null;
              var nearest = null;
              var dist = Infinity;
  
              //for each existing solution
              for (var kn = 0; kn < Object.keys(solutions).length; kn++) {
                  var n = Object.keys(solutions)[kn];
                  var ndist = solutions[n].dist;
                  var adj = topo[n].neighbours;
                  // for each of its adjacent nodes...
                  for (var i = 0; i < adj.length; i++) {
                      var a = adj[i].what.me;
                      // without a solution already...
                      if (solutions[a]) {
                          continue;
                      }
                      //choose nearest node with lowest *total* cost
                      var d = 1 /* adj[a] */ + ndist;
                      if (d < dist) {
                          //reference parent
                          parent = solutions[n];
                          nearest = a;
                          dist = d;
                      }
                  }
              }
  
              // no more solutions
              if (dist === Infinity) {
                  break;
              }
  
              //extend parent's solution path
              solutions[nearest] = parent.concat(nearest);
              //extend parent's cost
              solutions[nearest].dist = dist;
          }
  
          return solutions;
      }
  
      function reset() {       
          map    = Pacman.MAP.clone();
          height = map.length;
          width  = map[0].length;
  
          if (strategy.length > 0) return
  
          // Now build the connectivity graph. For every position find all other nodes immediately reachable
          // from the the given, along with direction required to achieve that node.
          topo = {};
          for (y = 0; y < height; y++) {
              for (x = 0; x < width; x++) {
                  if (wallOrIgnored(map, x, y)) {
                      continue;
                  }
                  var key = makeKey(x, y);
                  topo[key] = {me: key, neighbours: []};
              }
          }
          //TODO: we do not take connection in the center into account yet.
          for (var y = 0; y < height; y++) {
              for (var x = 0; x < width; x++) {
                 if (wallOrIgnored(map, x, y)) {
                     continue;
                 }
                 testDir(topo, x, y, x, y+1, DOWN);
                 testDir(topo, x, y, x, y-1, UP);
                 testDir(topo, x, y, x-1, y, LEFT);
                 testDir(topo, x, y, x+1, y, RIGHT);
              }
          }
  
          // Now try to build the strategy.
          // Roughly speaking, we do DFS.
          strategy = [];
          var current = makeKey(9, 12)
          var count = 0
          // Stack to remember the last point where we had more than one variant to go.
          var memoizationStack = [];
          var visited = {};
          function getVisited(k) {
              if (visited[k] == null)
                  return 0;
              else
                  return visited[k];
          }
          function add(where, k) {
              if (where[k] == null)
                  where[k] = 1;
              else
                  where[k]++;
          }
          function addVisited(k) {
              add(visited, k);
          }
          function hasUnvisitedNeighbour(place) {
              var neighbours = topo[place].neighbours;
              for (var i = 0; i < neighbours.length; i++) {
                  if (getVisited(neighbours[i].what.me) == 0) {
                      return true;
                  }
              }
              return false;
          }
          function directionTo(topo, from, to) {
              var neighbours = topo[from].neighbours;
              for (var i = 0; i < neighbours.length; i++) {
                  if (neighbours[i].what.me == to) {
                      return neighbours[i].where;
                  }
              }
              throw Error("no dir from " + from + " to "+ to);
              return null;
          }
          while (Object.keys(visited).length < Object.keys(topo).length && count++ < 280) {
              strategyPos.push(current);
              addVisited(current);
              // For next with just select first element with the lowest visited count.
              var neighbours = topo[current].neighbours;
              var next = null;
              var dir = null;
              var variants = {};
              for (var i = 0; i < neighbours.length; i++) {
                  var key = neighbours[i].what.me;
                  if (getVisited(key) == 0) {
                      next = key;
                      dir = neighbours[i].where;
                  }
              }
              if (next == null) {
                  var paths = dijkstra(topo, current);
                  var dist = Infinity;
                  for (var i = 0; i < Object.keys(paths).length; i++) {
                      var target = Object.keys(paths)[i];
                      var path = paths[target];
                      if ((getVisited(target) == 0) && (path.dist < dist)) {
                          next = target;
                          dist = path.dist;
                      }
                  }
                  if (next == null) {
                      break;
                  }
                  var path = paths[next];
                  var place = current;
                  for (var i = 0; i < path.length; i++) {
                      strategy.push(directionTo(topo, place, path[i]));
                      if (place != current)
                          strategyPos.push(place)
                      place = path[i];
                      if (place != current) addVisited(place);
                  }
                  current = next;
                  continue;
              }
              if (next == null) break;
              strategy.push(dir);
              current = next;
          }
          var result = [];
          for (var i = 0; i < strategy.length; i++) {
              result.push(asDirection(strategy[i]));
          }
      }
  
      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.drawImage(imgJB,(j * blockSize), (i * blockSize),
                      blockSize, blockSize);
                      /*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 = (function () {
  
      var state        = WAITING,
          audio        = null,
          ghosts       = [],
          ghostSpecs   = ["#FF0000"],
          eatenCount   = 0,
          level        = 0,
          tick         = 0,
          ghostPos, userPos, 
          stateChanged = true,
          timerStart   = null,
          lastTime     = 0,
          ctx          = null,
          timer        = null,
          map          = null,
          user         = null,
          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() {
          Pacman.autoplay = false;
          setState(WAITING);
          level = 1;
          user.reset();
          map.reset();
          map.draw(ctx);
          startLevel();
      }
  
      function startNewAutoGame() {
          Pacman.autoplay = true;
          level = 1;
          user.reset();
          map.reset();
          map.draw(ctx);
          setState(PLAYING);
          user.resetPosition();
          for (var i = 0; i < ghosts.length; i += 1) {
              ghosts[i].reset();
          }
          timerStart = tick;
          strategyIndex = 0;
      }
  
      function keyDown(e) {
          if (e.keyCode === KEY.N) {
              startNewGame();
          } else if (e.keyCode === KEY.A) {
              startNewAutoGame();
          } 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);
      }
  
      var doneCallback = function () {};
      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");
                      doneCallback();
                      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 START button to start");
          } 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() {
          doneCallback();
          setState(PAUSE);
          return;
          
          // original:
          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, doneCallbackHandler) {
          if (doneCallbackHandler != null) {
              doneCallback = doneCallbackHandler;
          }
          
          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, 0, 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]}]
  ];
  
  Object.prototype.clone = function () {
      var i, newObj = (this instanceof Array) ? [] : {};
      for (i in this) {
          if (i === 'clone') {
              continue;
          }
          if (this[i] && typeof this[i] === "object") {
              newObj[i] = this[i].clone();
          } else {
              newObj[i] = this[i];
          }
      }
      return newObj;
  };
  