目录
一、项目要求
- 随机生成一个迷宫并且求解迷宫
- 支持玩家走迷宫和系统走迷宫两种模式:玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫要求基于A*算法实现,输出走迷宫的最优路径并显示。
- 设计交互友好的游戏图形界面。
二、项目框架
三、项目实现步骤
第一步:界面总体设计
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class Test extends JFrame implements ActionListener, KeyListener {
boolean map[][] = new PMap().prim(2, 0, 20, 19, true);
PaintMap p = new PaintMap(map, new EMap(map).exitmap());
public Test() {//迷宫的窗口信息
this.setTitle("迷宫游戏");
this.add(p);
this.setSize(500, 500);
this.setVisible(true);
this.setLocationRelativeTo(null);
addKeyListener(this);//监听键盘
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new Test().setVisible(true);
}
});
}
public void keyPressed(KeyEvent key) {//监听键盘,上下左右空格
switch (key.getKeyCode()) {
case KeyEvent.VK_UP:
p.moveUp();
break;
case KeyEvent.VK_DOWN:
p.moveDown();
break;
case KeyEvent.VK_LEFT:
p.moveLeft();
break;
case KeyEvent.VK_RIGHT:
p.moveRight();
break;
case KeyEvent.VK_SPACE:
p.PressSp();
break;
}
}
public void keyReleased(KeyEvent arg0) {
}
public void keyTyped(KeyEvent arg0) {
}
public void actionPerformed(ActionEvent arg0) {
}
}
第二步:使用prim算法生成随机迷宫
Prim算法:
(1)初始化迷宫为墙壁全部封死状态
(2)把起点加入集合A
(3)找集合A中点周围的墙(非边界墙),选择其中任意一个判断墙两边的两个路径点是否都属于集合A,若不是则打破此墙,使新路径点加入集合A
(4)重复,直到A中包含所有路径点为止具体落实到编程上
1.让迷宫全是墙.
2.选一个单元格作为迷宫的通路,然后把它的邻墙放入列表
3.当列表里还有墙时,从列表里随机选一个墙:
(1)如果这面墙分隔的两个单元格只有一个单元格被访问过,那就从列表里移除这面墙,即把墙打通,让未访问的单元格成为迷宫的通路,再把这个格子的墙(2)如果墙两面的单元格都已经被访问过,那就从列表里移除这面墙
4.列表里已经没有墙了,结束
算法流程图:
算法空间、时间复杂度分析
Prim算法空间复杂度:O(2widthheight)
因为生成迷宫过程中需要一个数组记录每个块的所有邻块的位置,且位置的x,y是分开保存的,因此空间复杂度是O(2widthheight)
Prim算法时间复杂度:O(widthheight)
因为生成迷宫过程中需要处理所有块是墙还是通路问题,所以时间复杂度是O(widthheight)
生成的迷宫特点:
①起点和终点是固定的,内部路径是随机的
②每个迷宫地图只有一个一条可达终点的路径
import java.util.ArrayList;
import javax.swing.JPanel;
/**Prim算法:
让迷宫全都是墙。
选一个格,作为迷宫的通路,然后把它的邻墙放入列表。
当列表里还有墙时:
从列表里随机选一个墙,如果它对面的格子不是迷宫的通路:
把墙打通,让对面的格子成为迷宫的通路;
把那个格子的邻墙加入列表。
如果对面的格子已经是通路了,那就从列表里移除这面墙。
*/
//生成随机的迷宫
public class PMap extends JPanel {
public boolean[][] prim(int startX,int startY,int widthLimit,int heightLimit,boolean haveBorder){//0,1,20,19,true
final boolean block=false,unblock=true; //block表示是墙,unblock表示是可以走的路
//针对异常情况
if(widthLimit<1)//宽度最低设置为1
widthLimit=1;
if(heightLimit<1)//高度最低设置为1
heightLimit=1;
if(startX<0||startX>=widthLimit)//起点不能超出迷宫
startX=(int)Math.round(Math.random()*(widthLimit-1));//超出就在迷宫内随机选择一个作为起点
if(startY<0||startY>=heightLimit)
startY=(int)Math.round(Math.random()*(heightLimit-1));
if(!haveBorder) {
--widthLimit;
--heightLimit;
}
//迷宫尺寸换算成带墙尺寸
widthLimit*=2;
heightLimit*=2;
//迷宫起点换算成带墙起点
startX*=2;
startY*=2;
if(haveBorder) {
++startX;
++startY;
}
//初始化迷宫
boolean[][]mazeMap=new boolean [widthLimit+1][heightLimit+1];
//设置全为墙
for(int i=0;i<=widthLimit;i++)
for(int j=0;j<=heightLimit;j++)
mazeMap[i][j]=block;
mazeMap[0][1]=unblock;//入口
mazeMap[widthLimit][heightLimit-1]=unblock;//出口
ArrayList<Integer> blockPos = new ArrayList<Integer>(); //存放邻居墙的列表
int targetX=startX,targetY=startY;
mazeMap[targetX][targetY]=unblock;
//首先针对起点,将起点邻墙加入列表,和起点(0,1)点进行比较
//列表在意义上是每三个元素为一组:一个点的x坐标,一个点的y坐标,将要移动的方向
if(targetY>1) {
blockPos.add(targetX);blockPos.add(targetY-1);blockPos.add(0);//上
}
if (targetX < widthLimit)
{
blockPos.add(targetX+1);blockPos.add(targetY);blockPos.add(1);//右
}
if (targetY < heightLimit)
{
blockPos.add(targetX);blockPos.add(targetY+1);blockPos.add(2);//下
}
if (targetX > 1)
{
blockPos.add(targetX-1);blockPos.add(targetY);blockPos.add(3);//左
}
//列表不为空时
while(!blockPos.isEmpty()) {
int blockIndex=(int)Math.round(Math.random()*(blockPos.size()/3-1))*3;//选中三个一组的最开始那个,即x坐标位,+1位表示y坐标位,+2表示方向位
if(blockIndex+2<blockPos.size()) {
if(blockPos.get(blockIndex+2).equals(0)) {//+2表示方向位,如果向上,那么坐标跟着改变
targetX= blockPos.get(blockIndex);
targetY= blockPos.get(blockIndex+1)-1;
}
else if(blockPos.get(blockIndex+2).equals(1)) {
targetX= blockPos.get(blockIndex)+1;
targetY= blockPos.get(blockIndex+1);
}
else if(blockPos.get(blockIndex+2).equals(2)) {
targetX= blockPos.get(blockIndex);
targetY= blockPos.get(blockIndex+1)+1;
}
else if(blockPos.get(blockIndex+2).equals(3)) {
targetX= blockPos.get(blockIndex)-1;
targetY= blockPos.get(blockIndex+1);
}
}
//例子:口口口,最左的是起点,中间是它的邻墙,右面的则是上几行得到的,也就是“对面”的格子
if(mazeMap[targetX][targetY]==block) {//起点对面格子是墙,打通邻墙
//打通墙
if(blockIndex+1<blockPos.size())
mazeMap[blockPos.get(blockIndex)][blockPos.get(blockIndex+1)]=unblock;
else
System.out.println("error");
mazeMap[targetX][targetY]=unblock;
//然后再添加当前目标的邻墙
if (targetY > 1 && mazeMap[targetX][targetY - 1] == block && mazeMap[targetX][targetY - 2] == block)
{
blockPos.add(targetX);blockPos.add(targetY-1);blockPos.add(0);//向上情况
}
if (targetX < widthLimit -1&& mazeMap[targetX + 1][targetY] == block && mazeMap[targetX + 2][targetY] == block)
{
blockPos.add(targetX+1);blockPos.add(targetY);blockPos.add(1);//向右情况
}
if (targetY < heightLimit-1 && mazeMap[targetX][targetY + 1] == block && mazeMap[targetX][targetY + 2] == block)
{
blockPos.add(targetX);blockPos.add(targetY+1);blockPos.add(2);//向下情况
}
if (targetX > 1 && mazeMap[targetX - 1][targetY] == block && mazeMap[targetX - 1][targetY] == block)
{
blockPos.add(targetX-1);blockPos.add(targetY);blockPos.add(3);//向左情况
}
}
//对面已经是通路了,就从列表移除这面墙
for(int l=blockIndex,k=0;k<3;k++) {
blockPos.remove(l);
}
}
return mazeMap;
}
}
第三步:移动迷宫与尾迹生成
1.用户走迷宫是通过键盘监听来实现的,已经在第一部分Test类中说明。
2.尾迹生成主要包括了:
①用蓝色来显示用户自己走迷宫的路径,可以实现位置的后退,但路径不会消失;
②用红色来显示系统生成的迷宫路径,系统路径通过一个IsDisplay标志来控制,按空格键可以切换显示和隐藏。
import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class PaintMap extends JPanel {
final int unitSize = 10;//单位大小
private int width;//宽
private int height;//高
private int startX;//开始点
private int startY;
private boolean block;
private boolean b[][];
private boolean IsDisplay;
private boolean flag;
private ArrayList<Integer> ToExit = new ArrayList<Integer>(); //通向终点的路径的结点组成的列表
public PaintMap(boolean b[][], ArrayList<Integer> a) {//初始化
ToExit = a;
this.b = b;
width = b.length;
height = b[0].length;
startX = 0; //初始位置
startY = 1;
block = true;//初始化全是墙
IsDisplay = false;
flag = true;
}
public void paint(Graphics g) {
//墙的颜色
g.setColor(Color.gray);
for (int i = 0; i < width; i++)
for (int j = 0; j < height; j++)
if (!b[i][j]) {
g.fill3DRect(30 + i * unitSize, 30 + j * unitSize, unitSize, unitSize, true);
//参数依次表示为:要填充矩形的x坐标、y坐标、要填充矩形的宽度、高度
}
//出口路线颜色:红色
if (IsDisplay) {//只有在敲击空格键的情况下显示最佳路径
g.setColor(Color.red);
for (int i = 0; i < ToExit.size(); i += 2) {//路径横纵坐标依次存储在ToExit中,所以间隔加2
g.fill3DRect(30 + ToExit.get(i) * unitSize, 30 + ToExit.get(i + 1) * unitSize, unitSize, unitSize, true);
flag = true;
}
}
//白色
if (!flag) {//只有在敲击空格键的情况下显示最佳路径
g.setColor(Color.white);
for (int j = 0; j < ToExit.size(); j += 2) {//路径横纵坐标依次存储在ToExit中,所以间隔加2
g.fill3DRect(30 + ToExit.get(j) * unitSize, 30 + ToExit.get(j + 1) * unitSize, unitSize, unitSize, true);
}
}
//控制格子颜色:紫色
g.setColor(Color.pink);
if (IsEdge(startX, startY)) {
g.fill3DRect(30 + startX * unitSize, 30 + startY * unitSize, unitSize, unitSize, true);
} else {
g.fill3DRect(30 + unitSize, 30, unitSize, unitSize, true);
}
}
//本程序中使用的坐标轴为:以左上角顶点为原点,向右为x轴正方向,向下为y轴正方向
public void moveUp() {//向上移动
startY -= 1;//坐标先改变
if (IsEdge(startX, startY)) {//如果这个点在迷宫边界或超出迷宫范围,将改变还原
if (!b[startX][startY]) {
block = false;
startY += 1;
}
if (block)
repaint();
else
block = true;
Win(startX, startY);
} else
startY += 1;
}
public void moveDown() {
startY += 1;
if (IsEdge(startX, startY)) {
if (!b[startX][startY]) {
block = false;
startY -= 1;
}
if (block)
repaint();
else
block = true;
Win(startX, startY);
} else
startY -= 1;
}
public void moveLeft() {
startX -= 1;
if (IsEdge(startX, startY)) {
if (!b[startX][startY]) {
block = false;
startX += 1;
}
if (block)
repaint();
else
block = true;
Win(startX, startY);
} else
startX += 1;
}
public void moveRight() {
startX += 1;
if (IsEdge(startX, startY)) {
if (!b[startX][startY]) {
block = false;
startX -= 1;
}
if (block)
repaint();
else
block = true;
Win(startX, startY);
} else
startX -= 1;
}
public void PressSp() {//点击空格显示路径
if (IsDisplay && flag) {
IsDisplay = false;
flag = false;
} else {
IsDisplay = true;
flag = true;
repaint();
}
}
private boolean IsEdge(int x, int y) {//判断是否在迷宫内,在就返回true
return (x < width && y < height && x >= 0 && y >= 0);
}
private void Win(int x, int y) {//判断赢没赢游戏
if (x == width - 1 && y == height - 2) {//终点,是固定不变的
Object[] options = {"再潇洒一回吗?", "不潇洒了,拜拜!"};
int response = JOptionPane.showOptionDialog(this, "您真棒!", "挑战成功", JOptionPane.YES_OPTION, JOptionPane.PLAIN_MESSAGE, null,
options, options[0]);
if (response == 0) {//选再来一局的话
b = new PMap().prim(0, 0, (width - 1) / 2, (height - 1) / 2, true);
ToExit = new EMap(b).exitmap();
startX = 0;
startY = 1;
block = true;
IsDisplay = false;
repaint();
} else //选择退出
System.exit(0);
}
}
}
第四步: DFS算法实现迷宫自动寻路
尝试过使用A*的方法,但是没有运行处理想的结果,在这里也简单说明一下
A 算法:*
*G表示从起点到当前顶点n的实际距离;
*H表示当前顶点n到目标顶点的估算距离(根据所采用的评估函数的不同而变化);
*F=G+H,代表了该节点的综合预估值,值越小,到达目标的成本就越小,所以访问的时候尽量优先考虑最小的。
/**路径搜索过程如下:
* 1.首先需要创建两个集合,一个存储待访问的节点(openlist),一个存储已经访问过的节点(closelist)
* 2.添加起点openlist列表,并且计算该点的预估值。
* 3.查找openlist里预估值最小的节点,作为当前访问的节点,并且从openlist删除该节点。
* 4.获取当前节点的邻居节点,计算出他们的预估值 并且添加到openlist列表中。
* 5.把当前节点添加到closelist中,代表已经访问过了。
* 6.重复以上步骤3-5,直到找到目标节点位置为止。
* 7.循环输出最终节点的父节点,就是我们需要的路径了。
*
* 具体操作过程:
* 对当前方格的 8 个相邻方格的每一个方格:如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作:
* (1)如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。
* (2)如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。
*/
最终改用dfs方法生成最佳路径。
DFS算法找迷宫通路:
1.从一个点向四周开始搜索,选择一个方向向下搜索
2.直到遇到墙返回上一父节点,选择其他方向搜索,不断递归,并对已经搜索过的方向进行标注以免重复搜索,并删除列表中的路径
3.如果能够到达终点,就追溯之前路径,加入到列表中
算法流程图:
算法空间、时间复杂度分析
1.DFS算法空间复杂度:O(2widthheight)
因为深度搜索过程需要记录搜索到的块,且块的位置坐标x,y是分开保存的,因此空间复杂度是O(2widthheight)
2.DFS算法最坏情况时间复杂度:O(width*height);最好情况时间复杂度:O(width+height)
最坏情况需要遍历搜索所有块。最好情况一次就能找到通路
import java.util.ArrayList;
public class EMap {
private ArrayList<Integer> list = new ArrayList<Integer>();
private int d[][] = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};//下 右 上 左
private boolean a[][];
private int width;
private int height;
private boolean fl = false;
EMap(boolean b[][]) {//b是PMap.java用Prim算法生成的迷宫,复制到a中,使得不修改b中迷宫
width = (b.length - 1) / 2;
height = (b[0].length - 1) / 2;
a = new boolean[b.length][b[0].length];
for (int i = 0; i < b.length; i++)
for (int j = 0; j < b[0].length; j++)
a[i][j] = b[i][j];
}
/**
* 深度优先搜索找迷宫通路
* 1.从一个点向四周开始搜索,选择一个方向向下搜索
* 2.直到遇到墙返回上一父节点,选择其他方向搜索,不断递归,并对已经搜索过的方向进行标注以免重复搜索,并删除列表中的路径
* 3.如果能够到达终点,就追溯之前路径,加入到列表中
*/
private void dfs(int x, int y, int c) {//c表示不能走的方向;
if (x == (width * 2) && y == (height * 2 - 1)) {//找到终点,fl设为true
fl = true;
return;
}
for (int i = 0; i < 4; i++) {//从一个点向四周开始搜索
if (c == i)
continue;//c表示你从一个方向搜过来的,就不要再重新搜回去了
int dx = x + d[i][0];
int dy = y + d[i][1];
if (ise(dx, dy) && a[dx][dy]) {
if (fl)
break;//如果找到终点,直接跳出,不继续不搜索
list.add(dx);
list.add(dy);//路径加入到列表中
a[dx][dy] = false;
dfs(dx, dy, (i + 2) % 4);//从新的结点继续开始搜
}
}
if (!fl) {//没找到终点,说明不通,这次尝试不对,就将不对的移除列表
list.remove(list.size() - 1);
list.remove(list.size() - 1);
}
}
//private void Astar(){
//}
public ArrayList<Integer> exitmap() {
/* System.out.println(a.length);
System.out.println(a[0].length);
Solver solver = new AStarSolver(a);
ArrayList<Point> solution = solver.getSolution();
for(Point p : solution){
blockPos.add(p.getY());
blockPos.add(p.getX());
} */
list.add(0);
list.add(1);
dfs(0, 1, 3);//默认从(0,1)开始,不能走的方向为向下
// for(int i=0;i<a.length;i++)
// {
// for(int j=0;j<a[0].length;j++) {
// System.out.print(a[i][j]+" ");
// }
// System.out.print("\n");
// }
return list;
}
private boolean ise(int dx, int dy) {
return (0 <= dx && dx <= width * 2 && 0 <= dy && dy <= height * 2);
}
}