├── README.md
├── demo
├── demo.html
└── main.js
└── src
└── Maze.js
/README.md:
--------------------------------------------------------------------------------
1 | MazeJS
2 | ======
3 |
4 | A JavaScript tool for generating Maze by Growing Tree Algorithm.
5 |
6 |
7 | ====================
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Maze Demo
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
50 |
51 |
52 |
56 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/demo/main.js:
--------------------------------------------------------------------------------
1 | var maze = new Maze({
2 | width: 20,
3 | height: 20,
4 |
5 | perfect: true,
6 | braid: false,
7 | checkOver: false,
8 |
9 | onInit: function() {
10 | this.checkOver = $id("checkOver").checked;
11 | this.checkCount = {};
12 | // this.traceInfo = {};
13 | this.foundEndNode = false;
14 | },
15 |
16 | getNeighbor: function() {
17 | // if (this.currentDirCount < 6 && this.neighbors[this.currentDir]) {
18 | // return this.neighbors[this.currentDir];
19 | // }
20 | var list = this.neighbors.list;
21 | var n = list[list.length * Math.random() >> 0];
22 | return n;
23 | },
24 |
25 | isValid: function(nearNode, node, dir) {
26 | if (!nearNode || nearNode.value === null) {
27 | return false;
28 | }
29 | if (nearNode.value === 0) {
30 | return true;
31 | }
32 | if (this.perfect || this.braid) {
33 | return false;
34 | }
35 | var c = nearNode.x,
36 | r = nearNode.y;
37 | // 用于生成一种非Perfect迷宫
38 | this.checkCount[c + "-" + r] = this.checkCount[c + "-" + r] || 0;
39 | var count = ++this.checkCount[c + "-" + r];
40 | return Math.random() < 0.3 && count < 3;
41 | },
42 |
43 | beforeBacktrace: function() {
44 | // if (!this.braid || Math.random() < 0.5) {
45 | if (!this.braid) {
46 | return;
47 | }
48 | var n = [];
49 | var node = this.currentNode;
50 | var c = node.x;
51 | var r = node.y;
52 | var nearNode, dir, op;
53 |
54 | var first = null;
55 | var currentDir = this.currentDir;
56 | var updateNear = function() {
57 | op = Maze.Direction.opposite[dir];
58 | if (nearNode && nearNode.value !== null && (nearNode.value & op) !== op) {
59 | n.push([nearNode, dir]);
60 | if (dir == currentDir) {
61 | first = [nearNode, dir];
62 | }
63 | }
64 | };
65 |
66 | dir = Maze.Direction.N;
67 | nearNode = r > 0 ? this.grid[r - 1][c] : null;
68 | updateNear();
69 |
70 | if (!first) {
71 | dir = Maze.Direction.E;
72 | nearNode = this.grid[r][c + 1];
73 | updateNear();
74 | }
75 |
76 | if (!first) {
77 | dir = Maze.Direction.S;
78 | nearNode = r < this.height - 1 ? this.grid[r + 1][c] : null;
79 | updateNear();
80 | }
81 |
82 | if (!first) {
83 | dir = Maze.Direction.W;
84 | nearNode = this.grid[r][c - 1];
85 | updateNear();
86 | }
87 |
88 | n = first || n[n.length * Math.random() >> 0];
89 | this.moveTo(n[0], n[1]);
90 | },
91 |
92 | updateCurrent: function() {
93 | // this.traceInfo[this.currentNode.x + "-" + this.currentNode.y] = this.stepCount;
94 | if (this.braid) {
95 | return;
96 | }
97 | // 每步有 10% 的概率 进行回溯
98 | if (Math.random() <= 0.10) {
99 | this.backtrace();
100 | }
101 | },
102 |
103 | getTraceIndex: function() {
104 | var len = this.trace.length;
105 |
106 | if (this.braid) {
107 | return len - 1;
108 | }
109 |
110 | // 按一定的概率随机选择回溯策略
111 | var r = Math.random();
112 | var idx = 0;
113 | if (r < 0.5) {
114 | idx = len - 1;
115 | } else if (r < 0.7) {
116 | idx = len >> 1;
117 | } else if (r < 0.8) {
118 | idx = len * Math.random() >> 0;
119 | }
120 | return idx;
121 | },
122 |
123 | afterGenrate: function() {
124 | if (this.braid && this.getRoadCount(this.startNode)<2) {
125 | this.currentDirCount = 1000;
126 | this.setCurrent(this.startNode);
127 | this.nextStep();
128 | }
129 | },
130 |
131 | isOver: function() {
132 | if (!this.checkOver) {
133 | return false;
134 | }
135 | if (this.currentNode == this.endNode) {
136 | this.foundEndNode = true;
137 | }
138 | // 当探索到迷宫终点, 且探索了至少一半的区域时,终止迷宫的生成
139 | if (this.foundEndNode && this.stepCount >= this.size / 2) {
140 | return true;
141 | }
142 | return false;
143 | }
144 | });
145 |
146 |
147 |
148 | window.onload = function() {
149 | start();
150 | }
151 |
152 | function createPerfectMaze() {
153 | createMaze(true, false);
154 | }
155 |
156 | function createBraidMaze() {
157 | createMaze(false, true);
158 | }
159 |
160 | function createMaze(perfect, braid) {
161 | maze.perfect = perfect || false;
162 | maze.braid = braid || false;
163 |
164 | maze.init();
165 |
166 | // maze.setStart(0, 0);
167 | // maze.setEnd(4, 4);
168 |
169 | maze.startNode = maze.getRandomNode();
170 | do {
171 | maze.endNode = maze.getRandomNode();
172 | } while (maze.startNode == maze.endNode);
173 |
174 | // maze.setBlock(15, 15, 6, 5);
175 | // maze.setRoom(5, 5, 6, 5);
176 | maze.generate();
177 |
178 |
179 | renderMaze(context, maze);
180 | }
181 |
182 | function $id(id) {
183 | return document.getElementById(id);
184 | }
185 |
186 | var canvas, context;
187 |
188 | function start() {
189 | canvas = $id("canvas");
190 | context = canvas.getContext("2d");
191 | createPerfectMaze();
192 | }
193 |
194 | function renderMaze(context, maze) {
195 |
196 | // var grid = JSON.parse(JSON.stringify(maze.grid));
197 | var grid = maze.grid;
198 | var canvasWidth = 800,
199 | canvasHeight = 600;
200 | var padding = 10;
201 | var wallWidth = 2;
202 |
203 | var cellSize = (canvasWidth - padding * 2) / maze.width;
204 | cellSize = Math.min(cellSize, (canvasHeight - padding * 2) / maze.height) >> 0;
205 | var x = padding,
206 | y = padding;
207 |
208 | var cellW = cellSize;
209 | var cellH = cellSize;
210 |
211 | canvas.width = canvasWidth;
212 | canvas.height = canvasHeight;
213 |
214 | context.fillStyle = "#eeeeee";
215 | context.fillRect(0, 0, canvas.width, canvas.height);
216 | context.fillStyle = "#334466";
217 | context.strokeStyle = "#334466";
218 | context.font = "12px Arial";
219 | context.lineWidth = wallWidth;
220 |
221 | var cellY = y;
222 | var mazeHeight = 0;
223 | for (var r = 0; r < grid.length; r++) {
224 | var row = grid[r];
225 |
226 | // cellH=cellSize-5+(Math.random()*20>>0);
227 | cellH = cellSize;
228 |
229 | for (var c = 0; c < row.length; c++) {
230 | var node = row[c];
231 | var cx = c * cellW + x;
232 | var cy = cellY;
233 | if (!node.value) {
234 | context.fillRect(cx, cy, cellW, cellH);
235 | continue;
236 | }
237 |
238 | if (node == maze.startNode) {
239 | context.fillStyle = "#f3bbaa";
240 | context.fillRect(cx, cy, cellW, cellH);
241 | context.fillStyle = "#334466";
242 | context.fillText("S", cx + cellW * 1 / 3, cy + cellH - 2);
243 | } else if (node == maze.endNode) {
244 | context.fillStyle = "#f3bbaa";
245 | context.fillRect(cx, cy, cellW, cellH);
246 | context.fillStyle = "#334466";
247 | context.fillText("E", cx + cellW * 1 / 3, cy + cellH - 2);
248 | } else {
249 | // var text = maze.traceInfo[node.x + "-" + node.y];
250 | // context.fillText(text, cx + cellW * 1 / 3, cy + cellH - 2);
251 | }
252 |
253 | var left = (node.value & Maze.Direction.W) !== Maze.Direction.W;
254 | var top = (node.value & Maze.Direction.N) !== Maze.Direction.N;
255 | if (left && top) {
256 | context.fillRect(cx, cy, wallWidth, cellH);
257 | context.fillRect(cx, cy, cellW, wallWidth);
258 | } else if (left) {
259 | context.fillRect(cx, cy, wallWidth, cellH);
260 | } else if (top) {
261 | context.fillRect(cx, cy, cellW, wallWidth);
262 | } else {
263 | var w = false;
264 | if (r > 0) {
265 | w = (grid[r - 1][c].value & Maze.Direction.W) !== Maze.Direction.W;
266 | }
267 | if (w && c > 0) {
268 | w = (grid[r][c - 1].value & Maze.Direction.N) !== Maze.Direction.N;
269 | }
270 | var ltc = w ? 1 : 0;
271 | if (ltc) {
272 | context.fillRect(cx, cy, wallWidth, wallWidth);
273 | }
274 | }
275 | }
276 | cellY += cellH;
277 | mazeHeight += cellH;
278 | }
279 |
280 | context.fillRect(x, mazeHeight + y, cellW * maze.width, wallWidth);
281 | context.fillRect(cellW * maze.width + x, y, wallWidth, mazeHeight + wallWidth);
282 | }
283 |
--------------------------------------------------------------------------------
/src/Maze.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var Maze = function(options) {
4 | for (var p in options) {
5 | this[p] = options[p];
6 | }
7 | };
8 |
9 |
10 | Maze.prototype = {
11 | constructor: Maze,
12 |
13 | width: 0,
14 | height: 0,
15 | grid: null,
16 |
17 | currentDir: 0,
18 | currentDirCount: 0,
19 |
20 | currentNode: null,
21 | startNode: null,
22 | endNode: null,
23 |
24 | // 是否每走一步, 都尝试回溯.
25 | alwaysBacktrace: false,
26 |
27 | init: function() {
28 | this.trace = [];
29 |
30 | this.size = this.width * this.height;
31 | this.initGrid();
32 | this.onInit();
33 | },
34 |
35 | initGrid: function() {
36 | var grid = this.grid = [];
37 | for (var r = 0; r < this.height; r++) {
38 | var row = [];
39 | grid.push(row);
40 | for (var c = 0; c < this.width; c++) {
41 | var node = {
42 | x: c,
43 | y: r,
44 | value: 0,
45 | };
46 | row.push(node);
47 | }
48 | }
49 |
50 | },
51 |
52 | onInit: function() {},
53 |
54 | random: function(min, max) {
55 | return ((max - min + 1) * Math.random() + min) >> 0;
56 | },
57 | getNode: function(c, r) {
58 | return this.grid[r][c];
59 | },
60 | getRandomNode: function() {
61 | var r = this.random(0, this.height - 1);
62 | var c = this.random(0, this.width - 1);
63 | return this.grid[r][c];
64 | },
65 | setMark: function(node, value) {
66 | return node.value |= value;
67 | },
68 | removeMark: function(node, value) {
69 | return node.value &= ~value;
70 | },
71 | isMarked: function(node, value) {
72 | return (node.value & value) === value;
73 | },
74 |
75 | setStart: function(c, r) {
76 | var node = this.grid[r][c];
77 | this.startNode = node;
78 | },
79 | setEnd: function(c, r) {
80 | var node = this.grid[r][c];
81 | this.endNode = node;
82 | },
83 |
84 | getRoadCount: function(node){
85 | var count=0;
86 | this.isMarked(node, Maze.Direction.N) && count++ ;
87 | this.isMarked(node, Maze.Direction.E) && count++ ;
88 | this.isMarked(node, Maze.Direction.S) && count++ ;
89 | this.isMarked(node, Maze.Direction.W) && count++ ;
90 | return count;
91 | },
92 |
93 | setCurrent: function(node) {
94 | this.currentNode = node;
95 |
96 | this.neighbors = this.getValidNeighbors(node);
97 |
98 | if (this.neighbors && node.value === 0) {
99 | this.trace.push(node);
100 | this.onTrace(node);
101 | }
102 | },
103 | onTrace: function(node) {
104 |
105 | },
106 | moveTo: function(node, dir) {
107 | this.beforeMove(node);
108 | if (this.currentDir == dir) {
109 | this.currentDirCount++;
110 | } else {
111 | this.currentDir = dir;
112 | this.currentDirCount = 1;
113 | }
114 | this.currentNode.value |= dir;
115 | this.setCurrent(node);
116 | node.value |= Maze.Direction.opposite[dir];
117 | this.afterMove(node);
118 | },
119 | beforeMove: function(node) {
120 |
121 | },
122 | afterMove: function(node) {
123 |
124 | },
125 |
126 | generate: function() {
127 | this.beforeGenrate();
128 | this.setCurrent(this.startNode);
129 | this.stepCount = 0;
130 | while (this.nextStep()) {
131 | this.stepCount++;
132 | if (this.isOver() === true) {
133 | break;
134 | }
135 | // console.log(step);
136 | }
137 | console.log("Step Count : " + this.stepCount);
138 | this.afterGenrate();
139 | },
140 | beforeGenrate: function() {},
141 | afterGenrate: function() {},
142 |
143 | // 生成迷宫时的提前终止条件
144 | isOver: function() {},
145 |
146 | nextStep: function() {
147 | if (!this.neighbors) {
148 | this.beforeBacktrace();
149 | return this.backtrace();
150 | }
151 | var n = this.getNeighbor();
152 | this.moveTo(n[0], n[1]);
153 | this.updateCurrent();
154 | return true;
155 | },
156 | beforeBacktrace: function() {},
157 | backtrace: function() {
158 | var len = this.trace.length;
159 | while (len > 0) {
160 | var idx = this.getTraceIndex();
161 | var node = this.trace[idx];
162 | var nm = this.getValidNeighbors(node);
163 | if (nm) {
164 | this.currentNode = node;
165 | this.neighbors = nm;
166 | return true;
167 | } else {
168 | this.trace.splice(idx, 1);
169 | len--;
170 | }
171 | }
172 | return false;
173 | },
174 |
175 | setRoom: function(x, y, width, height) {
176 | var grid = this.grid;
177 | var ex = x + width;
178 | var ey = y + height;
179 |
180 | for (var r = y; r < ey; r++) {
181 | var row = grid[r];
182 | if (!row) {
183 | continue;
184 | }
185 | for (var c = x; c < ex; c++) {
186 | var node = row[c];
187 | if (node) {
188 | node.value = Maze.Direction.ALL;
189 | }
190 | }
191 | }
192 | },
193 | setBlock: function(x, y, width, height) {
194 | var grid = this.grid;
195 | var ex = x + width;
196 | var ey = y + height;
197 | for (var r = y; r < ey; r++) {
198 | var row = grid[r];
199 | if (!row) {
200 | continue;
201 | }
202 | for (var c = x; c < ex; c++) {
203 | var node = row[c]
204 | if (node) {
205 | node.value = null;
206 | }
207 | }
208 | }
209 | },
210 | /***************************************
211 | 通过重写以下几个方法, 可以实现不同的迷宫效果
212 | **************************************/
213 |
214 | getValidNeighbors: function(node) {
215 | var nList = [];
216 | var nMap = {};
217 |
218 | var c = node.x;
219 | var r = node.y;
220 | var dir, nearNode;
221 |
222 | dir = Maze.Direction.N;
223 | nearNode = r > 0 ? this.grid[r - 1][c] : null;
224 | this.isValid(nearNode, node, dir) && nList.push((nMap[dir] = [nearNode, dir]));
225 |
226 | dir = Maze.Direction.E;
227 | nearNode = this.grid[r][c + 1];
228 | this.isValid(nearNode, node, dir) && nList.push((nMap[dir] = [nearNode, dir]));
229 |
230 | dir = Maze.Direction.S;
231 | nearNode = r < this.height - 1 ? this.grid[r + 1][c] : null;
232 | this.isValid(nearNode, node, dir) && nList.push((nMap[dir] = [nearNode, dir]));
233 |
234 | dir = Maze.Direction.W;
235 | nearNode = this.grid[r][c - 1];
236 | this.isValid(nearNode, node, dir) && nList.push((nMap[dir] = [nearNode, dir]));
237 |
238 | this.updateValidNeighbors(nList, nMap);
239 |
240 | if (nList.length > 0) {
241 | nMap.list = nList;
242 | return nMap;
243 | }
244 | return null;
245 | },
246 |
247 | updateValidNeighbors: function(nList, nMap) {
248 |
249 | },
250 |
251 | isValid: function(nearNode, node, dir) {
252 | return nearNode && nearNode.value === 0;
253 | },
254 |
255 | updateCurrent: function() {
256 | if (this.alwaysBacktrace) {
257 | this.backtrace();
258 | }
259 | },
260 |
261 | getNeighbor: function() {
262 | var list = this.neighbors.list;
263 | var n = list[list.length * Math.random() >> 0];
264 | return n;
265 | },
266 |
267 | getTraceIndex: function() {
268 | var idx = this.trace.length - 1;
269 | return idx;
270 | },
271 |
272 | };
273 |
274 |
275 | Maze.Direction = {
276 | N: 1,
277 | S: 2,
278 | E: 4,
279 | W: 8,
280 | ALL: 1 | 2 | 4 | 8,
281 |
282 | opposite: {
283 | 1: 2,
284 | 2: 1,
285 | 4: 8,
286 | 8: 4
287 | },
288 | stepX: {
289 | 1: 0,
290 | 2: 0,
291 | 4: 1,
292 | 8: -1
293 | },
294 | stepY: {
295 | 1: -1,
296 | 2: 1,
297 | 4: 0,
298 | 8: 0
299 | },
300 | };
301 |
--------------------------------------------------------------------------------