├── .gitignore
├── .gitmodules
├── LICENSE
├── README.md
├── css
└── main.css
├── images
├── finish.png
└── player.png
├── index.html
├── js
├── config.js
├── main.js
├── model.js
├── stateMachine.js
└── view.js
└── notes.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "js/lib"]
2 | path = js/lib
3 | url = git@github.com:codebox/maze.js.git
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rob Dawson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Maze Generator
2 |
3 | Here is an online [maze generator](https://codebox.net/pages/maze-generator/online) that can create mazes using square,
4 | triangular, hexagonal or circular grids:
5 |
6 |
7 |
8 | As well as creating mazes the generator has many other features, for example it can render a 'distance map',
9 | colouring each location in the grid according how far away it is from a selected point:
10 |
11 |
12 |
13 | The generator offers a choice of 10 different algorithms, which each produce mazes with different characteristics.
14 | All mazes created by these algorithms are 'perfect' mazes, i.e. there is exactly one path connecting any pair of
15 | locations within the grid, and therefore one unique solution to each maze.
16 |
17 | If you want to try solving one of the mazes yourself then you can! The generator lets you navigate through the maze
18 | using mouse/keyboard controls, and can automatically move you forward to the next junction in the maze to save you
19 | time. Once you finish a maze your time is displayed, together with an 'optimality score' showing how close your
20 | solution was to the optimal one. Of course, you can also give up at any point and see where you should have gone:
21 |
22 |
23 |
24 | The generator can either create mazes instantly, or slow down the process so that you can watch the algorithms at work.
25 | Some algorithms work using a process of trial and error, and can take a long time to finish, whereas others are guaranteed
26 | to complete quickly:
27 |
28 |
29 |
30 |
31 |
32 | Sorry, your browser doesn't support embedded videos.
33 |
34 |
35 | By creating a mask you can remove cells from the default grids to create interesting shapes:
36 |
37 |
38 |
39 | Normally the generator creates a completely unique random maze each time you use it, however if you want to play around with a
40 | particular maze without changing the layout then just take a note of the 'Seed Value' that is displayed alongside it.
41 | Entering this value into the 'Seed' input field will make sure you get the same pattern again when you click the 'New Maze' button.
42 |
43 | If you make a maze that you would like to keep you can download your creation as an SVG file,
44 | either with or without the solution displayed.
45 |
46 | Many thanks to Jamis Buck for his excellent book [Mazes for Programmers](http://mazesforprogrammers.com) which taught me
47 | everything I needed to know to make this.
48 |
49 | ## Running Locally
50 | You can [try out the maze generator online here](https://codebox.net/pages/maze-generator/online).
51 | If you want to run your own copy then:
52 | * Clone the repository, including the 'mazejs' submodule:
53 |
54 | `git clone --recurse-submodules git@github.com:codebox/mazes.git`
55 |
56 | * Go into the 'mazes' directory:
57 |
58 | `cd mazes`
59 |
60 | * Start a web server at this location:
61 |
62 | `python3 -m http.server`
63 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-colour: #444;
3 | --border-colour: #222;
4 | --alt-colour: #006BB7;
5 | --panel-bg-colour: #EEE;
6 | --small-font-size: 0.8em;
7 | --wide-margin: 10px;
8 | --narrow-margin: 5px;
9 | }
10 | * {
11 | box-sizing: border-box
12 | }
13 | html, body {
14 | width: 100%;
15 | height: 100%;
16 | margin: 0;
17 | font-family: sans-serif;
18 | color: var(--font-colour);
19 | }
20 | #container {
21 | display: flex;
22 | flex-direction: row;
23 | height: 100%;
24 | padding: var(--wide-margin);
25 | }
26 | #mobileTitle {
27 | display: none;
28 | }
29 | #mazeContainer {
30 | flex: 1 1 auto;
31 | min-width: 0;
32 | }
33 | #sidebarContainer {
34 | flex: 0 0 220px;
35 | padding: var(--narrow-margin);
36 | border: 1px solid var(--border-colour);
37 | overflow-y: auto;
38 | user-select: none;
39 | }
40 | #info{
41 | user-select: text;
42 | }
43 | #sidebarContainer li.selected, p.selected {
44 | background-color: var(--alt-colour);
45 | color: white;
46 | }
47 | #sidebarContainer div, .title {
48 | border: 1px solid var(--border-colour);
49 | background-color: var(--panel-bg-colour);
50 | border-radius: 3px;
51 | }
52 | .title {
53 | text-align: center;
54 | padding: 15px 0;
55 | margin-bottom: var(--wide-margin);
56 | }
57 | h1 {
58 | margin: 0;
59 | font-weight:normal;
60 | font-size: 1.5em;
61 | }
62 | .title a {
63 | font-size: var(--small-font-size);
64 | }
65 | .title a, .title a:visited {
66 | color: var(--alt-colour);
67 | }
68 | button, li, .selectable {
69 | cursor: pointer;
70 | }
71 | li, #applyMask p {
72 | padding: 2px 5px;
73 | }
74 | #sidebarContainer ul {
75 | border: 1px solid var(--border-colour);
76 | padding: var(--narrow-margin) 0;
77 | list-style-type: none;
78 | font-size: var(--small-font-size);
79 | background-color: var(--panel-bg-colour);
80 | border-radius: 3px;
81 | margin: var(--narrow-margin) 0;
82 | }
83 | #applyMask {
84 | padding: var(--narrow-margin) 0;
85 | font-size: var(--small-font-size);
86 | margin: var(--narrow-margin) 0;
87 | }
88 | #applyMask p {
89 | margin: 0;
90 | }
91 | button {
92 | display: block;
93 | width: 100%;
94 | background-color: var(--panel-bg-colour);
95 | margin: 5px 0;
96 | border: 1px solid var(--border-colour);
97 | border-radius: 3px;
98 | padding: var(--narrow-margin) 0;
99 | font-size: 1em;
100 | }
101 | #go {
102 | font-weight: bold;
103 | }
104 | #maskNotSupported, #info {
105 | text-align: center;
106 | }
107 | div#info, div#details {
108 | margin-top: var(--wide-margin);
109 | background-color: white;
110 | padding: var(--wide-margin);
111 | }
112 | div#details em {
113 | font-style: normal;
114 | color: var(--alt-colour);
115 | }
116 | ul label {
117 | display: inline-block;
118 | width: 50px;
119 | }
120 | #shapeSelector li, #sizeParameters label {
121 | text-transform: capitalize;
122 | }
123 | #seedInput {
124 | width: 100px;
125 | }
126 | #play, #changeParams {
127 | margin-top: var(--wide-margin);
128 | }
129 | @media screen and (max-width: 500px) {
130 | #container {
131 | flex-direction: column;
132 | height: auto;
133 | }
134 | #mazeContainer {
135 | margin-bottom: 10px;
136 | }
137 | #maze {
138 | margin: 0 auto;
139 | display: block;
140 | }
141 | .title {
142 | display: none;
143 | }
144 | #mobileTitle {
145 | display: block;
146 | padding: 5px;
147 | }
148 | #sidebarContainer {
149 | flex: none;
150 | overflow: auto;
151 | }
152 | }
--------------------------------------------------------------------------------
/images/finish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codebox/mazes/9bc65d619ab750c325960e0026422428633861cb/images/finish.png
--------------------------------------------------------------------------------
/images/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codebox/mazes/9bc65d619ab750c325960e0026422428633861cb/images/player.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Maze Generator
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Maze Generator
12 |
13 |
14 |
15 |
16 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/js/config.js:
--------------------------------------------------------------------------------
1 | import {ALGORITHM_RECURSIVE_BACKTRACK} from './lib/constants.js';
2 |
3 | export const config = Object.freeze({
4 | shapes: {
5 | 'square': {
6 | description: 'Square Grid',
7 | parameters: {
8 | width: {
9 | min: 2,
10 | max: 50,
11 | initial: 10
12 | },
13 | height: {
14 | min: 2,
15 | max: 50,
16 | initial: 10
17 | }
18 | },
19 | defaultAlgorithm: ALGORITHM_RECURSIVE_BACKTRACK
20 | },
21 | 'triangle': {
22 | description: 'Triangle Grid',
23 | parameters: {
24 | width: {
25 | min: 4,
26 | max: 85,
27 | initial: 17
28 | },
29 | height: {
30 | min: 2,
31 | max: 50,
32 | initial: 10
33 | }
34 | },
35 | defaultAlgorithm: ALGORITHM_RECURSIVE_BACKTRACK
36 | },
37 | 'hexagon': {
38 | description: 'Hexagon Grid',
39 | parameters: {
40 | width: {
41 | min: 2,
42 | max: 50,
43 | initial: 10
44 | },
45 | height: {
46 | min: 2,
47 | max: 50,
48 | initial: 10
49 | }
50 | },
51 | defaultAlgorithm: ALGORITHM_RECURSIVE_BACKTRACK
52 | },
53 | 'circle': {
54 | description: 'Circular',
55 | parameters: {
56 | layers: {
57 | min: 2,
58 | max: 30,
59 | initial: 10
60 | }
61 | },
62 | defaultAlgorithm: ALGORITHM_RECURSIVE_BACKTRACK
63 | }
64 | }
65 | });
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | import {buildModel} from './model.js';
2 | import {buildView} from './view.js';
3 | import {buildMaze} from './lib/main.js';
4 | import {buildStateMachine, STATE_INIT, STATE_DISPLAYING, STATE_PLAYING, STATE_MASKING, STATE_DISTANCE_MAPPING, STATE_RUNNING_ALGORITHM} from './stateMachine.js';
5 | import {shapes} from './lib/shapes.js';
6 | import {drawingSurfaces} from './lib/drawingSurfaces.js';
7 | import {
8 | EVENT_MAZE_SHAPE_SELECTED, EVENT_SIZE_PARAMETER_CHANGED, EVENT_ALGORITHM_SELECTED, EVENT_GO_BUTTON_CLICKED, EVENT_WINDOW_RESIZED,
9 | EVENT_SHOW_MAP_BUTTON_CLICKED, EVENT_CLEAR_MAP_BUTTON_CLICKED, EVENT_CREATE_MASK_BUTTON_CLICKED,
10 | EVENT_SAVE_MASK_BUTTON_CLICKED, EVENT_CLEAR_MASK_BUTTON_CLICKED, EVENT_FINISH_RUNNING_BUTTON_CLICKED, EVENT_DELAY_SELECTED,
11 | EVENT_CHANGE_PARAMS_BUTTON_CLICKED, EVENT_EXITS_SELECTED, EVENT_SOLVE_BUTTON_CLICKED, EVENT_PLAY_BUTTON_CLICKED, EVENT_STOP_BUTTON_CLICKED,
12 | EVENT_KEY_PRESS, EVENT_DOWNLOAD_CLICKED
13 | } from './view.js';
14 | import {config} from './config.js';
15 | import {algorithms} from './lib/algorithms.js';
16 | import {buildRandom} from './lib/random.js';
17 | import {
18 | ALGORITHM_NONE, METADATA_MASKED, METADATA_END_CELL, METADATA_START_CELL, EVENT_CLICK, EXITS_NONE, EXITS_HARDEST, EXITS_HORIZONTAL, EXITS_VERTICAL,
19 | METADATA_PLAYER_CURRENT, METADATA_PLAYER_VISITED, METADATA_PATH, METADATA_VISITED,
20 | DIRECTION_NORTH, DIRECTION_SOUTH, DIRECTION_EAST, DIRECTION_WEST, DIRECTION_NORTH_WEST, DIRECTION_NORTH_EAST, DIRECTION_SOUTH_WEST, DIRECTION_SOUTH_EAST,
21 | DIRECTION_CLOCKWISE, DIRECTION_ANTICLOCKWISE, DIRECTION_INWARDS, DIRECTION_OUTWARDS,
22 | SHAPE_SQUARE, SHAPE_TRIANGLE, SHAPE_HEXAGON, SHAPE_CIRCLE
23 | } from './lib/constants.js';
24 |
25 | window.onload = () => {
26 | "use strict";
27 | const model = buildModel(),
28 | stateMachine = buildStateMachine(),
29 | view = buildView(model, stateMachine);
30 |
31 | function isMaskAvailableForCurrentConfig() {
32 | const currentMask = model.mask[getModelMaskKey()];
33 | return currentMask && currentMask.length;
34 | }
35 |
36 | function setupShapeParameter() {
37 | Object.keys(shapes).forEach(name => {
38 | view.addShape(name);
39 | });
40 |
41 | function onShapeSelected(shapeName) {
42 | view.setShape(model.shape = shapeName);
43 | view.updateMaskButtonCaption(isMaskAvailableForCurrentConfig());
44 | }
45 | onShapeSelected(model.shape);
46 |
47 | view.on(EVENT_MAZE_SHAPE_SELECTED, shapeName => {
48 | onShapeSelected(shapeName);
49 | setupSizeParameters();
50 | setupAlgorithms();
51 | showEmptyGrid(true);
52 | });
53 | }
54 |
55 | function setupSizeParameters() {
56 | const shape = model.shape,
57 | parameters = config.shapes[shape].parameters;
58 |
59 | model.size = {};
60 | view.clearSizeParameters();
61 |
62 | Object.entries(parameters).forEach(([paramName, paramValues]) => {
63 | view.addSizeParameter(paramName, paramValues.min, paramValues.max);
64 | });
65 |
66 | function onParameterChanged(name, value) {
67 | model.size[name] = value;
68 | view.setSizeParameter(name, value);
69 | view.updateMaskButtonCaption(isMaskAvailableForCurrentConfig());
70 | }
71 | Object.entries(parameters).forEach(([paramName, paramValues]) => {
72 | onParameterChanged(paramName, paramValues.initial);
73 | });
74 |
75 | view.on(EVENT_SIZE_PARAMETER_CHANGED, data => {
76 | if (view.getValidSizeParameters().includes(data.name)) {
77 | onParameterChanged(data.name, data.value);
78 | showEmptyGrid(true);
79 | setupAlgorithms();
80 | }
81 | });
82 | }
83 |
84 | function setupAlgorithms() {
85 | const shape = model.shape;
86 |
87 | view.clearAlgorithms();
88 |
89 | Object.entries(algorithms).filter(([algorithmId, algorithm]) => algorithmId !== ALGORITHM_NONE).forEach(([algorithmId, algorithm]) => {
90 | if (algorithm.metadata.shapes.includes(shape) && (algorithm.metadata.maskable || !isMaskAvailableForCurrentConfig())) {
91 | view.addAlgorithm(algorithm.metadata.description, algorithmId);
92 | }
93 | });
94 |
95 | function onAlgorithmChanged(algorithmId) {
96 | view.setAlgorithm(model.algorithm = algorithmId);
97 | }
98 | onAlgorithmChanged(config.shapes[shape].defaultAlgorithm);
99 |
100 | view.on(EVENT_ALGORITHM_SELECTED, onAlgorithmChanged);
101 | }
102 |
103 | function setupAlgorithmDelay() {
104 | view.addAlgorithmDelay('Instant Mazes', 0);
105 | view.addAlgorithmDelay('Show Algorithm Steps', 5000);
106 |
107 | view.on(EVENT_DELAY_SELECTED, algorithmDelay => {
108 | model.algorithmDelay = algorithmDelay;
109 | view.setAlgorithmDelay(algorithmDelay);
110 | });
111 | view.setAlgorithmDelay(model.algorithmDelay);
112 | }
113 |
114 | function setupExitConfigs() {
115 | view.addExitConfiguration('No Entrance/Exit', EXITS_NONE);
116 | view.addExitConfiguration('Bottom to Top', EXITS_VERTICAL);
117 | view.addExitConfiguration('Left to Right', EXITS_HORIZONTAL);
118 | view.addExitConfiguration('Hardest Entrance/Exit', EXITS_HARDEST);
119 |
120 | view.on(EVENT_EXITS_SELECTED, exitConfig => {
121 | view.setExitConfiguration(model.exitConfig = exitConfig);
122 | });
123 | view.setExitConfiguration(model.exitConfig);
124 | }
125 |
126 | setupShapeParameter();
127 | setupSizeParameters();
128 | setupExitConfigs();
129 | setupAlgorithmDelay();
130 | setupAlgorithms();
131 | showEmptyGrid(true);
132 |
133 | function buildMazeUsingModel(overrides={}) {
134 | if (model.maze) {
135 | model.maze.dispose();
136 | }
137 |
138 | const grid = Object.assign({'cellShape': model.shape}, model.size),
139 | maze = buildMaze({
140 | grid,
141 | 'algorithm': overrides.algorithm || model.algorithm,
142 | 'randomSeed' : model.randomSeed,
143 | 'element': overrides.element || document.getElementById('maze'),
144 | 'mask': overrides.mask || model.mask[getModelMaskKey()],
145 | 'exitConfig': overrides.exitConfig || model.exitConfig
146 | });
147 |
148 | model.maze = maze;
149 |
150 | maze.on(EVENT_CLICK, ifStateIs(STATE_DISTANCE_MAPPING).then(event => {
151 | maze.findDistancesFrom(...event.coords);
152 | maze.render();
153 | }));
154 |
155 | maze.on(EVENT_CLICK, ifStateIs(STATE_MASKING).then(event => {
156 | const cell = maze.getCellByCoordinates(event.coords);
157 | cell.metadata[METADATA_MASKED] = !cell.metadata[METADATA_MASKED];
158 | maze.render();
159 | }));
160 |
161 | maze.on(EVENT_CLICK, ifStateIs(STATE_PLAYING).then(event => {
162 | const currentCell = model.playState.currentCell,
163 | direction = maze.getClosestDirectionForClick(currentCell, event);
164 | navigate(direction, event.shift || view.isMobileLayout, event.alt || view.isMobileLayout);
165 | maze.render();
166 | }));
167 |
168 | const algorithmDelay = overrides.algorithmDelay !== undefined ? overrides.algorithmDelay : model.algorithmDelay,
169 | runAlgorithm = maze.runAlgorithm;
170 | if (algorithmDelay) {
171 | model.runningAlgorithm = {run: runAlgorithm};
172 | return new Promise(resolve => {
173 | stateMachine.runningAlgorithm();
174 | model.runningAlgorithm.interval = setInterval(() => {
175 | const done = runAlgorithm.oneStep();
176 | maze.render();
177 | if (done) {
178 | clearInterval(model.runningAlgorithm.interval);
179 | delete model.runningAlgorithm;
180 | stateMachine.displaying();
181 | resolve();
182 | }
183 | }, algorithmDelay/maze.cellCount);
184 | });
185 |
186 | } else {
187 | runAlgorithm.toCompletion();
188 | maze.render();
189 | return Promise.resolve();
190 | }
191 |
192 | }
193 |
194 | function showEmptyGrid(deleteMaskedCells) {
195 | buildMazeUsingModel({algorithmDelay: 0, exitConfig: EXITS_NONE, algorithm: ALGORITHM_NONE, mask: deleteMaskedCells ? model.mask[getModelMaskKey()] : []})
196 | .then(() => model.maze.render());
197 | }
198 |
199 | function ifStateIs(...states) {
200 | return {
201 | then(handler) {
202 | return event => {
203 | if (states.includes(stateMachine.state)) {
204 | handler(event);
205 | }
206 | };
207 | }
208 | }
209 | }
210 |
211 | view.on(EVENT_GO_BUTTON_CLICKED, () => {
212 | model.randomSeed = Number(view.getSeed() || buildRandom().int(Math.pow(10,9)));
213 | view.showSeedValue();
214 |
215 | const errors = view.inputErrorMessage();
216 | if (errors) {
217 | alert(errors);
218 | } else {
219 | buildMazeUsingModel().then(() => {
220 | view.toggleSolveButtonCaption(true);
221 | model.maze.render();
222 | stateMachine.displaying();
223 | });
224 | }
225 | });
226 | view.on(EVENT_SHOW_MAP_BUTTON_CLICKED, () => {
227 | stateMachine.distanceMapping();
228 | const [startCell, _1] = findStartAndEndCells(),
229 | coords = (startCell || model.maze.randomCell()).coords;
230 | model.maze.findDistancesFrom(...coords);
231 | model.maze.render();
232 | });
233 | view.on(EVENT_CLEAR_MAP_BUTTON_CLICKED, () => {
234 | stateMachine.displaying();
235 | model.maze.clearDistances();
236 | model.maze.render();
237 | });
238 |
239 | view.on(EVENT_FINISH_RUNNING_BUTTON_CLICKED, () => {
240 | clearInterval(model.runningAlgorithm.interval);
241 | model.runningAlgorithm.run.toCompletion();
242 | delete model.runningAlgorithm;
243 | stateMachine.displaying();
244 | model.maze.render();
245 | });
246 |
247 | stateMachine.onStateChange(newState => {
248 | view.updateForNewState(newState);
249 | });
250 | view.updateForNewState(stateMachine.state);
251 |
252 | function getModelMaskKey() {
253 | if (model.shape && model.size) {
254 | return `${model.shape}-${Object.values(model.size).join('-')}`;
255 | }
256 | }
257 |
258 | view.on(EVENT_CREATE_MASK_BUTTON_CLICKED, () => {
259 | stateMachine.masking();
260 | showEmptyGrid(false);
261 | (model.mask[getModelMaskKey()] || []).forEach(maskedCoords => {
262 | const cell = model.maze.getCellByCoordinates(maskedCoords);
263 | cell.metadata[METADATA_MASKED] = true;
264 | });
265 | model.maze.render();
266 | });
267 |
268 | function validateMask() {
269 | const isNotMasked = cell => !cell.metadata[METADATA_MASKED],
270 | startCell = model.maze.randomCell(isNotMasked);
271 | let unmaskedCellCount = 0;
272 |
273 | model.maze.forEachCell(cell => {
274 | if (isNotMasked(cell)) {
275 | unmaskedCellCount++;
276 | }
277 | });
278 | if (!startCell) {
279 | throw 'No unmasked cells remain';
280 | }
281 | if (unmaskedCellCount < 4) {
282 | throw 'Not enough unmasked cells to build a maze';
283 | }
284 |
285 | function countUnmasked(cell) {
286 | cell.metadata[METADATA_VISITED] = true;
287 | let count = 1;
288 | cell.neighbours.toArray(isNotMasked).forEach(neighbourCell => {
289 | if (!neighbourCell.metadata[METADATA_VISITED]) {
290 | count += countUnmasked(neighbourCell);
291 | }
292 | });
293 | return count;
294 | }
295 |
296 | model.maze.forEachCell(cell => {
297 | delete cell.metadata[METADATA_VISITED];
298 | });
299 |
300 | if (unmaskedCellCount !== countUnmasked(startCell)) {
301 | throw 'Your mask has cut off one or more cells so they are not reachable from the rest of the maze.';
302 | }
303 |
304 | if (model.shape === SHAPE_CIRCLE && model.maze.getCellByCoordinates(0,0).metadata[METADATA_MASKED]) {
305 | throw 'You can\'t mask out the centre of a circular maze';
306 | }
307 | }
308 |
309 | view.on(EVENT_SAVE_MASK_BUTTON_CLICKED, () => {
310 | try {
311 | validateMask();
312 | stateMachine.init();
313 | const mask = model.mask[getModelMaskKey()] = [];
314 | model.maze.forEachCell(cell => {
315 | if (cell.metadata[METADATA_MASKED]) {
316 | mask.push(cell.coords);
317 | }
318 | });
319 | showEmptyGrid(true);
320 | setupAlgorithms();
321 | view.updateMaskButtonCaption(isMaskAvailableForCurrentConfig());
322 | } catch (err) {
323 | alert(err);
324 | }
325 | });
326 |
327 | view.on(EVENT_CLEAR_MASK_BUTTON_CLICKED, () => {
328 | model.maze.forEachCell(cell => {
329 | delete cell.metadata[METADATA_MASKED];
330 | });
331 | model.maze.render();
332 | });
333 |
334 | view.on(EVENT_WINDOW_RESIZED, () => {
335 | model.maze.render();
336 | });
337 |
338 | view.on(EVENT_CHANGE_PARAMS_BUTTON_CLICKED, () => {
339 | showEmptyGrid(true);
340 | stateMachine.init();
341 | });
342 |
343 | function findStartAndEndCells() {
344 | let startCell, endCell;
345 | model.maze.forEachCell(cell => {
346 | if (cell.metadata[METADATA_START_CELL]) {
347 | startCell = cell;
348 | }
349 | if (cell.metadata[METADATA_END_CELL]) {
350 | endCell = cell;
351 | }
352 | });
353 | return [startCell, endCell];
354 | }
355 | view.on(EVENT_SOLVE_BUTTON_CLICKED, () => {
356 | const [startCell, endCell] = findStartAndEndCells();
357 | if (!(startCell && endCell)) {
358 | alert('You must generate a maze with exits in order to solve');
359 | return;
360 | }
361 | if (model.maze.metadata[METADATA_PATH]) {
362 | model.maze.clearPathAndSolution();
363 | view.toggleSolveButtonCaption(true);
364 | } else {
365 | const [startCell, endCell] = findStartAndEndCells();
366 | console.assert(startCell);
367 | console.assert(endCell);
368 | model.maze.findPathBetween(startCell.coords, endCell.coords);
369 | view.toggleSolveButtonCaption(false);
370 | }
371 | model.maze.render();
372 | });
373 |
374 | function getNavigationInstructions() {
375 | const isMobile = view.isMobileLayout,
376 | MOBILE_INSTRUCTIONS = 'Tap to move through the maze to the next junction',
377 | MOUSE_INSTRUCTIONS = 'Click to move through the maze',
378 | ALT_SHIFT_INSTRUCTIONS = 'Holding down SHIFT will move you as far as possible in one direction Holding down ALT and SHIFT will move you to the next junction';
379 |
380 | if (isMobile) {
381 | return MOBILE_INSTRUCTIONS;
382 | }
383 |
384 | return {
385 | [SHAPE_SQUARE]: `${MOUSE_INSTRUCTIONS} or use the arrow keys ${ALT_SHIFT_INSTRUCTIONS}`,
386 | [SHAPE_TRIANGLE]: `${MOUSE_INSTRUCTIONS} or use the arrow keys ${ALT_SHIFT_INSTRUCTIONS}`,
387 | [SHAPE_HEXAGON]: `${MOUSE_INSTRUCTIONS} ${ALT_SHIFT_INSTRUCTIONS}`,
388 | [SHAPE_CIRCLE]: `${MOUSE_INSTRUCTIONS} ${ALT_SHIFT_INSTRUCTIONS}`
389 | }[model.shape];
390 | }
391 |
392 | view.on(EVENT_PLAY_BUTTON_CLICKED, () => {
393 | const [startCell, endCell] = findStartAndEndCells();
394 | if (!(startCell && endCell)) {
395 | alert('You must generate a maze with exits in order to play');
396 | return;
397 | }
398 | model.maze.clearPathAndSolution();
399 | model.playState = {startCell, endCell, currentCell: startCell, startTime: Date.now()};
400 | startCell.metadata[METADATA_PLAYER_CURRENT] = true;
401 | startCell.metadata[METADATA_PLAYER_VISITED] = true;
402 | model.maze.render();
403 | stateMachine.playing();
404 | view.setNavigationInstructions(getNavigationInstructions());
405 | });
406 |
407 | view.on(EVENT_STOP_BUTTON_CLICKED, () => {
408 | model.maze.clearMetadata(METADATA_PLAYER_CURRENT, METADATA_PLAYER_VISITED);
409 | model.maze.render();
410 | stateMachine.displaying();
411 | });
412 |
413 | const keyCodeToDirection = {
414 | 38: DIRECTION_NORTH,
415 | 40: DIRECTION_SOUTH,
416 | 39: DIRECTION_EAST,
417 | 37: DIRECTION_WEST,
418 | 65: DIRECTION_NORTH_WEST, // A
419 | 83: DIRECTION_NORTH_EAST, // S
420 | 90: DIRECTION_SOUTH_WEST, // Z
421 | 88: DIRECTION_SOUTH_EAST, // X
422 | 81: DIRECTION_CLOCKWISE, // Q
423 | 87: DIRECTION_ANTICLOCKWISE, // W
424 | 80: DIRECTION_INWARDS, // P
425 | 76: `${DIRECTION_OUTWARDS}_1`, // L
426 | 186: `${DIRECTION_OUTWARDS}_0` // ;
427 | };
428 |
429 | function padNum(num) {
430 | return num < 10 ? '0' + num : num;
431 | }
432 | function formatTime(millis) {
433 | const hours = Math.floor(millis / (1000 * 60 * 60)),
434 | minutes = Math.floor((millis % (1000 * 60 * 60)) / (1000 * 60)),
435 | seconds = Math.floor((millis % (1000 * 60)) / 1000);
436 |
437 | return `${padNum(hours)}:${padNum(minutes)}:${padNum(seconds)}`;
438 | }
439 |
440 | function onMazeCompleted() {
441 | const timeMs = Date.now() - model.playState.startTime,
442 | time = formatTime(timeMs),
443 | {startCell, endCell} = model.playState;
444 |
445 | model.playState.finished = true;
446 |
447 | model.maze.findPathBetween(startCell.coords, endCell.coords);
448 | const optimalPathLength = model.maze.metadata[METADATA_PATH].length;
449 | delete model.maze.metadata[METADATA_PATH];
450 |
451 | let visitedCells = 0;
452 | model.maze.forEachCell(cell => {
453 | if (cell.metadata[METADATA_PLAYER_VISITED]) {
454 | visitedCells++;
455 | }
456 | });
457 |
458 | const cellsPerSecond = visitedCells / (timeMs / 1000);
459 | model.maze.render();
460 | stateMachine.displaying();
461 | view.showInfo(`
462 | Finish Time: ${time}
463 | Visited Cells: ${visitedCells}
464 | Optimal Route: ${optimalPathLength}
465 | Optimality: ${Math.floor(100 * optimalPathLength / visitedCells)}%
466 | Cells per Second: ${Math.round(cellsPerSecond)}
467 | `);
468 | }
469 |
470 | function navigate(direction, shift, alt) {
471 | while (true) {
472 | const currentCell = model.playState.currentCell,
473 | targetCell = currentCell.neighbours[direction],
474 | moveOk = targetCell && targetCell.isLinkedTo(currentCell);
475 |
476 | if (moveOk) {
477 | delete currentCell.metadata[METADATA_PLAYER_CURRENT];
478 | targetCell.metadata[METADATA_PLAYER_VISITED] = true;
479 | targetCell.metadata[METADATA_PLAYER_CURRENT] = true;
480 | model.playState.previousCell = currentCell;
481 | model.playState.currentCell = targetCell;
482 |
483 | if (targetCell.metadata[METADATA_END_CELL]) {
484 | onMazeCompleted();
485 | }
486 |
487 | if (model.playState.finished) {
488 | break;
489 | } else if (!shift) {
490 | break;
491 | } else if (alt) {
492 | const linkedDirections = targetCell.neighbours.linkedDirections();
493 | if (linkedDirections.length === 2) {
494 | direction = linkedDirections.find(neighbourDirection => targetCell.neighbours[neighbourDirection] !== model.playState.previousCell);
495 | } else {
496 | break;
497 | }
498 | }
499 |
500 | } else {
501 | break;
502 | }
503 | }
504 | }
505 |
506 | view.on(EVENT_KEY_PRESS, ifStateIs(STATE_PLAYING).then(event => {
507 | const {keyCode, shift, alt} = event,
508 | direction = keyCodeToDirection[keyCode];
509 |
510 | navigate(direction, shift, alt);
511 |
512 | model.maze.render();
513 | }));
514 |
515 | view.on(EVENT_DOWNLOAD_CLICKED, () => {
516 | function saveSvg(svgEl, name) {
517 | const svgData = svgEl.outerHTML,
518 | prolog = '',
519 | blob = new Blob([prolog, svgData], {type: 'image/svg+xml;charset=utf-8'}),
520 | blobAsUrl = URL.createObjectURL(blob),
521 | downloadLink = document.createElement('a');
522 | downloadLink.href = blobAsUrl;
523 | downloadLink.download = name;
524 | downloadLink.click();
525 | }
526 |
527 | const SVG_SIZE = 500,
528 | elSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
529 | elSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
530 | elSvg.setAttribute('width', SVG_SIZE);
531 | elSvg.setAttribute('height', SVG_SIZE);
532 |
533 | const svgDrawingSurface = drawingSurfaces.svg({el: elSvg}),
534 | fileName = `maze_${model.shape}_${Object.values(model.size).join('_')}_${model.randomSeed}.svg`;
535 | model.maze.render(svgDrawingSurface);
536 | saveSvg(elSvg, fileName);
537 | });
538 | };
--------------------------------------------------------------------------------
/js/model.js:
--------------------------------------------------------------------------------
1 | export function buildModel() {
2 | const model = {
3 | shape: 'square',
4 | mask: {},
5 | algorithmDelay: 0,
6 | exitConfig: 'vertical'
7 | };
8 |
9 | return model;
10 | }
--------------------------------------------------------------------------------
/js/stateMachine.js:
--------------------------------------------------------------------------------
1 | import {buildEventTarget} from './lib/utils.js';
2 |
3 | export const STATE_INIT = 'Init',
4 | STATE_MASKING = 'Masking',
5 | STATE_DISPLAYING = 'Displaying',
6 | STATE_DISTANCE_MAPPING = 'Distance Mapping',
7 | STATE_RUNNING_ALGORITHM = 'Running Algorithm',
8 | STATE_PLAYING = 'Playing';
9 |
10 | export function buildStateMachine() {
11 | "use strict";
12 | const eventTarget = buildEventTarget('stateMachine'),
13 | EVENT_STATE_CHANGED = 'stateChanged';
14 | let state = STATE_INIT;
15 |
16 | function ifStateIsOneOf(...validStates) {
17 | return {
18 | thenChangeTo(newState) {
19 | if (validStates.includes(state)) {
20 | console.debug('State changed to', newState);
21 | state = newState;
22 | eventTarget.trigger(EVENT_STATE_CHANGED, newState);
23 |
24 | } else if (state === newState) {
25 | console.debug('Ignoring redundant state transition', state);
26 |
27 | } else {
28 | console.warn(`Unexpected state transition requested: ${state} -> ${newState}`);
29 | }
30 | }
31 | }
32 | }
33 |
34 | return {
35 | get state() {
36 | return state;
37 | },
38 | init() {
39 | ifStateIsOneOf(STATE_DISPLAYING, STATE_MASKING, STATE_DISTANCE_MAPPING)
40 | .thenChangeTo(STATE_INIT);
41 | },
42 | masking() {
43 | ifStateIsOneOf(STATE_INIT, STATE_DISPLAYING)
44 | .thenChangeTo(STATE_MASKING);
45 | },
46 | displaying() {
47 | ifStateIsOneOf(STATE_INIT, STATE_MASKING, STATE_PLAYING, STATE_DISTANCE_MAPPING, STATE_RUNNING_ALGORITHM)
48 | .thenChangeTo(STATE_DISPLAYING);
49 | },
50 | distanceMapping() {
51 | ifStateIsOneOf(STATE_DISPLAYING)
52 | .thenChangeTo(STATE_DISTANCE_MAPPING);
53 | },
54 | playing() {
55 | ifStateIsOneOf(STATE_DISPLAYING)
56 | .thenChangeTo(STATE_PLAYING);
57 | },
58 | runningAlgorithm() {
59 | ifStateIsOneOf(STATE_INIT, STATE_DISPLAYING)
60 | .thenChangeTo(STATE_RUNNING_ALGORITHM);
61 | },
62 | onStateChange(handler) {
63 | eventTarget.on(EVENT_STATE_CHANGED, handler);
64 | }
65 | };
66 |
67 | }
--------------------------------------------------------------------------------
/js/view.js:
--------------------------------------------------------------------------------
1 | import {buildEventTarget} from './lib/utils.js';
2 |
3 | export const
4 | EVENT_MAZE_SHAPE_SELECTED = 'mazeShapeSelected',
5 | EVENT_SIZE_PARAMETER_CHANGED = 'mazeSizeParameterChanged',
6 | EVENT_DELAY_SELECTED = 'runModeSelected',
7 | EVENT_ALGORITHM_SELECTED = 'algorithmSelected',
8 | EVENT_GO_BUTTON_CLICKED = 'goButtonClicked',
9 | EVENT_SHOW_MAP_BUTTON_CLICKED = 'showDistanceMapButtonClicked',
10 | EVENT_CLEAR_MAP_BUTTON_CLICKED = 'clearDistanceMapButtonClicked',
11 | EVENT_CREATE_MASK_BUTTON_CLICKED = 'createMaskButtonClicked',
12 | EVENT_SAVE_MASK_BUTTON_CLICKED = 'saveMaskButtonClicked',
13 | EVENT_CLEAR_MASK_BUTTON_CLICKED = 'clearMaskButtonClicked',
14 | EVENT_FINISH_RUNNING_BUTTON_CLICKED = 'finishRunningButtonClicked',
15 | EVENT_CHANGE_PARAMS_BUTTON_CLICKED = 'changeParamsButtonClicked',
16 | EVENT_SOLVE_BUTTON_CLICKED = 'solveButtonClicked',
17 | EVENT_PLAY_BUTTON_CLICKED = 'playButtonClicked',
18 | EVENT_STOP_BUTTON_CLICKED = 'stopButtonClicked',
19 | EVENT_DOWNLOAD_CLICKED = 'downloadClicked',
20 | EVENT_KEY_PRESS = 'keyPress',
21 | EVENT_WINDOW_RESIZED = 'windowResized',
22 | EVENT_EXITS_SELECTED = 'exitsSelected';
23 |
24 |
25 | import {STATE_INIT, STATE_DISPLAYING, STATE_PLAYING, STATE_MASKING, STATE_DISTANCE_MAPPING, STATE_RUNNING_ALGORITHM} from './stateMachine.js';
26 |
27 | export function buildView(model, stateMachine) {
28 | "use strict";
29 |
30 | const eventTarget = buildEventTarget('view'),
31 | elCanvas = document.getElementById('maze'),
32 | elMazeContainer = document.getElementById('mazeContainer'),
33 | elGoButton = document.getElementById('go'),
34 | elShowDistanceMapButton = document.getElementById('showDistanceMap'),
35 | elClearDistanceMapButton = document.getElementById('clearDistanceMap'),
36 | elCreateMaskButton = document.getElementById('createMask'),
37 | elSaveMaskButton = document.getElementById('saveMask'),
38 | elClearMaskButton = document.getElementById('clearMask'),
39 | elFinishRunningButton = document.getElementById('finishRunning'),
40 | elSolveButton = document.getElementById('solve'),
41 | elPlayButton = document.getElementById('play'),
42 | elStopButton = document.getElementById('stop'),
43 | elChangeParamsButton = document.getElementById('changeParams'),
44 | elDownloadButton = document.getElementById('download'),
45 | elInfo = document.getElementById('info'),
46 | elSeedInput = document.getElementById('seedInput'),
47 | elSizeParameterList = document.getElementById('sizeParameters'),
48 | elSeedParameterList = document.getElementById('seedParameters'),
49 | elMazeShapeList = document.getElementById('shapeSelector'),
50 | elMazeAlgorithmList = document.getElementById('algorithmSelector'),
51 | elAlgorithmDelayList = document.getElementById('delaySelector'),
52 | elExitsList = document.getElementById('exitSelector'),
53 | elMobileTitle = document.getElementById('mobileTitle'),
54 |
55 | isMobileLayout = !! elMobileTitle.offsetParent;
56 |
57 |
58 | elGoButton.onclick = () => eventTarget.trigger(EVENT_GO_BUTTON_CLICKED);
59 | elShowDistanceMapButton.onclick = () => eventTarget.trigger(EVENT_SHOW_MAP_BUTTON_CLICKED);
60 | elClearDistanceMapButton.onclick = () => eventTarget.trigger(EVENT_CLEAR_MAP_BUTTON_CLICKED);
61 | elCreateMaskButton.onclick = () => eventTarget.trigger(EVENT_CREATE_MASK_BUTTON_CLICKED);
62 | elSaveMaskButton.onclick = () => eventTarget.trigger(EVENT_SAVE_MASK_BUTTON_CLICKED);
63 | elClearMaskButton.onclick = () => eventTarget.trigger(EVENT_CLEAR_MASK_BUTTON_CLICKED);
64 | elFinishRunningButton.onclick = () => eventTarget.trigger(EVENT_FINISH_RUNNING_BUTTON_CLICKED);
65 | elChangeParamsButton.onclick = () => eventTarget.trigger(EVENT_CHANGE_PARAMS_BUTTON_CLICKED);
66 | elSolveButton.onclick = () => eventTarget.trigger(EVENT_SOLVE_BUTTON_CLICKED);
67 | elPlayButton.onclick = () => eventTarget.trigger(EVENT_PLAY_BUTTON_CLICKED);
68 | elStopButton.onclick = () => eventTarget.trigger(EVENT_STOP_BUTTON_CLICKED);
69 | elDownloadButton.onclick = () => eventTarget.trigger(EVENT_DOWNLOAD_CLICKED);
70 |
71 | window.onkeydown = event => eventTarget.trigger(EVENT_KEY_PRESS, {keyCode: event.keyCode, alt: event.altKey, shift: event.shiftKey});
72 |
73 | function fitCanvasToContainer() {
74 | if (isMobileLayout) {
75 | elMazeContainer.style.height = `${elMazeContainer.clientWidth}px`;
76 | }
77 |
78 | elCanvas.width = elMazeContainer.clientWidth;
79 | elCanvas.height = elMazeContainer.clientHeight;
80 | }
81 | window.onresize = () => {
82 | fitCanvasToContainer();
83 | eventTarget.trigger(EVENT_WINDOW_RESIZED);
84 | };
85 | fitCanvasToContainer();
86 |
87 | function toggleElementVisibility(el, display) {
88 | el.style.display = display ? 'block' : 'none';
89 | }
90 |
91 | return {
92 | // Shape
93 | addShape(shapeName) {
94 | const elMazeShapeItem = document.createElement('li');
95 | elMazeShapeItem.innerHTML = shapeName;
96 | elMazeShapeItem.onclick = () => eventTarget.trigger(EVENT_MAZE_SHAPE_SELECTED, shapeName);
97 | elMazeShapeList.appendChild(elMazeShapeItem);
98 | elMazeShapeItem.dataset.value = shapeName;
99 | },
100 | setShape(shapeName) {
101 | [...elMazeShapeList.querySelectorAll('li')].forEach(el => {
102 | el.classList.toggle('selected', el.dataset.value === shapeName);
103 | });
104 | },
105 |
106 | // Size
107 | clearSizeParameters() {
108 | elSizeParameterList.innerHTML = '';
109 | },
110 | addSizeParameter(name, minimumValue, maximumValue) {
111 | const elParameterItem = document.createElement('li'),
112 | elParameterName = document.createElement('label'),
113 | elParameterValue = document.createElement('input');
114 |
115 | elParameterName.innerHTML = name;
116 |
117 | elParameterValue.setAttribute('type', 'number');
118 | elParameterValue.setAttribute('required', 'required');
119 | elParameterValue.setAttribute('min', minimumValue);
120 | elParameterValue.setAttribute('max', maximumValue);
121 | elParameterValue.oninput = () => eventTarget.trigger(EVENT_SIZE_PARAMETER_CHANGED, {
122 | name,
123 | value: Number(elParameterValue.value)
124 | });
125 | elParameterValue.dataset.value = name;
126 |
127 | elParameterItem.appendChild(elParameterName);
128 | elParameterItem.appendChild(elParameterValue);
129 | elSizeParameterList.appendChild(elParameterItem);
130 | },
131 | setSizeParameter(name, value) {
132 | const elParamInput = [...elSizeParameterList.querySelectorAll('input')].find(el => el.dataset.value === name);
133 | elParamInput.value = value;
134 | },
135 |
136 | // Exits
137 | addExitConfiguration(description, value) {
138 | const elExitsItem = document.createElement('li');
139 | elExitsItem.innerHTML = description;
140 | elExitsItem.onclick = () => eventTarget.trigger(EVENT_EXITS_SELECTED, value);
141 | elExitsList.appendChild(elExitsItem);
142 | elExitsItem.dataset.value = value;
143 | },
144 | setExitConfiguration(exitConfiguration) {
145 | [...elExitsList.querySelectorAll('li')].forEach(el => {
146 | el.classList.toggle('selected', el.dataset.value === exitConfiguration);
147 | });
148 | },
149 |
150 | // Algorithm Delay
151 | addAlgorithmDelay(description, value) {
152 | const elDelayItem = document.createElement('li');
153 | elDelayItem.innerHTML = description;
154 | elDelayItem.onclick = () => eventTarget.trigger(EVENT_DELAY_SELECTED, value);
155 | elAlgorithmDelayList.appendChild(elDelayItem);
156 | elDelayItem.dataset.value = value;
157 | },
158 | setAlgorithmDelay(algorithmDelay) {
159 | [...elAlgorithmDelayList.querySelectorAll('li')].forEach(el => {
160 | el.classList.toggle('selected', Number(el.dataset.value) === algorithmDelay);
161 | });
162 | },
163 |
164 | // Algorithm
165 | clearAlgorithms() {
166 | elMazeAlgorithmList.innerHTML = '';
167 | },
168 | addAlgorithm(description, algorithmId) {
169 | const elAlgorithmItem = document.createElement('li');
170 | elAlgorithmItem.innerHTML = description;
171 | elAlgorithmItem.onclick = () => eventTarget.trigger(EVENT_ALGORITHM_SELECTED, algorithmId);
172 | elMazeAlgorithmList.appendChild(elAlgorithmItem);
173 | elAlgorithmItem.dataset.value = algorithmId;
174 | },
175 | setAlgorithm(algorithmId) {
176 | [...elMazeAlgorithmList.querySelectorAll('li')].forEach(el => {
177 | el.classList.toggle('selected', el.dataset.value === algorithmId);
178 | });
179 | },
180 |
181 | toggleSolveButtonCaption(solve) {
182 | elSolveButton.innerHTML = solve ? 'Solve' : 'Clear Solution';
183 | },
184 |
185 | getSeed() {
186 | return elSeedInput.value;
187 | },
188 |
189 | getValidSizeParameters() {
190 | return [...elSizeParameterList.querySelectorAll('input')].filter(elInput => elInput.checkValidity()).map(el => el.dataset.value);
191 | },
192 |
193 | inputErrorMessage() {
194 | const errors = [];
195 |
196 | [...elSizeParameterList.querySelectorAll('input')].forEach(elInput => {
197 | if (!elInput.checkValidity()) {
198 | errors.push(`Enter a number between ${elInput.min} and ${elInput.max} for ${elInput.dataset.value}`);
199 | }
200 | });
201 |
202 | if (!elSeedInput.checkValidity()) {
203 | errors.push('Enter between 1 and 9 digits for the Seed');
204 | }
205 |
206 | return errors.join('\n');
207 | },
208 | isMobileLayout,
209 |
210 | updateForNewState(state) {
211 | toggleElementVisibility(elMazeShapeList, [STATE_INIT].includes(state));
212 | toggleElementVisibility(elMazeAlgorithmList, [STATE_INIT].includes(state));
213 | toggleElementVisibility(elSizeParameterList, [STATE_INIT].includes(state));
214 | toggleElementVisibility(elSeedParameterList, [STATE_INIT].includes(state));
215 | toggleElementVisibility(elExitsList, [STATE_INIT].includes(state));
216 | toggleElementVisibility(elAlgorithmDelayList, [STATE_INIT].includes(state));
217 | toggleElementVisibility(elCreateMaskButton, [STATE_INIT].includes(state));
218 |
219 | toggleElementVisibility(elGoButton, [STATE_INIT, STATE_DISPLAYING].includes(state));
220 | toggleElementVisibility(elDownloadButton, [STATE_DISPLAYING, STATE_DISTANCE_MAPPING].includes(state));
221 |
222 | toggleElementVisibility(elChangeParamsButton, [STATE_DISPLAYING].includes(state));
223 | toggleElementVisibility(elShowDistanceMapButton, [STATE_DISPLAYING].includes(state));
224 | toggleElementVisibility(elSolveButton, [STATE_DISPLAYING].includes(state));
225 | toggleElementVisibility(elPlayButton, [STATE_DISPLAYING].includes(state));
226 | toggleElementVisibility(elStopButton, [STATE_PLAYING].includes(state));
227 |
228 | toggleElementVisibility(elClearDistanceMapButton, [STATE_DISTANCE_MAPPING].includes(state));
229 |
230 | toggleElementVisibility(elSaveMaskButton, [STATE_MASKING].includes(state));
231 | toggleElementVisibility(elClearMaskButton, [STATE_MASKING].includes(state));
232 | toggleElementVisibility(elFinishRunningButton, [STATE_RUNNING_ALGORITHM].includes(state));
233 |
234 | switch(state) {
235 | case STATE_INIT:
236 | this.showInfo('Select parameters for your maze and then click New Maze ');
237 | break;
238 | case STATE_DISPLAYING:
239 | this.showSeedValue();
240 | this.toggleSolveButtonCaption(true);
241 | break;
242 | case STATE_DISTANCE_MAPPING:
243 | this.showInfo('Click somewhere in the maze to generate a distance map for that location. Cells are coloured according to how difficult they are to reach from your chosen point.');
244 | break;
245 | case STATE_PLAYING:
246 | this.showInfo('');
247 | break;
248 | case STATE_RUNNING_ALGORITHM:
249 | this.showInfo('The maze generation algorithm has been slowed down. Click FINISH to skip to the end.');
250 | break;
251 | case STATE_MASKING:
252 | this.showInfo('Define a mask by selecting cells from the grid. Masked cells will not be included in your maze');
253 | break;
254 | default:
255 | console.assert(false, 'unexpected state value: ' + state);
256 | }
257 | },
258 |
259 | updateMaskButtonCaption(maskAvailable) {
260 | elCreateMaskButton.innerHTML = maskAvailable ? 'Edit Mask' : 'Create Mask';
261 | },
262 |
263 | showSeedValue() {
264 | this.showInfo(`Seed Value:${model.randomSeed} `);
265 | },
266 | showInfo(msg) {
267 | toggleElementVisibility(elInfo, msg);
268 | elInfo.innerHTML = msg;
269 | },
270 | setNavigationInstructions(instructions) {
271 | this.showInfo(instructions);
272 | },
273 |
274 | on(eventName, handler) {
275 | eventTarget.on(eventName, handler);
276 | }
277 | };
278 | }
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 | TODO Later
2 | ----------
3 | Add arrows to solution path
4 | Split maze types into separate files
5 | Show maze stats/details
6 | Mask shift key selection
7 |
--------------------------------------------------------------------------------