本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772776
项目要求
- 随机生成一个迷宫并且求解迷宫
- 支持玩家走迷宫和系统走迷宫两种模式:玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫要求基于A*算法实现,输出走迷宫的最优路径并显示。
- 设计交互友好的游戏图形界面。
项目框架
项目实现步骤
第一步:界面总体设计
package gyt;
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("Prim迷宫");
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);
}
});
}
第二步:使用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)
生成的迷宫特点:
①起点和终点是固定的,内部路径是随机的
②每个迷宫地图只有一个一条可达终点的路径
package gyt;
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标志来控制,按空格键可以切换显示和隐藏。
package gyt;
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 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;
}
public void paint(Graphics g) {
//墙的颜色
g.setColor(Color.black);
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);
}
}
if(IsDisplay) {//只有在敲击空格键的情况下显示最佳路径
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.blue);
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) {
IsDisplay=false;}
else
IsDisplay=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, "出来了","Game Over",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方法生成最佳路径。代码其中A*部分已经注释掉。实现敲击空格键会产生自动生成的最佳路径。
DFS算法找迷宫通路:
1.从一个点向四周开始搜索,选择一个方向向下搜索
2.直到遇到墙返回上一父节点,选择其他方向搜索,不断递归,并对已经搜索过的方向进行标注以免重复搜索,并删除列表中的路径
3.如果能够到达终点,就追溯之前路径,加入到列表中
算法流程图:
算法空间、时间复杂度分析
- DFS算法空间复杂度:O(2widthheight)
因为深度搜索过程需要记录搜索到的块,且块的位置坐标x,y是分开保存的,因此空间复杂度是O(2widthheight) - DFS算法最坏情况时间复杂度:O(width*height);最好情况时间复杂度:O(width+height)
最坏情况需要遍历搜索所有块。最好情况一次就能找到通路
package gyt;
import java.util.ArrayList;
public class EMap {
private ArrayList<Integer> blockPos = 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;
public 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;//如果找到终点,直接跳出,不继续不搜索
blockPos.add(dx);blockPos.add(dy);//路径加入到列表中
a[dx][dy]=false;
dfs(dx,dy,(i+2)%4);//从新的结点继续开始搜
}
}
if(!fl) {//没找到终点,说明不通,这次尝试不对,就将不对的移除列表
blockPos.remove(blockPos.size()-1);
blockPos.remove(blockPos.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());
} */
blockPos.add(0);blockPos.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 blockPos;
}
private boolean ise(int dx,int dy) {
return (0 <= dx && dx <= width*2 && 0 <= dy && dy <= height*2);
}
}
结果图:
大致要点就是这些,详细解释可以看代码部分注释,里面写的更加仔细
项目总结分析
- 实现了通过Prim算法生成随机的迷宫。
- 实现了两种走迷宫方式:玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫要求基于A*算法实现,输出走迷宫的最优路径并显示。
心得体会
个人认为,对于这个项目,我大致还是满意的,最大的遗憾就是没有实现用A算法来完成生成系统走迷宫路径的操作,主要原因是在对A算法没有足够了解的情况下,缺少一些用A算法来生成路径的资料,使得让独立摸索写代码有一定的困难。后续我也会继续学习A算法,来完善这次的项目。
本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772776