text: /**
   * Blockly Apps: Maze
   *
   * Copyright 2012 Google Inc.
   * https://developers.google.com/blockly/
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  /**
   * @fileoverview JavaScript for Blockly's Maze application.
   * @author fraser@google.com (Neil Fraser)
   */
  'use strict';
  
  /**
   * Create a namespace for the application.
   */
  var Maze = {};
  
  // Temporary hack to fix bug #349701 in Chrome 34.
  // Harmless for other browsers.
  var CHROME34 = navigator.userAgent.indexOf('Chrome/34') != -1;
  
  // Supported languages.
  BlocklyApps.LANGUAGES =
      ['ar', 'br', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fr',
       'gl', 'hu', 'hrx', 'ia', 'is', 'it', 'ja', 'ko', 'lv', 'mk', 'ms', 'nl',
       'pl', 'pms', 'pt-br', 'ro', 'ru', 'sco', 'sk', 'si', 'sr', 'sv', 'sw',
       'th', 'tr', 'uk', 'vi', 'zh-hans', 'zh-hant'];
  BlocklyApps.LANG = BlocklyApps.getLang();
  
  document.write('<script type="text/javascript" src="generated/' +
                 BlocklyApps.LANG + '.js"></script>\n');
  
  Maze.MAX_LEVEL = 10;
  Maze.LEVEL = BlocklyApps.getNumberParamFromUrl('level', 1, Maze.MAX_LEVEL);
  Maze.MAX_BLOCKS = [undefined, // Level 0.
      Infinity, Infinity, 2, 5, 5, 5, 5, 10, 7, 10][Maze.LEVEL];
  
  // Crash type constants.
  Maze.CRASH_STOP = 1;
  Maze.CRASH_SPIN = 2;
  Maze.CRASH_FALL = 3;
  
  Maze.SKINS = [
    // sprite: A 1029x51 set of 21 avatar images.
    // tiles: A 250x200 set of 20 map images.
    // marker: A 20x34 goal image.
    // background: An optional 400x450 background image, or false.
    // graph: Colour of optional grid lines, or false.
    // look: Colour of sonar-like look icon.
    // winSound: List of sounds (in various formats) to play when the player wins.
    // crashSound: List of sounds (in various formats) for player crashes.
    // crashType: Behaviour when player crashes (stop, spin, or fall).
    {
      sprite: 'pegman.png',
      tiles: 'tiles_pegman.png',
      marker: 'marker.png',
      background: false,
      graph: false,
      look: '#000',
      winSound: ['apps/maze/win.mp3', 'apps/maze/win.ogg'],
      crashSound: ['apps/maze/fail_pegman.mp3', 'apps/maze/fail_pegman.ogg'],
      crashType: Maze.CRASH_STOP
    },
    {
      sprite: 'astro.png',
      tiles: 'tiles_astro.png',
      marker: 'marker.png',
      background: 'bg_astro.jpg',
      // Coma star cluster, photo by George Hatfield, used with permission.
      graph: false,
      look: '#fff',
      winSound: ['apps/maze/win.mp3', 'apps/maze/win.ogg'],
      crashSound: ['apps/maze/fail_astro.mp3', 'apps/maze/fail_astro.ogg'],
      crashType: Maze.CRASH_SPIN
    },
    {
      sprite: 'panda.png',
      tiles: 'tiles_panda.png',
      marker: 'marker.png',
      background: 'bg_panda.jpg',
      // Spring canopy, photo by Rupert Fleetingly, CC licensed for reuse.
      graph: false,
      look: '#000',
      winSound: ['apps/maze/win.mp3', 'apps/maze/win.ogg'],
      crashSound: ['apps/maze/fail_panda.mp3', 'apps/maze/fail_panda.ogg'],
      crashType: Maze.CRASH_FALL
    }
  ];
  Maze.SKIN_ID = BlocklyApps.getNumberParamFromUrl('skin', 0, Maze.SKINS.length);
  Maze.SKIN = Maze.SKINS[Maze.SKIN_ID];
  
  /**
   * Milliseconds between each animation frame.
   */
  Maze.stepSpeed;
  
  /**
   * The types of squares in the maze, which is represented
   * as a 2D array of SquareType values.
   * @enum {number}
   */
  Maze.SquareType = {
    WALL: 0,
    OPEN: 1,
    START: 2,
    FINISH: 3
  };
  
  // The maze square constants defined above are inlined here
  // for ease of reading and writing the static mazes.
  Maze.map = [
  // Level 0.
   undefined,
  // Level 1.
   [[0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0],
    [0, 0, 2, 1, 3, 0, 0],
    [0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0]],
  // Level 2.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 3, 0, 0, 0],
    [0, 0, 2, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 3.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 2, 1, 1, 1, 1, 3, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 4.
  /**
   * Note, the path continues past the start and the goal in both directions.
   * This is intentionally done so users see the maze is about getting from
   * the start to the goal and not necessarily about moving over every part of
   * the maze, 'mowing the lawn' as Neil calls it.
   */
   [[0, 0, 0, 0, 0, 0, 0, 1],
    [0, 0, 0, 0, 0, 0, 1, 1],
    [0, 0, 0, 0, 0, 1, 3, 0],
    [0, 0, 0, 0, 1, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 0, 0, 0, 0],
    [0, 2, 1, 0, 0, 0, 0, 0],
    [1, 1, 0, 0, 0, 0, 0, 0]],
  // Level 5.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 3, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 2, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 6.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 0, 0, 0, 1, 0, 0],
    [0, 1, 1, 3, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0],
    [0, 2, 1, 1, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 7.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 1, 0],
    [0, 2, 1, 1, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 1, 0],
    [0, 1, 1, 3, 0, 1, 0, 0],
    [0, 1, 0, 1, 0, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 8.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1, 0, 0, 0],
    [0, 1, 0, 0, 1, 1, 0, 0],
    [0, 1, 1, 1, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 1, 0, 0],
    [0, 2, 1, 1, 0, 3, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 9.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1, 1, 0, 0],
    [0, 0, 1, 0, 0, 0, 0, 0],
    [3, 1, 1, 1, 1, 1, 1, 0],
    [0, 1, 0, 1, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 0, 1, 0],
    [0, 1, 0, 1, 0, 2, 1, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]],
  // Level 10.
   [[0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 0, 3, 0, 1, 0],
    [0, 1, 1, 0, 1, 1, 1, 0],
    [0, 1, 0, 1, 0, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 0],
    [0, 0, 0, 1, 0, 0, 1, 0],
    [0, 2, 1, 1, 1, 0, 1, 0],
    [0, 0, 0, 0, 0, 0, 0, 0]]
  ][Maze.LEVEL];
  
  /**
   * Measure maze dimensions and set sizes.
   * ROWS: Number of tiles down.
   * COLS: Number of tiles across.
   * SQUARE_SIZE: Pixel height and width of each maze square (i.e. tile).
   */
  Maze.ROWS = Maze.map.length;
  Maze.COLS = Maze.map[0].length;
  Maze.SQUARE_SIZE = 50;
  Maze.PEGMAN_HEIGHT = 52;
  Maze.PEGMAN_WIDTH = 49;
  
  Maze.MAZE_WIDTH = Maze.SQUARE_SIZE * Maze.COLS;
  Maze.MAZE_HEIGHT = Maze.SQUARE_SIZE * Maze.ROWS;
  Maze.PATH_WIDTH = Maze.SQUARE_SIZE / 3;
  
  /**
   * Constants for cardinal directions.  Subsequent code assumes these are
   * in the range 0..3 and that opposites have an absolute difference of 2.
   * @enum {number}
   */
  Maze.DirectionType = {
    NORTH: 0,
    EAST: 1,
    SOUTH: 2,
    WEST: 3
  };
  
  /**
   * Outcomes of running the user program.
   */
  Maze.ResultType = {
    UNSET: 0,
    SUCCESS: 1,
    FAILURE: -1,
    TIMEOUT: 2,
    ERROR: -2
  };
  
  /**
   * Result of last execution.
   */
  Maze.result = Maze.ResultType.UNSET;
  
  /**
   * Starting direction.
   */
  Maze.startDirection = Maze.DirectionType.EAST;
  
  /**
   * PIDs of animation tasks currently executing.
   */
  Maze.pidList = [];
  
  // Map each possible shape to a sprite.
  // Input: Binary string representing Centre/North/West/South/East squares.
  // Output: [x, y] coordinates of each tile's sprite in tiles.png.
  Maze.tile_SHAPES = {
    '10010': [4, 0],  // Dead ends
    '10001': [3, 3],
    '11000': [0, 1],
    '10100': [0, 2],
    '11010': [4, 1],  // Vertical
    '10101': [3, 2],  // Horizontal
    '10110': [0, 0],  // Elbows
    '10011': [2, 0],
    '11001': [4, 2],
    '11100': [2, 3],
    '11110': [1, 1],  // Junctions
    '10111': [1, 0],
    '11011': [2, 1],
    '11101': [1, 2],
    '11111': [2, 2],  // Cross
    'null0': [4, 3],  // Empty
    'null1': [3, 0],
    'null2': [3, 1],
    'null3': [0, 3],
    'null4': [1, 3]
  };
  
  /**
   * Create and layout all the nodes for the path, scenery, Pegman, and goal.
   */
  Maze.drawMap = function() {
    var svg = document.getElementById('svgMaze');
    var scale = Math.max(Maze.ROWS, Maze.COLS) * Maze.SQUARE_SIZE;
    svg.setAttribute('viewBox', '0 0 ' + scale + ' ' + scale);
  
    // Draw the outer square.
    var square = document.createElementNS(Blockly.SVG_NS, 'rect');
    square.setAttribute('width', Maze.MAZE_WIDTH);
    square.setAttribute('height', Maze.MAZE_HEIGHT);
    square.setAttribute('fill', '#F1EEE7');
    square.setAttribute('stroke-width', 1);
    square.setAttribute('stroke', '#CCB');
    svg.appendChild(square);
  
    if (Maze.SKIN.background) {
      var tile = document.createElementNS(Blockly.SVG_NS, 'image');
      tile.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
          Maze.SKIN.background);
      tile.setAttribute('height', Maze.MAZE_HEIGHT);
      tile.setAttribute('width', Maze.MAZE_WIDTH);
      tile.setAttribute('x', 0);
      tile.setAttribute('y', 0);
      svg.appendChild(tile);
    }
  
    if (Maze.SKIN.graph) {
      // Draw the grid lines.
      // The grid lines are offset so that the lines pass through the centre of
      // each square.  A half-pixel offset is also added to as standard SVG
      // practice to avoid blurriness.
      var offset = Maze.SQUARE_SIZE / 2 + 0.5;
      for (var k = 0; k < Maze.ROWS; k++) {
        var h_line = document.createElementNS(Blockly.SVG_NS, 'line');
        h_line.setAttribute('y1', k * Maze.SQUARE_SIZE + offset);
        h_line.setAttribute('x2', Maze.MAZE_WIDTH);
        h_line.setAttribute('y2', k * Maze.SQUARE_SIZE + offset);
        h_line.setAttribute('stroke', Maze.SKIN.graph);
        h_line.setAttribute('stroke-width', 1);
        svg.appendChild(h_line);
      }
      for (var k = 0; k < Maze.COLS; k++) {
        var v_line = document.createElementNS(Blockly.SVG_NS, 'line');
        v_line.setAttribute('x1', k * Maze.SQUARE_SIZE + offset);
        v_line.setAttribute('x2', k * Maze.SQUARE_SIZE + offset);
        v_line.setAttribute('y2', Maze.MAZE_HEIGHT);
        v_line.setAttribute('stroke', Maze.SKIN.graph);
        v_line.setAttribute('stroke-width', 1);
        svg.appendChild(v_line);
      }
    }
  
    // Draw the tiles making up the maze map.
  
    // Return a value of '0' if the specified square is wall or out of bounds,
    // '1' otherwise (empty, start, finish).
    var normalize = function(x, y) {
      if (x < 0 || x >= Maze.COLS || y < 0 || y >= Maze.ROWS) {
        return '0';
      }
      return (Maze.map[y][x] == Maze.SquareType.WALL) ? '0' : '1';
    };
  
    // Compute and draw the tile for each square.
    var tileId = 0;
    for (var y = 0; y < Maze.ROWS; y++) {
      for (var x = 0; x < Maze.COLS; x++) {
        // Compute the tile index.
        var tile = normalize(x, y) +
            normalize(x, y - 1) +  // North.
            normalize(x + 1, y) +  // West.
            normalize(x, y + 1) +  // South.
            normalize(x - 1, y);   // East.
  
        // Draw the tile.
        if (!Maze.tile_SHAPES[tile]) {
          // Empty square.  Use null0 for large areas, with null1-4 for borders.
          // Add some randomness to avoid large empty spaces.
          if (tile == '00000' && Math.random() > 0.3) {
            tile = 'null0';
          } else {
            tile = 'null' + Math.floor(1 + Math.random() * 4);
          }
        }
        var left = Maze.tile_SHAPES[tile][0];
        var top = Maze.tile_SHAPES[tile][1];
        // Tile's clipPath element.
        var tileClip = document.createElementNS(Blockly.SVG_NS, 'clipPath');
        tileClip.setAttribute('id', 'tileClipPath' + tileId);
        var clipRect = document.createElementNS(Blockly.SVG_NS, 'rect');
        clipRect.setAttribute('width', Maze.SQUARE_SIZE);
        clipRect.setAttribute('height', Maze.SQUARE_SIZE);
  
        clipRect.setAttribute('x', x * Maze.SQUARE_SIZE);
        clipRect.setAttribute('y', y * Maze.SQUARE_SIZE);
  
        tileClip.appendChild(clipRect);
        if (CHROME34) {
          var wrapSvg = Blockly.createSvgElement('svg',
              {'xmlns': 'http://www.w3.org/2000/svg', 'version': '1.1'}, null);
          wrapSvg.appendChild(tileClip);
          svg.appendChild(wrapSvg);
        } else {
          svg.appendChild(tileClip);
        }
        // Tile sprite.
        var tile = document.createElementNS(Blockly.SVG_NS, 'image');
        tile.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
            Maze.SKIN.tiles);
        // Position the tile sprite relative to the clipRect.
        tile.setAttribute('height', Maze.SQUARE_SIZE * 4);
        tile.setAttribute('width', Maze.SQUARE_SIZE * 5);
        tile.setAttribute('clip-path', 'url(#tileClipPath' + tileId + ')');
        tile.setAttribute('x', (x - left) * Maze.SQUARE_SIZE);
        tile.setAttribute('y', (y - top) * Maze.SQUARE_SIZE);
        if (CHROME34) {
          wrapSvg.appendChild(tile);
        } else {
          svg.appendChild(tile);
        }
        tileId++;
      }
    }
  
    // Add finish marker.
    var finishMarker = document.createElementNS(Blockly.SVG_NS, 'image');
    finishMarker.setAttribute('id', 'finish');
    finishMarker.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
        Maze.SKIN.marker);
    finishMarker.setAttribute('height', 34);
    finishMarker.setAttribute('width', 20);
    svg.appendChild(finishMarker);
  
    // Pegman's clipPath element, whose (x, y) is reset by Maze.displayPegman
    var pegmanClip = document.createElementNS(Blockly.SVG_NS, 'clipPath');
    pegmanClip.setAttribute('id', 'pegmanClipPath');
    var clipRect = document.createElementNS(Blockly.SVG_NS, 'rect');
    clipRect.setAttribute('id', 'clipRect');
    clipRect.setAttribute('width', Maze.PEGMAN_WIDTH);
    clipRect.setAttribute('height', Maze.PEGMAN_HEIGHT);
    pegmanClip.appendChild(clipRect);
    if (CHROME34) {
      var wrapSvg = Blockly.createSvgElement('svg',
          {'xmlns': 'http://www.w3.org/2000/svg', 'version': '1.1'}, null);
      wrapSvg.appendChild(pegmanClip);
      svg.appendChild(wrapSvg);
    } else {
      svg.appendChild(pegmanClip);
    }
  
    // Add Pegman.
    var pegmanIcon = document.createElementNS(Blockly.SVG_NS, 'image');
    pegmanIcon.setAttribute('id', 'pegman');
    pegmanIcon.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
        Maze.SKIN.sprite);
    pegmanIcon.setAttribute('height', Maze.PEGMAN_HEIGHT);
    pegmanIcon.setAttribute('width', Maze.PEGMAN_WIDTH * 21); // 49 * 21 = 1029
    pegmanIcon.setAttribute('clip-path', 'url(#pegmanClipPath)');
    if (CHROME34) {
      wrapSvg.appendChild(pegmanIcon);
    } else {
      svg.appendChild(pegmanIcon);
    }
  };
  
  /**
   * Initialize Blockly and the maze.  Called on page load.
   */
  Maze.init = function() {
    BlocklyApps.init();
  
    // Setup the Pegman menu.
    var pegmanImg = document.querySelector('#pegmanButton>img');
    pegmanImg.style.backgroundImage = 'url(' + Maze.SKIN.sprite + ')';
    var pegmanMenu = document.getElementById('pegmanMenu');
    var handlerFactory = function(n) {
      return function() {
        Maze.changePegman(n);
      };
    };
    for (var i = 0; i < Maze.SKINS.length; i++) {
      if (i == Maze.SKIN_ID) {
        continue;
      }
      var div = document.createElement('div');
      var img = document.createElement('img');
      img.src = '../../media/1x1.gif';
      img.style.backgroundImage = 'url(' + Maze.SKINS[i].sprite + ')';
      div.appendChild(img);
      pegmanMenu.appendChild(div);
      Blockly.bindEvent_(div, 'mousedown', null, handlerFactory(i));
    }
    Blockly.bindEvent_(window, 'resize', null, Maze.hidePegmanMenu);
    var pegmanButton = document.getElementById('pegmanButton');
    Blockly.bindEvent_(pegmanButton, 'mousedown', null, Maze.showPegmanMenu);
  
    var rtl = BlocklyApps.isRtl();
    var blocklyDiv = document.getElementById('blockly');
    var visualization = document.getElementById('visualization');
    var onresize = function(e) {
      var top = visualization.offsetTop;
      blocklyDiv.style.top = Math.max(10, top - window.pageYOffset) + 'px';
      blocklyDiv.style.left = rtl ? '10px' : '420px';
      blocklyDiv.style.width = (window.innerWidth - 440) + 'px';
    };
    window.addEventListener('scroll', function() {
        onresize();
        Blockly.fireUiEvent(window, 'resize');
      });
    window.addEventListener('resize', onresize);
    onresize();
  
    var toolbox = document.getElementById('toolbox');
    Blockly.inject(document.getElementById('blockly'),
        {path: '../../',
         maxBlocks: Maze.MAX_BLOCKS,
         rtl: rtl,
         toolbox: toolbox,
         trashcan: true});
    Blockly.loadAudio_(Maze.SKIN.winSound, 'win');
    Blockly.loadAudio_(Maze.SKIN.crashSound, 'fail');
  
    Blockly.JavaScript.INFINITE_LOOP_TRAP = '  BlocklyApps.checkTimeout(%1);\n';
    Maze.drawMap();
  
    var defaultXml =
        '<xml>' +
        '  <block movable="' + (Maze.LEVEL != 1) + '" type="maze_moveForward" ' +
        'x="70" y="70"></block>' +
        '</xml>';
    BlocklyApps.loadBlocks(defaultXml);
  
    // Locate the start and finish squares.
    for (var y = 0; y < Maze.ROWS; y++) {
      for (var x = 0; x < Maze.COLS; x++) {
        if (Maze.map[y][x] == Maze.SquareType.START) {
          Maze.start_ = {x: x, y: y};
        } else if (Maze.map[y][x] == Maze.SquareType.FINISH) {
          Maze.finish_ = {x: x, y: y};
        }
      }
    }
  
    Maze.reset(true);
    Blockly.addChangeListener(function() {Maze.updateCapacity()});
  
    document.body.addEventListener('mousemove', Maze.updatePegSpin_, true);
  
    BlocklyApps.bindClick('runButton', Maze.runButtonClick);
    BlocklyApps.bindClick('resetButton', Maze.resetButtonClick);
  
    if (Maze.LEVEL == 1) {
      // Make connecting blocks easier for beginners.
      Blockly.SNAP_RADIUS *= 2;
    }
    if (Maze.LEVEL == 10) {
      // Level 10 gets an introductory modal dialog.
      var content = document.getElementById('dialogHelpWallFollow');
      var style = {
        width: '30%',
        left: '35%',
        top: '12em'
      };
      BlocklyApps.showDialog(content, null, false, true, style,
          BlocklyApps.stopDialogKeyDown);
      BlocklyApps.startDialogKeyDown();
    } else {
      // All other levels get interactive help.  But wait 5 seconds for the
      // user to think a bit before they are told what to do.
      window.setTimeout(function() {
        Blockly.addChangeListener(function() {Maze.levelHelp()});
        Maze.levelHelp();
      }, 5000);
    }
  
    // Lazy-load the syntax-highlighting.
    window.setTimeout(BlocklyApps.importPrettify, 1);
  };
  
  if (window.location.pathname.match(/readonly.html$/)) {
    window.addEventListener('load', BlocklyApps.initReadonly);
  } else {
    window.addEventListener('load', Maze.init);
  }
  
  /**
   * When the workspace changes, update the help as needed.
   */
  Maze.levelHelp = function() {
    if (Blockly.Block.dragMode_ != 0) {
      // Don't change helps during drags.
      return;
    } else if (Maze.result == Maze.ResultType.SUCCESS) {
      // The user has already won.  They are just playing around.
      return;
    }
    var userBlocks = Blockly.Xml.domToText(
        Blockly.Xml.workspaceToDom(Blockly.mainWorkspace));
    var toolbar = Blockly.mainWorkspace.flyout_.workspace_.getTopBlocks(true);
    var content = null;
    var origin = null;
    var style = null;
    if (Maze.LEVEL == 1) {
      if (Blockly.mainWorkspace.getAllBlocks().length < 2) {
        content = document.getElementById('dialogHelpStack');
        style = {width: '370px', top: '120px'};
        style[Blockly.RTL ? 'right' : 'left'] = '215px';
        origin = toolbar[0].getSvgRoot();
      } else {
        var topBlocks = Blockly.mainWorkspace.getTopBlocks(true)
        if (topBlocks.length > 1) {
          var iframe = document.getElementById('iframeOneTopBlock');
          var xml = '<block type="maze_moveForward" x="10" y="10">' +
              '<next><block type="maze_moveForward"></block></next></block>';
          iframe.src = 'readonly.html' +
              '?lang=' + encodeURIComponent(BlocklyApps.LANG) +
              '&xml=' + encodeURIComponent(xml);
          content = document.getElementById('dialogHelpOneTopBlock');
          style = {width: '360px', top: '120px'};
          style[Blockly.RTL ? 'right' : 'left'] = '225px';
          origin = topBlocks[0].getSvgRoot();
        } else if (Maze.result == Maze.ResultType.UNSET) {
          // Show run help dialog.
          content = document.getElementById('dialogHelpRun');
          style = {width: '360px', top: '410px'};
          style[Blockly.RTL ? 'right' : 'left'] = '400px';
          origin = document.getElementById('runButton');
        }
      }
    } else if (Maze.LEVEL == 2) {
      if (Maze.result != Maze.ResultType.UNSET &&
          document.getElementById('runButton').style.display == 'none') {
        content = document.getElementById('dialogHelpReset');
        style = {width: '360px', top: '410px'};
        style[Blockly.RTL ? 'right' : 'left'] = '400px';
        origin = document.getElementById('resetButton');
      }
    } else if (Maze.LEVEL == 3) {
      if (userBlocks.indexOf('maze_forever') == -1) {
        if (Blockly.mainWorkspace.remainingCapacity() == 0) {
          content = document.getElementById('dialogHelpCapacity');
          style = {width: '430px', top: '310px'};
          style[Blockly.RTL ? 'right' : 'left'] = '50px';
          origin = document.getElementById('capacityBubble');
        } else {
          content = document.getElementById('dialogHelpRepeat');
          style = {width: '360px', top: '320px'};
          style[Blockly.RTL ? 'right' : 'left'] = '425px';
          origin = toolbar[3].getSvgRoot();
        }
      }
    } else if (Maze.LEVEL == 4) {
      if (Blockly.mainWorkspace.remainingCapacity() == 0 &&
          (userBlocks.indexOf('maze_forever') == -1 ||
           Blockly.mainWorkspace.getTopBlocks(false).length > 1)) {
        content = document.getElementById('dialogHelpCapacity');
        style = {width: '430px', top: '310px'};
        style[Blockly.RTL ? 'right' : 'left'] = '50px';
        origin = document.getElementById('capacityBubble');
      } else {
        var showHelp = true;
        // Only show help if there is not a loop with two nested blocks.
        var blocks = Blockly.mainWorkspace.getAllBlocks();
        for (var i = 0; i < blocks.length; i++) {
          var block = blocks[i];
          if (block.type != 'maze_forever') {
            continue;
          }
          var j = 0;
          while (block) {
            var kids = block.getChildren();
            block = kids.length ? kids[0] : null;
            j++;
          }
          if (j > 2) {
            showHelp = false;
            break;
          }
        }
        if (showHelp) {
          content = document.getElementById('dialogHelpRepeatMany');
          style = {width: '360px', top: '320px'};
          style[Blockly.RTL ? 'right' : 'left'] = '425px';
          origin = toolbar[3].getSvgRoot();
        }
      }
    } else if (Maze.LEVEL == 5) {
      if (Maze.SKIN_ID == 0 && !Maze.showPegmanMenu.activatedOnce) {
        content = document.getElementById('dialogHelpSkins');
        style = {width: '360px', top: '60px'};
        style[Blockly.RTL ? 'left' : 'right'] = '20px';
        origin = document.getElementById('pegmanButton');
      }
    } else if (Maze.LEVEL == 6) {
      if (userBlocks.indexOf('maze_if') == -1) {
        content = document.getElementById('dialogHelpIf');
        style = {width: '360px', top: '400px'};
        style[Blockly.RTL ? 'right' : 'left'] = '425px';
        origin = toolbar[4].getSvgRoot();
      }
    } else if (Maze.LEVEL == 7) {
      if (!Maze.levelHelp.initialized7_) {
        // Create fake dropdown.
        var span = document.createElement('span');
        span.className = 'helpMenuFake';
        var options =
            [BlocklyApps.getMsg('Maze_pathAhead'),
             BlocklyApps.getMsg('Maze_pathLeft'),
             BlocklyApps.getMsg('Maze_pathRight')];
        var prefix = Blockly.commonWordPrefix(options);
        var suffix = Blockly.commonWordSuffix(options);
        if (suffix) {
          var option = options[0].slice(prefix, -suffix);
        } else {
          var option = options[0].substring(prefix);
        }
        span.textContent = option + ' \u25BE';
        // Inject fake dropdown into message.
        var container = document.getElementById('helpMenuText');
        var msg = container.textContent;
        container.textContent = '';
        var parts = msg.split(/%\d/);
        for (var i = 0; i < parts.length; i++) {
          container.appendChild(document.createTextNode(parts[i]));
          if (i != parts.length - 1) {
            container.appendChild(span.cloneNode(true));
          }
        }
        Maze.levelHelp.initialized7_ = true;
      }
      // The hint says to change from 'ahead', but keep the hint visible
      // until the user chooses 'right'.
      if (userBlocks.indexOf('isPathRight') == -1) {
        content = document.getElementById('dialogHelpMenu');
        style = {width: '360px', top: '400px'};
        style[Blockly.RTL ? 'right' : 'left'] = '425px';
        origin = toolbar[4].getSvgRoot();
      }
    } else if (Maze.LEVEL == 9) {
      if (userBlocks.indexOf('maze_ifElse') == -1) {
        content = document.getElementById('dialogHelpIfElse');
        style = {width: '360px', top: '305px'};
        style[Blockly.RTL ? 'right' : 'left'] = '425px';
        origin = toolbar[5].getSvgRoot();
      }
    }
    if (content) {
      if (content.parentNode != document.getElementById('dialog')) {
        BlocklyApps.showDialog(content, origin, true, false, style, null);
      }
    } else {
      BlocklyApps.hideDialog(false);
    }
  };
  
  /**
   * Reload with a different Pegman skin.
   * @param {number} newSkin ID of new skin.
   */
  Maze.changePegman = function(newSkin) {
    Maze.saveToStorage();
    window.location = window.location.protocol + '//' +
        window.location.host + window.location.pathname +
        '?lang=' + BlocklyApps.LANG + '&level=' + Maze.LEVEL + '&skin=' + newSkin;
  };
  
  /**
   * Save the blocks for a one-time reload.
   */
  Maze.saveToStorage = function() {
    // MSIE 11 does not support sessionStorage on file:// URLs.
    if (window.sessionStorage) {
      var xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace);
      var text = Blockly.Xml.domToText(xml);
      window.sessionStorage.loadOnceBlocks = text;
    }
  };
  
  /**
   * Display the Pegman skin-change menu.
   */
  Maze.showPegmanMenu = function() {
    var menu = document.getElementById('pegmanMenu');
    if (menu.style.display == 'block') {
      return;  // Menu is already open.
    }
    var button = document.getElementById('pegmanButton');
    Blockly.addClass_(button, 'buttonHover');
    menu.style.top = (button.offsetTop + button.offsetHeight) + 'px';
    menu.style.left = button.offsetLeft + 'px';
    menu.style.display = 'block';
    window.setTimeout(function() {
        Maze.pegmanMenuMouse_ = Blockly.bindEvent_(document.body, 'mousedown',
                                                   null, Maze.hidePegmanMenu);
        }, BlocklyApps.DOUBLE_CLICK_TIME);
  
    // Close the skin-changing hint if open.
    if (document.getElementById('dialogHelpSkins').className !=
        'dialogHiddenContent') {
      BlocklyApps.hideDialog(false);
    }
    Maze.showPegmanMenu.activatedOnce = true;
  };
  
  /**
   * Hide the Pegman skin-change menu.
   */
  Maze.hidePegmanMenu = function() {
    document.getElementById('pegmanMenu').style.display = 'none';
    Blockly.removeClass_(document.getElementById('pegmanButton'), 'buttonHover');
    if (Maze.pegmanMenuMouse_) {
      Blockly.unbindEvent_(Maze.pegmanMenuMouse_);
      delete Maze.pegmanMenuMouse_;
    }
  };
  
  /**
   * Reset the maze to the start position and kill any pending animation tasks.
   * @param {boolean} first True if an opening animation is to be played.
   */
  Maze.reset = function(first) {
    // Kill all tasks.
    for (var x = 0; x < Maze.pidList.length; x++) {
      window.clearTimeout(Maze.pidList[x]);
    }
    Maze.pidList = [];
  
    // Move Pegman into position.
    Maze.pegmanX = Maze.start_.x;
    Maze.pegmanY = Maze.start_.y;
  
    if (first) {
      Maze.pegmanD = Maze.startDirection + 1;
      Maze.scheduleFinish(false);
      Maze.pidList.push(window.setTimeout(function() {
        Maze.stepSpeed = 100;
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4 - 4]);
        Maze.pegmanD++;
      }, Maze.stepSpeed * 5));
    } else {
      Maze.pegmanD = Maze.startDirection;
      Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4);
    }
  
    // Move the finish icon into position.
    var finishIcon = document.getElementById('finish');
    finishIcon.setAttribute('x', Maze.SQUARE_SIZE * (Maze.finish_.x + 0.5) -
        finishIcon.getAttribute('width') / 2);
    finishIcon.setAttribute('y', Maze.SQUARE_SIZE * (Maze.finish_.y + 0.6) -
        finishIcon.getAttribute('height'));
  
    // Make 'look' icon invisible and promote to top.
    var lookIcon = document.getElementById('look');
    lookIcon.style.display = 'none';
    lookIcon.parentNode.appendChild(lookIcon);
    var paths = lookIcon.getElementsByTagName('path');
    for (var i = 0, path; path = paths[i]; i++) {
      path.setAttribute('stroke', Maze.SKIN.look);
    }
  };
  
  /**
   * Click the run button.  Start the program.
   */
  Maze.runButtonClick = function() {
    BlocklyApps.hideDialog(false);
    // Only allow a single top block on level 1.
    if (Maze.LEVEL == 1 && Blockly.mainWorkspace.getTopBlocks().length > 1) {
      return;
    }
    var runButton = document.getElementById('runButton');
    var resetButton = document.getElementById('resetButton');
    // Ensure that Reset button is at least as wide as Run button.
    if (!resetButton.style.minWidth) {
      resetButton.style.minWidth = runButton.offsetWidth + 'px';
    }
    runButton.style.display = 'none';
    resetButton.style.display = 'inline';
    // Prevent double-clicks or double-taps.
    resetButton.disabled = true;
    setTimeout(function() {resetButton.disabled = false;},
               BlocklyApps.DOUBLE_CLICK_TIME);
  
    Blockly.mainWorkspace.traceOn(true);
    Maze.reset(false);
    Maze.execute();
  };
  
  /**
   * Updates the document's 'capacity' element with a message
   * indicating how many more blocks are permitted.  The capacity
   * is retrieved from Blockly.mainWorkspace.remainingCapacity().
   */
  Maze.updateCapacity = function() {
    var cap = Blockly.mainWorkspace.remainingCapacity();
    var p = document.getElementById('capacity');
    if (cap == Infinity) {
      p.style.display = 'none';
    } else {
      p.style.display = 'inline';
      p.innerHTML = '';
      cap = Number(cap);
      var capSpan = document.createElement('span');
      capSpan.className = 'capacityNumber';
      capSpan.appendChild(document.createTextNode(cap));
      if (cap == 0) {
        var msg = BlocklyApps.getMsg('Maze_capacity0');
      } else if (cap == 1) {
        var msg = BlocklyApps.getMsg('Maze_capacity1');
      } else {
        var msg = BlocklyApps.getMsg('Maze_capacity2');
      }
      var parts = msg.split(/%\d/);
      for (var i = 0; i < parts.length; i++) {
        p.appendChild(document.createTextNode(parts[i]));
        if (i != parts.length - 1) {
          p.appendChild(capSpan.cloneNode(true));
        }
      }
    }
  };
  
  /**
   * Click the reset button.  Reset the maze.
   */
  Maze.resetButtonClick = function() {
    var runButton = document.getElementById('runButton');
    runButton.style.display = 'inline';
    document.getElementById('resetButton').style.display = 'none';
    // Prevent double-clicks or double-taps.
    runButton.disabled = true;
    setTimeout(function() {runButton.disabled = false;},
               BlocklyApps.DOUBLE_CLICK_TIME);
  
    Blockly.mainWorkspace.traceOn(false);
    Maze.reset(false);
    Maze.levelHelp();
  };
  
  /**
   * Execute the user's code.  Heaven help us...
   */
  Maze.execute = function() {
    BlocklyApps.log = [];
    BlocklyApps.ticks = 1000;
    var code = Blockly.JavaScript.workspaceToCode();
    Maze.result = Maze.ResultType.UNSET;
  
    // Try running the user's code.  There are four possible outcomes:
    // 1. If pegman reaches the finish [SUCCESS], true is thrown.
    // 2. If the program is terminated due to running too long [TIMEOUT],
    //    false is thrown.
    // 3. If another error occurs [ERROR], that error is thrown.
    // 4. If the program ended normally but without solving the maze [FAILURE],
    //    no error or exception is thrown.
    try {
      eval(code);
      Maze.result = Maze.ResultType.FAILURE;
    } catch (e) {
      // A boolean is thrown for normal termination.
      // Abnormal termination is a user error.
      if (e === Infinity) {
        Maze.result = Maze.ResultType.TIMEOUT;
      } else if (e === true) {
        Maze.result = Maze.ResultType.SUCCESS;
      } else if (e === false) {
        Maze.result = Maze.ResultType.ERROR;
      } else {
        // Syntax error, can't happen.
        Maze.result = Maze.ResultType.ERROR;
        window.alert(e);
      }
    }
  
    // Fast animation if execution is successful.  Slow otherwise.
    Maze.stepSpeed = (Maze.result == Maze.ResultType.SUCCESS) ? 100 : 150;
  
    // BlocklyApps.log now contains a transcript of all the user's actions.
    // Reset the maze and animate the transcript.
    Maze.reset(false);
    Maze.pidList.push(window.setTimeout(Maze.animate, 100));
  };
  
  /**
   * Iterate through the recorded path and animate pegman's actions.
   */
  Maze.animate = function() {
    var action = BlocklyApps.log.shift();
    if (!action) {
      BlocklyApps.highlight(null);
      Maze.levelHelp();
      return;
    }
    BlocklyApps.highlight(action[1]);
  
    switch (action[0]) {
      case 'north':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX, Maze.pegmanY - 1, Maze.pegmanD * 4]);
        Maze.pegmanY--;
        break;
      case 'east':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX + 1, Maze.pegmanY, Maze.pegmanD * 4]);
        Maze.pegmanX++;
        break;
      case 'south':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX, Maze.pegmanY + 1, Maze.pegmanD * 4]);
        Maze.pegmanY++;
        break;
      case 'west':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX - 1, Maze.pegmanY, Maze.pegmanD * 4]);
        Maze.pegmanX--;
        break;
      case 'look_north':
        Maze.scheduleLook(Maze.DirectionType.NORTH);
        break;
      case 'look_east':
        Maze.scheduleLook(Maze.DirectionType.EAST);
        break;
      case 'look_south':
        Maze.scheduleLook(Maze.DirectionType.SOUTH);
        break;
      case 'look_west':
        Maze.scheduleLook(Maze.DirectionType.WEST);
        break;
      case 'fail_forward':
        Maze.scheduleFail(true);
        break;
      case 'fail_backward':
        Maze.scheduleFail(false);
        break;
      case 'left':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4 - 4]);
        Maze.pegmanD = Maze.constrainDirection4(Maze.pegmanD - 1);
        break;
      case 'right':
        Maze.schedule([Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4],
                      [Maze.pegmanX, Maze.pegmanY, Maze.pegmanD * 4 + 4]);
        Maze.pegmanD = Maze.constrainDirection4(Maze.pegmanD + 1);
        break;
      case 'finish':
        Maze.scheduleFinish(true);
        window.setTimeout(Maze.congratulations, 1000);
    }
  
    Maze.pidList.push(window.setTimeout(Maze.animate, Maze.stepSpeed * 5));
  };
  
  /**
   * Congratulates the user for completing the level and offers to
   * direct them to the next level, if available.
   */
  Maze.congratulations = function() {
    var content = document.getElementById('dialogDone');
    var buttonDiv = document.getElementById('dialogDoneButtons');
    buttonDiv.textContent = '';
    var style = {
      width: '40%',
      left: '30%',
      top: '5em'
    };
    if (Maze.LEVEL < Maze.MAX_LEVEL) {
      var text = BlocklyApps.getMsg('Maze_nextLevel')
          .replace('%1', Maze.LEVEL + 1);
      var cancel = document.createElement('button');
      cancel.appendChild(
          document.createTextNode(BlocklyApps.getMsg('dialogCancel')));
      cancel.addEventListener('click', BlocklyApps.hideDialog, true);
      cancel.addEventListener('touchend', BlocklyApps.hideDialog, true);
      buttonDiv.appendChild(cancel);
  
      var ok = document.createElement('button');
      ok.className = 'secondary';
      ok.appendChild(document.createTextNode(BlocklyApps.getMsg('dialogOk')));
      ok.addEventListener('click', Maze.nextLevel, true);
      ok.addEventListener('touchend', Maze.nextLevel, true);
      buttonDiv.appendChild(ok);
  
      BlocklyApps.showDialog(content, null, false, true, style,
          function() {
            document.body.removeEventListener('keydown',
                Maze.congratulationsKeyDown_, true);
            });
      document.body.addEventListener('keydown',
          Maze.congratulationsKeyDown_, true);
  
    } else {
      var text = BlocklyApps.getMsg('Maze_finalLevel');
      var ok = document.createElement('button');
      ok.className = 'secondary';
      ok.addEventListener('click', BlocklyApps.hideDialog, true);
      ok.addEventListener('touchend', BlocklyApps.hideDialog, true);
      ok.appendChild(document.createTextNode(BlocklyApps.getMsg('dialogOk')));
      buttonDiv.appendChild(ok);
      BlocklyApps.showDialog(content, null, false, true, style,
          BlocklyApps.stopDialogKeyDown);
      BlocklyApps.startDialogKeyDown();
    }
    document.getElementById('dialogDoneText').textContent = text;
  
    var pegSpin = document.getElementById('pegSpin');
    pegSpin.style.backgroundImage = 'url(' + Maze.SKIN.sprite + ')';
  };
  
  /**
   * If the user preses enter, escape, or space, hide the dialog.
   * Enter and space move to the next level, escape does not.
   * @param {!Event} e Keyboard event.
   * @private
   */
  Maze.congratulationsKeyDown_ = function(e) {
    if (e.keyCode == 13 ||
        e.keyCode == 27 ||
        e.keyCode == 32) {
      BlocklyApps.hideDialog(true);
      e.stopPropagation();
      e.preventDefault();
      if (e.keyCode != 27) {
        Maze.nextLevel();
      }
    }
  };
  
  /**
   * Go to the next level.
   */
  Maze.nextLevel = function() {
    window.location = window.location.protocol + '//' +
        window.location.host + window.location.pathname +
        '?lang=' + BlocklyApps.LANG + '&level=' + (Maze.LEVEL + 1) +
        '&skin=' + Maze.SKIN_ID;
  };
  
  /**
   * Point the congratulations Pegman to face the mouse.
   * @param {Event} e Mouse move event.
   * @private
   */
  Maze.updatePegSpin_ = function(e) {
    if (document.getElementById('dialogDone').className ==
        'dialogHiddenContent') {
      return;
    }
    var pegSpin = document.getElementById('pegSpin');
    var bBox = BlocklyApps.getBBox_(pegSpin);
    var x = bBox.x + bBox.width / 2 - window.pageXOffset;
    var y = bBox.y + bBox.height / 2 - window.pageYOffset;
    var dx = e.clientX - x;
    var dy = e.clientY - y;
    var angle = Math.atan(dy / dx);
    // Convert from radians to degrees because I suck at math.
    angle = angle / Math.PI * 180;
    // 0: North, 90: East, 180: South, 270: West.
    if (dx > 0) {
      angle += 90;
    } else {
      angle += 270;
    }
    // Divide into 16 quads.
    var quad = Math.round(angle / 360 * 16);
    if (quad == 16) {
      quad = 15;
    }
    // Display correct Pegman sprite.
    pegSpin.style.backgroundPosition = (-quad * Maze.PEGMAN_WIDTH) + 'px 0px';
  };
  
  /**
   * Schedule the animations for a move or turn.
   * @param {!Array.<number>} startPos X, Y and direction starting points.
   * @param {!Array.<number>} endPos X, Y and direction ending points.
   */
  Maze.schedule = function(startPos, endPos) {
    var deltas = [(endPos[0] - startPos[0]) / 4,
                  (endPos[1] - startPos[1]) / 4,
                  (endPos[2] - startPos[2]) / 4];
    Maze.displayPegman(startPos[0] + deltas[0],
                       startPos[1] + deltas[1],
                       Maze.constrainDirection16(startPos[2] + deltas[2]));
    Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(startPos[0] + deltas[0] * 2,
            startPos[1] + deltas[1] * 2,
            Maze.constrainDirection16(startPos[2] + deltas[2] * 2));
      }, Maze.stepSpeed));
    Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(startPos[0] + deltas[0] * 3,
            startPos[1] + deltas[1] * 3,
            Maze.constrainDirection16(startPos[2] + deltas[2] * 3));
      }, Maze.stepSpeed * 2));
    Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(endPos[0], endPos[1],
            Maze.constrainDirection16(endPos[2]));
      }, Maze.stepSpeed * 3));
  };
  
  /**
   * Schedule the animations and sounds for a failed move.
   * @param {boolean} forward True if forward, false if backward.
   */
  Maze.scheduleFail = function(forward) {
    var deltaX = 0;
    var deltaY = 0;
    switch (Maze.pegmanD) {
      case Maze.DirectionType.NORTH:
        deltaY = -1;
        break;
      case Maze.DirectionType.EAST:
        deltaX = 1;
        break;
      case Maze.DirectionType.SOUTH:
        deltaY = 1;
        break;
      case Maze.DirectionType.WEST:
        deltaX = -1;
        break;
    }
    if (!forward) {
      deltaX = -deltaX;
      deltaY = -deltaY;
    }
    if (Maze.SKIN.crashType == Maze.CRASH_STOP) {
      // Bounce bounce.
      deltaX /= 4;
      deltaY /= 4;
      var direction16 = Maze.constrainDirection16(Maze.pegmanD * 4);
      Maze.displayPegman(Maze.pegmanX + deltaX,
                         Maze.pegmanY + deltaY,
                         direction16);
      Blockly.playAudio('fail', 0.5);
      Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(Maze.pegmanX,
                           Maze.pegmanY,
                           direction16);
        }, Maze.stepSpeed));
      Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(Maze.pegmanX + deltaX,
                           Maze.pegmanY + deltaY,
                           direction16);
        Blockly.playAudio('fail', 0.5);
      }, Maze.stepSpeed * 2));
      Maze.pidList.push(window.setTimeout(function() {
          Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, direction16);
        }, Maze.stepSpeed * 3));
    } else {
      // Add a small random delta away from the grid.
      var deltaZ = (Math.random() - 0.5) * 10;
      var deltaD = (Math.random() - 0.5) / 2;
      deltaX += (Math.random() - 0.5) / 4;
      deltaY += (Math.random() - 0.5) / 4;
      deltaX /= 8;
      deltaY /= 8;
      var acceleration = 0;
      if (Maze.SKIN.crashType == Maze.CRASH_FALL) {
        acceleration = 0.01;
      }
      Maze.pidList.push(window.setTimeout(function() {
        Blockly.playAudio('fail', 0.5);
      }, Maze.stepSpeed * 2));
      var setPosition = function(n) {
        return function() {
          var direction16 = Maze.constrainDirection16(Maze.pegmanD * 4 +
                                                      deltaD * n);
          Maze.displayPegman(Maze.pegmanX + deltaX * n,
                             Maze.pegmanY + deltaY * n,
                             direction16,
                             deltaZ * n);
          deltaY += acceleration;
        };
      };
      // 100 frames should get Pegman offscreen.
      for (var i = 1; i < 100; i++) {
        Maze.pidList.push(window.setTimeout(setPosition(i),
            Maze.stepSpeed * i / 2));
      }
    }
  };
  
  /**
   * Schedule the animations and sound for a victory dance.
   * @param {boolean} sound Play the victory sound.
   */
  Maze.scheduleFinish = function(sound) {
    var direction16 = Maze.constrainDirection16(Maze.pegmanD * 4);
    Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, 16);
    if (sound) {
      Blockly.playAudio('win', 0.5);
    }
    Maze.stepSpeed = 150;  // Slow down victory animation a bit.
    Maze.pidList.push(window.setTimeout(function() {
      Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, 18);
      }, Maze.stepSpeed));
    Maze.pidList.push(window.setTimeout(function() {
      Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, 16);
      }, Maze.stepSpeed * 2));
    Maze.pidList.push(window.setTimeout(function() {
        Maze.displayPegman(Maze.pegmanX, Maze.pegmanY, direction16);
      }, Maze.stepSpeed * 3));
  };
  
  /**
   * Display Pegman at the specified location, facing the specified direction.
   * @param {number} x Horizontal grid (or fraction thereof).
   * @param {number} y Vertical grid (or fraction thereof).
   * @param {number} d Direction (0 - 15) or dance (16 - 17).
   * @param {number} opt_angle Optional angle (in degrees) to rotate Pegman.
   */
  Maze.displayPegman = function(x, y, d, opt_angle) {
    var pegmanIcon = document.getElementById('pegman');
    pegmanIcon.setAttribute('x',
        x * Maze.SQUARE_SIZE - d * Maze.PEGMAN_WIDTH + 1);
    pegmanIcon.setAttribute('y',
        Maze.SQUARE_SIZE * (y + 0.5) - Maze.PEGMAN_HEIGHT / 2 - 8);
    if (opt_angle) {
      pegmanIcon.setAttribute('transform', 'rotate(' + opt_angle + ', ' +
          (x * Maze.SQUARE_SIZE + Maze.SQUARE_SIZE / 2) + ', ' +
          (y * Maze.SQUARE_SIZE + Maze.SQUARE_SIZE / 2) + ')');
    } else {
      pegmanIcon.setAttribute('transform', 'rotate(0, 0, 0)');
    }
  
    var clipRect = document.getElementById('clipRect');
    clipRect.setAttribute('x', x * Maze.SQUARE_SIZE + 1);
    clipRect.setAttribute('y', pegmanIcon.getAttribute('y'));
  };
  
  /**
   * Display the look icon at Pegman's current location,
   * in the specified direction.
   * @param {!Maze.DirectionType} d Direction (0 - 3).
   */
  Maze.scheduleLook = function(d) {
    var x = Maze.pegmanX;
    var y = Maze.pegmanY;
    switch (d) {
      case Maze.DirectionType.NORTH:
        x += 0.5;
        break;
      case Maze.DirectionType.EAST:
        x += 1;
        y += 0.5;
        break;
      case Maze.DirectionType.SOUTH:
        x += 0.5;
        y += 1;
        break;
      case Maze.DirectionType.WEST:
        y += 0.5;
        break;
    }
    x *= Maze.SQUARE_SIZE;
    y *= Maze.SQUARE_SIZE;
    d = d * 90 - 45;
  
    var lookIcon = document.getElementById('look');
    lookIcon.setAttribute('transform',
        'translate(' + x + ', ' + y + ') ' +
        'rotate(' + d + ' 0 0) scale(.4)');
    var paths = lookIcon.getElementsByTagName('path');
    lookIcon.style.display = 'inline';
    for (var x = 0, path; path = paths[x]; x++) {
      Maze.scheduleLookStep(path, Maze.stepSpeed * x);
    }
  };
  
  /**
   * Schedule one of the 'look' icon's waves to appear, then disappear.
   * @param {!Element} path Element to make appear.
   * @param {number} delay Milliseconds to wait before making wave appear.
   */
  Maze.scheduleLookStep = function(path, delay) {
    Maze.pidList.push(window.setTimeout(function() {
      path.style.display = 'inline';
      window.setTimeout(function() {
        path.style.display = 'none';
      }, Maze.stepSpeed * 2);
    }, delay));
  };
  
  /**
   * Keep the direction within 0-3, wrapping at both ends.
   * @param {number} d Potentially out-of-bounds direction value.
   * @return {number} Legal direction value.
   */
  Maze.constrainDirection4 = function(d) {
    d = Math.round(d) % 4;
    if (d < 0) {
      d += 4;
    }
    return d;
  };
  
  /**
   * Keep the direction within 0-15, wrapping at both ends.
   * @param {number} d Potentially out-of-bounds direction value.
   * @return {number} Legal direction value.
   */
  Maze.constrainDirection16 = function(d) {
    d = Math.round(d) % 16;
    if (d < 0) {
      d += 16;
    }
    return d;
  };
  
  // API
  // Human-readable aliases.
  
  Maze.moveForward = function(id) {
    Maze.move(0, id);
  };
  
  Maze.moveBackward = function(id) {
    Maze.move(2, id);
  };
  
  Maze.turnLeft = function(id) {
    Maze.turn(0, id);
  };
  
  Maze.turnRight = function(id) {
    Maze.turn(1, id);
  };
  
  Maze.isPathForward = function(id) {
    return Maze.isPath(0, id);
  };
  
  Maze.isPathRight = function(id) {
    return Maze.isPath(1, id);
  };
  
  Maze.isPathBackward = function(id) {
    return Maze.isPath(2, id);
  };
  
  Maze.isPathLeft = function(id) {
    return Maze.isPath(3, id);
  };
  
  // Core functions.
  
  /**
   * Attempt to move pegman forward or backward.
   * @param {number} direction Direction to move (0 = forward, 2 = backward).
   * @param {string} id ID of block that triggered this action.
   * @throws {true} If the end of the maze is reached.
   * @throws {false} If Pegman collides with a wall.
   */
  Maze.move = function(direction, id) {
    if (!Maze.isPath(direction, null)) {
      BlocklyApps.log.push(['fail_' + (direction ? 'backward' : 'forward'), id]);
      throw false;
    }
    // If moving backward, flip the effective direction.
    var effectiveDirection = Maze.pegmanD + direction;
    var command;
    switch (Maze.constrainDirection4(effectiveDirection)) {
      case Maze.DirectionType.NORTH:
        Maze.pegmanY--;
        command = 'north';
        break;
      case Maze.DirectionType.EAST:
        Maze.pegmanX++;
        command = 'east';
        break;
      case Maze.DirectionType.SOUTH:
        Maze.pegmanY++;
        command = 'south';
        break;
      case Maze.DirectionType.WEST:
        Maze.pegmanX--;
        command = 'west';
        break;
    }
    BlocklyApps.log.push([command, id]);
    if (Maze.pegmanX == Maze.finish_.x && Maze.pegmanY == Maze.finish_.y) {
      // Finished.  Terminate the user's program.
      BlocklyApps.log.push(['finish', null]);
      throw true;
    }
  };
  
  /**
   * Turn pegman left or right.
   * @param {number} direction Direction to turn (0 = left, 1 = right).
   * @param {string} id ID of block that triggered this action.
   */
  Maze.turn = function(direction, id) {
    if (direction) {
      // Right turn (clockwise).
      Maze.pegmanD++;
      BlocklyApps.log.push(['right', id]);
    } else {
      // Left turn (counterclockwise).
      Maze.pegmanD--;
      BlocklyApps.log.push(['left', id]);
    }
    Maze.pegmanD = Maze.constrainDirection4(Maze.pegmanD);
  };
  
  /**
   * Is there a path next to pegman?
   * @param {number} direction Direction to look
   *     (0 = forward, 1 = right, 2 = backward, 3 = left).
   * @param {?string} id ID of block that triggered this action.
   *     Null if called as a helper function in Maze.move().
   * @return {boolean} True if there is a path.
   */
  Maze.isPath = function(direction, id) {
    var effectiveDirection = Maze.pegmanD + direction;
    var square;
    var command;
    switch (Maze.constrainDirection4(effectiveDirection)) {
      case Maze.DirectionType.NORTH:
        square = Maze.map[Maze.pegmanY - 1] &&
            Maze.map[Maze.pegmanY - 1][Maze.pegmanX];
        command = 'look_north';
        break;
      case Maze.DirectionType.EAST:
        square = Maze.map[Maze.pegmanY][Maze.pegmanX + 1];
        command = 'look_east';
        break;
      case Maze.DirectionType.SOUTH:
        square = Maze.map[Maze.pegmanY + 1] &&
            Maze.map[Maze.pegmanY + 1][Maze.pegmanX];
        command = 'look_south';
        break;
      case Maze.DirectionType.WEST:
        square = Maze.map[Maze.pegmanY][Maze.pegmanX - 1];
        command = 'look_west';
        break;
    }
    if (id) {
      BlocklyApps.log.push([command, id]);
    }
    return square !== Maze.SquareType.WALL && square !== undefined;
  };
  