项目三 基于A*搜索算法迷宫游戏开发

目录

项目要求

项目框架

项目实现步骤

第一步:界面总体设计

第二步:使用prim算法生成随机迷宫


一、项目要求

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

二、项目框架

在这里插入图片描述

三、项目实现步骤

第一步:界面总体设计

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);
    }

}

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值