软件实习项目三:基于A*搜索算法迷宫游戏开发


本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772776

项目要求

  1. 随机生成一个迷宫并且求解迷宫
  2. 支持玩家走迷宫和系统走迷宫两种模式:玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫要求基于A*算法实现,输出走迷宫的最优路径并显示。
  3. 设计交互友好的游戏图形界面。

项目框架

在这里插入图片描述

项目实现步骤

第一步:界面总体设计

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(width
height)

生成的迷宫特点:
①起点和终点是固定的,内部路径是随机的
②每个迷宫地图只有一个一条可达终点的路径

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.如果能够到达终点,就追溯之前路径,加入到列表中

算法流程图:
在这里插入图片描述
算法空间、时间复杂度分析

  1. DFS算法空间复杂度:O(2widthheight)
    因为深度搜索过程需要记录搜索到的块,且块的位置坐标x,y是分开保存的,因此空间复杂度是O(2widthheight)
  2. 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);
	}
	
}

结果图:
在这里插入图片描述
大致要点就是这些,详细解释可以看代码部分注释,里面写的更加仔细

项目总结分析

  1. 实现了通过Prim算法生成随机的迷宫。
  2. 实现了两种走迷宫方式:玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫要求基于A*算法实现,输出走迷宫的最优路径并显示。

心得体会

个人认为,对于这个项目,我大致还是满意的,最大的遗憾就是没有实现用A算法来完成生成系统走迷宫路径的操作,主要原因是在对A算法没有足够了解的情况下,缺少一些用A算法来生成路径的资料,使得让独立摸索写代码有一定的困难。后续我也会继续学习A算法,来完善这次的项目。

本项目配套代码链接:
https://blog.csdn.net/qq_50944418/article/details/111772776

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于A*算法的迷宫小游戏开发,可以让玩家面对迷宫的挑战,通过智慧和策略找到迷宫的出口。 首先,我们需要设计一个迷宫地图。可以采用多种方式生成迷宫地图,如随机生成、手动设计或者使用迷宫生成算法。迷宫地图由起点、终点以及迷宫墙壁组成。 接下来,我们使用A*算法来寻找最佳路径。A*算法是一种启发式搜索算法,通过估计每个节点到目标点的距离来决定搜索方向。在实现A*算法时,需要定义一个启发函数来评估节点的价值,以便选择最优的路径。在该游戏中,可以使用曼哈顿距离或欧几里得距离作为启发函数。 当玩家开始游戏后,可以使用方向键或鼠标来控制角色移动。同时,在游戏界面上显示迷宫地图和玩家的当前位置。 在实现A*算法时,需要考虑一些特殊情况。比如,如何处理墙壁、如何处理无法到达的位置等。可以采用合适的数据结构,如优先队列或堆栈,来实现算法的搜索和路径的存储。 最后,为了增加游戏的趣味性和挑战性,可以在迷宫中添加一些道具或陷阱,用来干扰玩家的寻路过程。比如,道具可以提供额外的移动能力,而陷阱则会减慢玩家的速度。 通过以上方法,基于A*算法的迷宫小游戏可以提供给玩家一个有趣而挑战的寻路体验。同时,这个游戏也可以帮助玩家锻炼逻辑思维和空间认知能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值