blockly/apps/maze/maze.js
/**
* Blockly Apps: Maze
*
* Copyright 2012 Google Inc.
* https://blockly.googlecode.com/
*
* 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;
};