├── 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 |
53 | 54 |
55 |
56 |
57 | 58 |
Perfect Maze: 迷宫中没有不可达区域, 没有循环路径, 没有"房间".
59 |
60 |
61 | 62 |
Braid Maze: 迷宫中没有不可达区域, 没有死路, 有循环路径, 没有"房间".
63 |
64 |
65 | 66 |
非Perfect Maze: 迷宫中没有不可达区域, 有死路, 有循环路径, 有"房间".
67 |
68 |
69 |
终止条件
70 |
当探索到迷宫终点E, 且探索了至少一半的区域时,终止迷宫的生成
71 |
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 | --------------------------------------------------------------------------------