写在前面

算法设计与分析课程大作业为实现一个迷宫游戏,我觉得挺有意思的,于是做一下记录。

java的图形化编程没怎么学,我打算采用js来实现,使用网页编程的好处是比较轻量,并且自由度比较高,也挺适合这类小游戏的,但最主要的原因还是java图形化编程不太会。

前期准备

首先引入jq简化开发,生成迷宫的基本思路是外面套一层div作为包裹,中间再包裹上小div,当然外层div拥有边框,内部的小div就需要对边框进行处理:

  • 所有div加上右边和下边的边框
  • 最后一行去除下边框
  • 最后一列去除右边框

html结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<body>
<div class="maze-outer">
<div class="first-grid">
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
</div>
<div class="first-grid">
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
</div>
<div class="first-grid">
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
</div>
<div class="first-grid">
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
</div>
<div class="first-grid">
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
<div class="second-grid"></div>
</div>
</div>
<div class="message">游戏成功!</div>
</body>

样式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<style>
* {
margin: 0;
padding: 0;
}
.maze-outer {
margin: 10px auto;
height: 700px;
width: 700px;
border: 1px solid black;
}
.first-grid {
width: 100%;
height: 140px;
display: flex;
}
.second-grid {
width: 140px;
height: 100%;
background-color: gold;
border-right: 1px solid black;
border-bottom: 1px solid black;
box-sizing: border-box;
}
.first-grid:last-child .second-grid {
border-bottom: none;
}
.second-grid:last-child {
border-right: none;
}
.none-right {
border-right: none;
}
.none-bottom {
border-bottom: none;
}
.message {
position: fixed;
top: 25px;
left: 50%;
transform: translateX(-50%);
font-size: 45px;
display: none;
}
</style>

最终效果如下:

img

引入人物样式:

1
2
3
4
5
<style>
.wolfman {
background-color: red;
}
</style>

最终效果如下:

img

准备全局变量:

1
2
3
4
5
6
7
8
9
10
11
//迷宫墙壁
let mazeOuter = $(".maze-outer");
//消息提示
let message = $(".message");
//迷宫数组
let maze = [];
//迷宫格子数
let n = 10;
//当前人物位置
let x = 0;
let y = 0;

以上基本的准备工作已经完成,下面介绍迷宫生成算法。

迷宫生成算法

递归回溯算法

算法思想

每次把新找到的未访问迷宫单元作为优先,寻找其相邻的未访问过的迷宫单元,直到所有的单元都被访问到。通俗的说,就是从起点开始随机走,走不通了就返回上一步,从下一个能走的地方再开始随机走。

算法步骤

1
2
3
4
5
6
7
8
9
10
1.将起点作为当前迷宫单元并标记为已访问
2.当还存在未标记的迷宫单元,进行循环
1.如果当前迷宫单元有未被访问过的的相邻的迷宫单元
1.随机选择一个未访问的相邻迷宫单元
2.将当前迷宫单元入栈
3.移除当前迷宫单元与相邻迷宫单元的墙
4.标记相邻迷宫单元并用它作为当前迷宫单元
2.如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
1.栈顶的迷宫单元出栈
2.令其成为当前迷宫单元

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//深度遍历初始化迷宫
function initMazeByDfs() {
let stack = [];
//初始化数组
initMazeArray();
maze[0][0].visited = true;
let i = 0, j = 0;
//初始化迷宫
while (isAllVisited(maze)) {
let unVisited = [];
//判断上一个单元格
if (i - 1 >= 0 && maze[i - 1][j].visited === false) {
unVisited.push(maze[i - 1][j]);
}
//判断右一个单元格
if (j + 1 < n && maze[i][j + 1].visited === false) {
unVisited.push(maze[i][j + 1]);
}
//判断下一个单元格
if (i + 1 < n && maze[i + 1][j].visited === false) {
unVisited.push(maze[i + 1][j]);
}
//判断左一个单元格
if (j - 1 >= 0 && maze[i][j - 1].visited === false) {
unVisited.push(maze[i][j - 1]);
}
if (unVisited.length !== 0) {
let r = getRandomGrid(unVisited);
r.visited = true;
stack.push(maze[i][j]);
removeWall(maze[i][j], r);
i = r.i;
j = r.j;
} else if (stack.length !== 0) {
let r = stack.pop();
i = r.i;
j = r.j;
}
}
}

其中所用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//初始化数组
function initMazeArray() {
for (let i = 0; i < n; i++) {
let m = [];
for (let j = 0; j < n; j++) {
let item = {class: "second-grid", visited: false, i: i, j: j};
m.push(item);
}
maze.push(m);
}
}
//初始化全局maze数组,构造对象加入数组
//其中class代表类名,visited表示该单元格是否已访问,i,j代表该单元格位置。
//判断是否还有剩余单元格未访问
function isAllVisited(maze) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (!maze[i][j].visited) {
return true;
}
}
}
return false;
}
//获取随机元素
function getRandomGrid(unVisited) {
let index = Math.floor(Math.random() * unVisited.length);
return unVisited[index];
}
//在未访问的元素集合中随机返回一个元素
//移除两个格子间的墙壁
function removeWall(cur, nei) {
//nei在cur的上部
if (cur.i - 1 >= 0 && cur.i - 1 === nei.i) {
nei.class += " none-bottom";
}
//nei在cur的右部
if (cur.j + 1 < n && cur.j + 1 === nei.j) {
cur.class += " none-right";
}
//nei在cur的下部
if (cur.i + 1 < n && cur.i + 1 === nei.i) {
cur.class += " none-bottom";
}
//nei在cur的左部
if (cur.j - 1 >= 0 && cur.j - 1 === nei.j) {
nei.class += " none-right";
}
}
//其中cur参数表示当前格子,nei表示与其邻近的格子,两种格子的位置关系分为以下四种:

img

注意判断边界条件。

生成效果

img

深度优先法生成的迷宫极度扭曲,有着一条明显的主路。

随机prim算法

算法思想

类似图论中的prim算法,随机prim算法将单元格当作节点,墙作为边,可生成从起点到终点的路径。

算法步骤

1
2
3
4
5
6
7
1.让迷宫全是墙.
2.选一个单元格作为迷宫的通路,然后把它的邻墙放入列表
3.当列表里还有墙时
1.从列表里随机选一个墙,如果这面墙分隔的两个单元格只有一个单元格被访问过
1.那就从列表里移除这面墙,即把墙打通,让未访问的单元格成为迷宫的通路
2.把这个格子的墙加入列表
2.如果墙两面的单元格都已经被访问过,那就从列表里移除这面墙

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function initMazeByRandomPrim() {
//初始化数组
initMazeArray();
//初始化墙
let list = [];
maze[0][0].visited = true;
addWallToList(list, maze[0][0]);
while (list.length !== 0) {
let index = Math.floor(Math.random() * list.length);
let w = list[index];
if (w.type === "right" && w.j + 1 < n) {
if ((maze[w.i][w.j].visited ^ maze[w.i][w.j + 1].visited)) {
maze[w.i][w.j + 1].visited = true;
maze[w.i][w.j].class += " none-right";
addWallToList(list, maze[w.i][w.j + 1]);
} else if (maze[w.i][w.j].visited && maze[w.i][w.j + 1].visited) {
list.splice(index, 1);
}

} else if (w.type === "bottom" && w.i + 1 < n) {
if ((maze[w.i][w.j].visited ^ maze[w.i + 1][w.j].visited)) {
maze[w.i + 1][w.j].visited = true;
maze[w.i][w.j].class += " none-bottom";
addWallToList(list, maze[w.i + 1][w.j]);
} else if (maze[w.i][w.j].visited && maze[w.i + 1][w.j].visited) {
list.splice(index, 1);
}
}
}
}

生成效果

img

相对于深度优先的算法,Prim随机算法不是优先选择最近选中的单元格,而是随机的从所有的列表中的单元格进行选择,新加入的单元格和旧加入的单元格同样概率会被选择,新加入的单元格没有有优先权。因此其分支更多,生成的迷宫更复杂,难度更大,也更自然。

区域划分算法

算法思想

把空间用十字分成四个子空间,然后在三面墙上挖洞(为了确保连通),之后对每个子空间继续做这件事直到空间不足以继续分割为止。

img

算法步骤

1
2
3
4
5
1.初始化数组没有墙
2.判断数组是否满足条件,不满足则返回
1.随机选择一行和一列加入墙,并挖洞
2.将数组划分为四块
3.递归绘制每一块

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//基于分治法初始化迷宫
function initMazeByDivideAndConquer() {
//初始化数组
initMazeArrayInDivideAndConquer();
//递归生成迷宫
initMazeByRecursion(0, n - 2, 0, n - 2);
}

//区域划分算法
function initMazeByRecursion(xStart, xEnd, yStart, yEnd) {
if (xStart >= xEnd || yStart >= yEnd) {
return;
}
let row = getRandom(xStart, xEnd);
let col = getRandom(yStart, yEnd);
drawLine(xStart, xEnd, yStart, yEnd, row, col);
//左上
initMazeByRecursion(xStart, row - 1, yStart, col - 1);
//右上
initMazeByRecursion(xStart, row - 1, col + 1, yEnd);
//右下
initMazeByRecursion(row + 1, xEnd, col + 1, yEnd);
//左下
initMazeByRecursion(row + 1, xEnd, yStart, col - 1);
}

其中所用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//画线
function drawLine(xStart, xEnd, yStart, yEnd, row, col) {
//上
let r1 = getRandom(xStart, row);
//右
let r2 = getRandom(col + 1, yEnd);
//下
let r3 = getRandom(row + 1, xEnd);
//左
let r4 = getRandom(yStart, col);
for (let i = xStart; i < xEnd + 2; i++) {
for (let j = yStart; j < yEnd + 2; j++) {
if (i === row && j !== r2 && j !== r4) {
maze[i][j].class = maze[i][j].class.replace(/none-bottom/, "");
}
if (j === col && i !== r1 && i !== r3) {
maze[i][j].class = maze[i][j].class.replace(/none-right/, "1");
}
}
}
}
//获取[n,m]间的随机数
function getRandom(n, m) {
return Math.floor(Math.random() * (m - n) + n + 0.5);
}

区域划分算法代码实现有一些小bug😥😥😥,有时生成出来的迷宫没有完全连通,有个别单元格是封闭的…

生成效果

img

区域划分算法生成的迷宫有很明显的直路,这是因为在生成过程中,墙壁是从大到小生成,并且通路是通过挖洞来实现的。

人物移动实现

通过以上算法,我们实现了迷宫的生成,接下来我们简单实现一下游戏交互功能,基本思路为:

  • 监听键盘事件
  • 移动当前人物位置

键盘监听

注册键盘监听事件:

1
$(window).bind('keydown', listenKeyDown);

其中所用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//监听键盘事件
function listenKeyDown(event) {
let type = event.originalEvent.code;
switch (type) {
case "ArrowUp":
if (x - 1 >= 0 && getCubePosition(x - 1, y).className.indexOf("none-bottom") > 0) {
removeCubePosition(x, y);
changeCubePosition(--x, y);
}
break;
case "ArrowRight":
if (y + 1 < n && getCubePosition(x, y).className.indexOf("none-right") > 0) {
removeCubePosition(x, y);
changeCubePosition(x, ++y);
}
break;
case "ArrowDown":
if (x + 1 < n && getCubePosition(x, y).className.indexOf("none-bottom") > 0) {
removeCubePosition(x, y);
changeCubePosition(++x, y);
}
break;
case "ArrowLeft":
if (y - 1 >= 0 && getCubePosition(x, y - 1).className.indexOf("none-right") > 0) {
removeCubePosition(x, y);
changeCubePosition(x, --y);
}
break;
default:
break;
}
}

其中listenKeyDown函数需要注意移动时判断边界条件:

  • 是否有墙壁
  • 是否移动到迷宫外
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//根据元素i,j获取元素
function getCubePosition(i, j) {
if (i < 0 || j < 0 || i >= n || j >= n) {
return;
}
let firstChildren = mazeOuter.children();
let secondChildren = $(firstChildren[i]).children();
return secondChildren[j];
}
//删除指定位置人物轨迹
function removeCubePosition(i, j) {
if (i < 0 || j < 0 || i >= n || j >= n) {
return;
}
let firstChildren = mazeOuter.children();
//将原来的标记删除
$($(firstChildren[x]).children()[y]).removeClass("wolfman");
}
//改变人物位置
function changeCubePosition(i, j) {
if (i < 0 || j < 0 || i >= n || j >= n) {
return;
}
let firstChildren = mazeOuter.children();
//添加新的标记
$($(firstChildren[i]).children()[j]).addClass("wolfman");
if (i === n - 1 && j === n - 1) {
message.fadeIn();
}
}

人物移动

人物移动我们只需要改变移动位置的颜色即可,并不需要实现真实的位置移动。

以上代码即可实现通过上下左右键控制人物的移动了!🤗🤗🤗

迷宫路径搜索

接下来我们实现迷宫路径的自动寻路,一般有两种方式,深度优先搜索和广度优先搜索。

深度优先搜索

算法思想

依次向上右下左进行递归,直到没有路,则回退。

算法步骤

1
2
3
1.判断是否超出边界,超出则回退
2.标记当前位置已访问并加入记录路径
3.按上右下左依次判断是否能访问,能则继续递归

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//深度搜索自动寻路
function getShortestPathDFS(start, end) {
let path = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
maze[i][j].visited = false;
}
}
dfs(maze[start][end], path);
let s = 0;
let timer = setInterval(function () {
if (s === path.length || (path[s].i === n - 1 && path[s].j === n - 1)) {
clearInterval(timer);
return;
}
s++;
// removeCubePosition(x, y);
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);
}, 20);
}
//深度搜索自动寻路
function dfs(start, path) {
if (start.i < 0 || start.j < 0 || start.i >= n || start.j >= n) {
return;
}
start.visited = true;
path.push(start);
//上
if (start.i - 1 >= 0 && !maze[start.i - 1][start.j].visited && getCubePosition(start.i - 1, start.j).className.indexOf("none-bottom") > 0) {
dfs(maze[start.i - 1][start.j], path);
}
//右
if (start.j + 1 < n && !maze[start.i][start.j + 1].visited && getCubePosition(start.i, start.j).className.indexOf("none-right") > 0) {
dfs(maze[start.i][start.j + 1], path);
}
//下
if (start.i + 1 < n && !maze[start.i + 1][start.j].visited && getCubePosition(start.i, start.j).className.indexOf("none-bottom") > 0) {
dfs(maze[start.i + 1][start.j], path);
}
//左
if (start.j - 1 >= 0 && !maze[start.i][start.j - 1].visited && getCubePosition(start.i, start.j - 1).className.indexOf("none-right") > 0) {
dfs(maze[start.i][start.j - 1], path);
}
}

注意判断边界条件。

最终效果

img

img

img

观察可以看到,深度优先搜索和代码的关联度比较大,我们深度遍历的次序是上->右->下->左,那么最终形成的路径就会优先遍历上右部分,我们还可以观察到,深度优先搜索到达终点后是可以提前结束的。

回溯记录路径

算法思想

以上代码可以实现,深度优先遍历查找终点,我们可以使用回溯法找到的路径。具体思想为,如果当前单元格不是终点并且周围单元格都无法访问(包含不能访问和已经访问过),则表明该单元格不是路径中的单元格,将其移出结果数组即可。

因为回溯找到一条路径就直接退出了,那么这条路径不能保证是最短路径,后面的广度优先搜索会讲到如何求得最短路径。

算法步骤

1
2
3
4
5
1.判断递归条件(是否越界或是否已找到终点)是否满足,满足则返回
2.将该单元格标记为已访问并入栈
3.判断该单元格是否为终点,是则标记已找到终点
4.判断上右下左是否能访问,能则继续递归
5.判断条件(栈不为空且未找到终点),条件满足则出栈

具体实现

将深度优先搜索的代码修改一下即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//深度搜索自动寻路
function dfs(start, path) {
if (start.i < 0 || start.j < 0 || start.i >= n || start.j >= n || dfsFindPathFlag) {
return;
}
start.visited = true;
path.push(start);
if (start.i === n - 1 && start.j === n - 1) {
dfsFindPathFlag = true;
return;
}
//上
if (start.i - 1 >= 0 && !maze[start.i - 1][start.j].visited && getCubePosition(start.i - 1, start.j).className.indexOf("none-bottom") > 0) {
dfs(maze[start.i - 1][start.j], path);
}
//右
if (start.j + 1 < n && !maze[start.i][start.j + 1].visited && getCubePosition(start.i, start.j).className.indexOf("none-right") > 0) {
dfs(maze[start.i][start.j + 1], path);
}
//下
if (start.i + 1 < n && !maze[start.i + 1][start.j].visited && getCubePosition(start.i, start.j).className.indexOf("none-bottom") > 0) {
dfs(maze[start.i + 1][start.j], path);
}
//左
if (start.j - 1 >= 0 && !maze[start.i][start.j - 1].visited && getCubePosition(start.i, start.j - 1).className.indexOf("none-right") > 0) {
dfs(maze[start.i][start.j - 1], path);
}
if (path.length !== 0 && !dfsFindPathFlag) {
path.pop();
}
}
//深度搜索自动寻路
function getShortestPathDFS(start, end) {
let path = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
maze[i][j].visited = false;
}
}
dfs(maze[start][end], path);
let s = 0;
// removeCubePosition(x, y);
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);
timer = setInterval(function () {
if (s === path.length || (path[s].i === n - 1 && path[s].j === n - 1)) {
clearInterval(timer);
return;
}
s++;
// removeCubePosition(x, y);
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);
}, 50);
}

其中dfsFindPathFlag为全局标志遍历,记录是否找到终点。

最终效果

img

广度优先搜索

算法思想

一层一层向外扩张。

算法步骤

1
2
3
4
1.初始队列为空,将第一个单元格加入队列
2.当队列不为空时
1.出队,置该单元格为已访问并加入记录路径
2.上右下左判断其相邻单元格是否可访问,若能,则入队

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//广度搜索自动寻路
function getShortestPathBFS(start, end) {
let queue = [];
let path = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
maze[i][j].visited = false;
}
}
queue.push(maze[start][end]);
while (queue.length !== 0) {
let q = queue.shift();
path.push(q);
q.visited = true;
//上
if (q.i - 1 >= 0 && !maze[q.i - 1][q.j].visited && getCubePosition(q.i - 1, q.j).className.indexOf("none-bottom") > 0) {
queue.push(maze[q.i - 1][q.j]);
}
//右
if (q.j + 1 < n && !maze[q.i][q.j + 1].visited && getCubePosition(q.i, q.j).className.indexOf("none-right") > 0) {
queue.push(maze[q.i][q.j + 1]);
}
//下
if (q.i + 1 < n && !maze[q.i + 1][q.j].visited && getCubePosition(q.i, q.j).className.indexOf("none-bottom") > 0) {
queue.push(maze[q.i + 1][q.j]);
}
//左
if (q.j - 1 >= 0 && !maze[q.i][q.j - 1].visited && getCubePosition(q.i, q.j - 1).className.indexOf("none-right") > 0) {
queue.push(maze[q.i][q.j - 1]);
}
}
let s = 0;
let timer = setInterval(function () {
if (s === path.length || (path[s].i === n - 1 && path[s].j === n - 1)) {
clearInterval(timer);
return;
}
s++;
// removeCubePosition(x, y);
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);

}, 1);

}

最终效果

img

img

img

观察可以得到,广度优先搜索类似军队,步步为营,一层一层向外扩张的。其优点是可以找到起点到终点的最短路径。

广度最短路径

算法思想

广度优先是一层一层向外扩张,我们可以画个图来表示一下:

img

我们可以看到,从起点到终点,我将每一层的数字的标记起来。那么我们可以通过层数从终点来回溯推出最短路径。满足最短路径的条件有:

  • 当前单元格与邻近单元格互通(即没有墙)
  • 当前单元格层数大于邻近单元格层数

第二点需要说明一下,首先层序遍历的定义决定了起点单元格的层数必定是最小的,我们从终点沿着小于当前单元格层数的路径,肯定能找到起点

算法步骤

1
2
3
4
5
6
1.使用广度优先搜索求的所有路径及其层数,根据此结果集求最短路径
2.将终点加入结果数组中(这里数组添加采用头插)
3.遍历结果集
1.结果数组下标为0赋值为当前单元格
2.当前结果集遍历为目标单元格
3.判断当前单元格与目标单元格是否满足(互通且当前单元格层数大于目标单元格层数),能则加入结果数组

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//广度搜索自动寻路
function getShortestPathBFS(start, end) {
let queue = [];
let path = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
maze[i][j].visited = false;
}
}
let layer = 1;
maze[start][end].layer = layer++;
queue.push(maze[start][end]);
path.push(maze[start][end]);
while (queue.length !== 0) {
for (let i = queue.length; i > 0; i--) {
let q = queue.shift();
path.push(q);
q.visited = true;
//上
if (q.i - 1 >= 0 && !maze[q.i - 1][q.j].visited && getCubePosition(q.i - 1, q.j).className.indexOf("none-bottom") > 0) {
maze[q.i - 1][q.j].layer = layer;
queue.push(maze[q.i - 1][q.j]);
}
//右
if (q.j + 1 < n && !maze[q.i][q.j + 1].visited && getCubePosition(q.i, q.j).className.indexOf("none-right") > 0) {
maze[q.i][q.j + 1].layer = layer;
queue.push(maze[q.i][q.j + 1]);
}
//下
if (q.i + 1 < n && !maze[q.i + 1][q.j].visited && getCubePosition(q.i, q.j).className.indexOf("none-bottom") > 0) {
maze[q.i + 1][q.j].layer = layer;
queue.push(maze[q.i + 1][q.j]);
}
//左
if (q.j - 1 >= 0 && !maze[q.i][q.j - 1].visited && getCubePosition(q.i, q.j - 1).className.indexOf("none-right") > 0) {
maze[q.i][q.j - 1].layer = layer;
queue.push(maze[q.i][q.j - 1]);
}
}
layer++;
}
let s = 0;
// path = getShortestPath(path);
// removeCubePosition(x, y);
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);
timer = setInterval(function () {
if (s >= path.length || (path[s].i === n - 1 && path[s].j === n - 1)) {
clearInterval(timer);
return;
}
s++;
// removeCubePosition(x, y);
if (s >= path.length) {
clearInterval(timer);
return;
}
x = path[s].i;
y = path[s].j;
changeCubePosition(x, y);
}, 50);

}
//根据广度搜索结果集获取最短路径
function getShortestPath(path) {
let result = [];
result.push(maze[n - 1][n - 1]);
for (let i = path.length - 2; i >= 0; i--) {
let cur = result[0];
let nei = path[i];
if (reachable(cur, nei) && cur.layer > nei.layer) {
result.unshift(nei);
}
}
return result;
}
//判断cur和nei是否互通
function reachable(cur, nei) {
//nei在cur的上部
if (cur.i - 1 >= 0 && cur.i - 1 === nei.i && cur.j === nei.j) {
return nei.class.indexOf("none-bottom") >= 0;
}
//nei在cur的右部
if (cur.j + 1 < n && cur.j + 1 === nei.j && cur.i === nei.i) {
return cur.class.indexOf("none-right") >= 0;

}
//nei在cur的下部
if (cur.i + 1 < n && cur.i + 1 === nei.i && cur.j === nei.j) {
return cur.class.indexOf("none-bottom") >= 0;
}
//nei在cur的左部
if (cur.j - 1 >= 0 && cur.j - 1 === nei.j && cur.i === nei.i) {
return nei.class.indexOf("none-right") >= 0;
}
return false;
}

最终效果

img

观察上图我们发现,路径中的数字是连起来的,说明已是最短路径。

两种路径对比

拿同一个迷宫距离,回溯法求得的路径为:

img

而通过广度优先求解出的最短路径为:

img

对比可以发现回溯法求解出来的路径层数是可能有重复,例如上图中的15,16,而广度优先求解出来的是没有重复的,即使最短。因为层数是不可能的跳跃的,这是由广度优先算法的定义所决定的。

游戏整体交互

为了使游戏整体的交互性更强,我们增加一些操作面板供玩家选择。其中包括:

  • 迷宫生成算法的选择
  • 自动寻路算法的选择
  • 开始游戏选择
  • 游戏成功提示
  • 游戏操作

最终效果如下:

img

完整的代码已经放在码云上,有需要的可以参考:迷宫游戏

参考文章: