mirror of
https://github.com/google/blockly.git
synced 2026-01-08 17:40:09 +01:00
1516 lines
48 KiB
JavaScript
1516 lines
48 KiB
JavaScript
/**
|
|
* Blockly Apps: Maze
|
|
*
|
|
* Copyright 2012 Google Inc.
|
|
* http://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 = {};
|
|
|
|
// Supported languages.
|
|
BlocklyApps.LANGUAGES = ['ar', 'br', 'ca', 'cs', 'da', 'de', 'el', 'en',
|
|
'es', 'eu', 'fa', 'fr', 'gl', 'hu', 'ia', 'is', 'it',
|
|
'ja', 'ko', 'lv', 'mk', 'ms', 'nl', 'pl', 'pms',
|
|
'pt-br', 'ro', 'ru', 'sk', '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);
|
|
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);
|
|
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);
|
|
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)');
|
|
svg.appendChild(pegmanIcon);
|
|
};
|
|
|
|
/**
|
|
* Initialize Blockly and the maze. Called on page load.
|
|
*/
|
|
Maze.init = function() {
|
|
// Measure the height of arrow characters.
|
|
// Firefox on Vista creates enormously high arrows (80px) for no reason.
|
|
// TODO: Detect if arrow is printed, or Unicode square is printed.
|
|
var textElement = document.getElementById('arrowTest');
|
|
var height = textElement.getBBox().height;
|
|
Maze.addArrows = height < Blockly.BlockSvg.MIN_BLOCK_Y;
|
|
var svg = textElement.ownerSVGElement
|
|
svg.parentNode.removeChild(svg);
|
|
|
|
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');
|
|
pegmanButton.addEventListener('mousedown', Maze.showPegmanMenu, true);
|
|
pegmanButton.addEventListener('touchstart', Maze.showPegmanMenu, true);
|
|
|
|
var rtl = BlocklyApps.isRtl();
|
|
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 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.scrollY) + '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();
|
|
Blockly.fireUiEvent(window, 'resize');
|
|
|
|
var defaultXml =
|
|
'<xml>' +
|
|
' <block 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;
|
|
}
|
|
if (userBlocks.indexOf('maze_if') == -1 ||
|
|
userBlocks.indexOf('isPathForward') != -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() {
|
|
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);
|
|
}, 0);
|
|
// 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';
|
|
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() {
|
|
document.getElementById('runButton').style.display = 'inline';
|
|
document.getElementById('resetButton').style.display = 'none';
|
|
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.scrollX;
|
|
var y = bBox.y + bBox.height / 2 - window.scrollY;
|
|
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;
|
|
};
|