一、项目要求
(1)迷宫游戏是非常经典的游戏,在该题中要求随机生成一个迷宫,并求解迷宫。
(2)要求游戏支持玩家走迷宫,和系统走迷宫路径两种模式。玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫路径要求基于A*算法实现,输出走迷宫的最优路径并显示。
(3)设计交互友好的游戏图形界面。
二.项目平台
IDEA.Java Swing
三.项目实现过程
1.深度优先算法生成迷宫:
(1)选择一个靠近边缘的1作为起点,在它的周围随机找另一个黄色的1(这里的“周围”指的是上下左右4个方向)。找到就把他们联通,并且把两个1之间的0(灰色墙)也变成通路,这里用红色来表示。
(2)把上一步”终点”的格子作为新的一个“起点”格子,不断循环第2步的过程。直到,找不到周围有黄色的1,就回溯,回到之前的位置,看看周围是否有黄色的1,如果有,就按照步骤1,不断将黄色1变联通,接下来就是不停地重复上面的步骤,找到就联通,找不到就往回走。
(3)遍历完所有的点即可生成一个迷宫,然后再选择出口与入口,一个完整的迷宫就形成了。
总结:这种方案生成的迷宫会有一条明显的主路,这条主路特别长,贯穿大部分区域的路线,同时,迷宫的路线一般比较扭曲。这种采用深度优先算法(递归回溯算法)生成的迷宫称之为“主路扭曲型”迷宫。
相关代码:
//初始化,初始化迷宫参数
Maze(){
mMap = new int [num][num];
visit = new boolean[num][num];
for (int i = 0; i < num; i = i+2) {//初始化地图的空格
for (int j = 0; j < num; j=j+2){
mMap[i][j] = wall;//其余均为墙
visit[i][j] = false;
}
}
for (int i = 1; i < num; i = i+2) {//初始化地图的空格
for (int j = 1; j < num; j=j+2){
mMap[i][j] = road;//奇数行奇数列的格子均为路
visit[i][j] = false;
}
}
visit[start.x][start.y] = true;
mMap[start.x][start.y] = road;
cur = start; //将当前格标记为开始格
movePerson=new Node(start.x-1,start.y);
drawPath=false;
createMaze();
this.addKeyListener(this);
this.setFocusable(true);
}
visit[start.x][start.y] = true;
mMap[start.x][start.y] = road;
cur = start; //将当前格标记为开始格
movePerson=new Node(start.x-1,start.y);//设置移动的起始点
drawPath=false;
xPath.clear();//清空行走的路径坐标
yPath.clear();
openList.clear();
closeList.clear();
createMaze();
this.setFocusable(true);
repaint();
}
//深度优先遍历
void createMaze() {
path.push(cur); //将当前格压入栈
while(!path.empty()) {
ArrayList<Node> mNei=notVisitedNei(cur);
if(mNei.size()==0){//如果该格子没有可访问的邻接格,则跳回上一个格子
cur = path.pop();
continue;
}
next = mNei.get(new Random().nextInt(mNei.size()));//随机选取一个邻接格
int x = next.x;
int y = next.y;
if(visit[x][y]){//如果该节点被访问过,则回到上一步继续寻找
cur = path.pop();
}
else{//否则将当前格压入栈,标记当前格为已访问,并且在迷宫地图上移除障碍物
path.push(next);
visit[x][y] = true;
mMap[x][y] = road;
mMap[(cur.x + x) / 2][(cur.y + y) / 2] = road; //打通当前格与下一格
cur = next;//当前格等于下一格
}
}
mMap[start.x-1][start.y]=1;//设置入口
mMap[end.x+1][end.y]=1;//设置出口
}
public ArrayList<Node> notVisitedNei(Node node)//寻找未访问的邻接节点
{
int []nei={2,0,-2,0,2};
ArrayList<Node> list = new ArrayList<Node>();
for(int i = 0; i < nei.length-1; i++)
{
int x = node.x + nei[i];
int y = node.y + nei[i+1];
if( x >= 0 && x < num && y >= 0 && y < num)
{
if(!visit[x][y])//未访问,则入数组
list.add(new Node(x,y));
}
}
return list;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
this.setBackground(Color.WHITE);
g.setColor(Color.black);//画墙
for(int i=0;i<num;i++){
for(int j=0;j<num;j++){
if(mMap[i][j]==0){
g.fillRect(10+i*width,10+j*width,width,width);
}
}
}
g.setColor(Color.pink);//画移动的轨迹
for(int i=0;i<xPath.size();i++){
g.fillOval(10+xPath.get(i)*width+width/4 , 10+yPath.get(i)*width+width/4,
width / 2, width / 2);
}
g.setColor(Color.RED);//画点的移动
g.fillOval(10+movePerson.x*width+width/4 , 10+movePerson.y*width+width/4,
width / 2, width / 2);
}
private boolean isOutOfBorder(int x, int y) {//越界检测
if (x > num-1 || y >num-1 || x < 0 || y < 0) {
return true;
}
return false;
}
2.A*算法
搜索区域(The Search Area):搜索区域被划分为简单的二维数组,数组每个元素对应一个结点。
开放列表(Open List):将寻路过程中待检测的结点存放于Open List中,而已检测过的结点则存放于Close List中。
路径排序(Path Sorting):下一步怎么移动由以下公式确定;F(n)=G+H。F(n)为估价函数,G代表的是从初始位置Start沿着已生成的路径到指定待检测结点移动开销。H表示待检测结点到目标节点B的估计移动开销。
启发函数(Heuristics Function): H为启发函数,可以看作是一种试探,由于在找到唯一路径前,不确定在前面会出现什么障碍物,因此用了一种计算H的算法,具体可以根据实际情况决定。为了简化问题,H采用的是传统的曼哈顿距离,也就是横纵向走的距离之和。
相关算法:
//A*算法
public Node findMinFNodeInOpenList() {//寻找最小移动开销的节点
Node tempNode = openList.get(0);
for (Node node : openList) {
if (node.F < tempNode.F) {
tempNode = node;
}
}
return tempNode;
}
public ArrayList<Node> findNeighborNodes(Node currentNode) {//找上下左右四个方向的邻居节点
ArrayList<Node> arrayList = new ArrayList<Node>();
int topX = currentNode.x;
int topY = currentNode.y - 1;
if (!isOutOfBorder(topX, topY) && !exists(closeList, topX, topY) && mMap[topX][topY]==1) {
arrayList.add(new Node(topX, topY));
}
int bottomX = currentNode.x;
int bottomY = currentNode.y + 1;
if (!isOutOfBorder(bottomX, bottomY) && !exists(closeList, bottomX, bottomY) && mMap[bottomX][bottomY]==1) {
arrayList.add(new Node(bottomX, bottomY));
}
int leftX = currentNode.x - 1;
int leftY = currentNode.y;
if (!isOutOfBorder(leftX, leftY) && !exists(closeList, leftX, leftY) && mMap[leftX][leftY]==1) {
arrayList.add(new Node(leftX, leftY));
}
int rightX = currentNode.x + 1;
int rightY = currentNode.y;
if (!isOutOfBorder(rightX, rightY) && !exists(closeList, rightX, rightY) && mMap[rightX][rightY]==1) {
arrayList.add(new Node(rightX, rightY));
}
return arrayList;
}
public Node findPath(Node startNode, Node endNode) {
openList.add(startNode);// 把起点加入 open list
while (openList.size() > 0) {
Node currentNode = findMinFNodeInOpenList();// 遍历 open list ,查找 F值最小的节点,把它作为当前要处理的节点
openList.remove(currentNode);// 从open list中移除
closeList.add(currentNode);// 把这个节点移到 close list
ArrayList<Node> neighborNodes = findNeighborNodes(currentNode);//寻找邻居节点
for (Node node : neighborNodes) {
if (exists(openList, node)) {//如果邻居节点在open列表中
foundPoint(currentNode, node);//更新列表中父节点和估价函数信息
} else {
notFoundPoint(currentNode, endNode, node);//如果邻居节点不在open列表中,则将该点加入open列表中
}
}
if (find(openList, endNode) != null) {//如果找到尾节点,则返回尾节点
return find(openList, endNode);
}
}
// return find(openList, endNode);
return null;
}
private void foundPoint(Node tempStart, Node node) {
int G = calcG(tempStart, node);
if (G < node.G) {
node.parent = tempStart;
node.G = G;
node.calcF();
}
}
private void notFoundPoint(Node tempStart, Node end, Node node) {
node.parent = tempStart;
node.G = calcG(tempStart, node);
node.H = calcH(end, node);
node.calcF();
openList.add(node);
}
private int calcG(Node start, Node node) {
int G = sValue;
int parentG = node.parent != null ? node.parent.G : 0;
return G + parentG;
}
private int calcH(Node end, Node node) {
int step = Math.abs(node.x - end.x) + Math.abs(node.y - end.y);
return step * sValue;
}
public static Node find(List<Node> nodes, Node point) {
for (Node n : nodes)
if ((n.x == point.x) && (n.y == point.y)) {
return n;
}
return null;
}
public static boolean exists(List<Node> nodes, Node node) {//判断节点是否存在某一list中
for (Node n : nodes) {
if ((n.x == node.x) && (n.y == node.y)) {
return true;
}
}
return false;
}
public static boolean exists(List<Node> nodes, int x, int y) {//判断节点是否存在某一list中
for (Node n : nodes) {
if ((n.x == x) && (n.y == y)) {
return true;
}
}
return false;
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {//响应键盘
int keyCode = e.getKeyCode();
int x=movePerson.x;
int y=movePerson.y;
switch (keyCode){
case KeyEvent.VK_SPACE: //空格键选择是否路径的显示的情况
if (drawPath) {
drawPath = false;
} else {
drawPath = true;
}
repaint();
break;
case KeyEvent.VK_LEFT: //根据键盘控制移动
x--;
break;
case KeyEvent.VK_RIGHT:
x++;
break;
case KeyEvent.VK_UP:
y--;
break;
case KeyEvent.VK_DOWN:
y++;
break;
}
if(!isOutOfBorder(x,y)&&mMap[x][y]==1){
xPath.add(movePerson.x);
yPath.add(movePerson.y);
movePerson.x=x;
movePerson.y=y;
}
repaint();
checkWin();
}
@Override
public void keyReleased(KeyEvent e) {
}
public static void main(String[] args) {
JPanel p = new Maze();
Image icon=Toolkit.getDefaultToolkit().getImage("maze.png");//设置最小化的图标
JFrame frame = new JFrame("MAZE(按空格键显示或隐藏提示路径)");
frame.setIconImage(icon);
frame.getContentPane().add(p);//添加画布
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//使用 System exit 方法退出应用程序。
frame.setSize(480, 500);//设置窗口大小
frame.setLocation(550, 150);//窗口在屏幕中的位置
frame.setVisible(true);
}
四.最终项目实现
项目完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Stack;
class Node {
public int x,y;
public Node parent;
public Node(int x, int y) {
this.x = x;
this.y = y;
}
public int F;//F为估价函数
public int G;//G代表的是从初始位置Start沿着已生成的路径到指定待检测结点移动开销
public int H;//H表示待检测结点到目标节点B的估计移动开销
public void calcF() {
this.F = this.G + this.H;
}//计算估价函数
}
public class Maze extends JPanel implements KeyListener {
//生成地图用到变量
final static int wall =0; //代表墙
final static int road =1; //代表空地
static int num = 21; //迷宫长度
int width = 21; //迷宫宽度
static int [][] mMap; //迷宫
boolean[][] visit; //用来标记某一格是否被访问过
Node start = new Node(1,1); //开始节点
Node end = new Node(num-2,num-2);//结束节点
Node cur; //当前格
Node next; //下一格
Stack<Node> path = new Stack<>();//记录生成地图时遍历的顺序
//走迷宫时用到的变量
private Node movePerson;
List<Integer> xPath=new ArrayList<>();//记录迷宫中行进的轨迹
List<Integer> yPath=new ArrayList<>();
private boolean drawPath = false;
//A*算法使用到的变量
public int sValue = 10;//设每一步的权值为10
private ArrayList<Node> openList = new ArrayList<>();//维护一个开放列表
private ArrayList<Node> closeList = new ArrayList<>();//维护一个关闭列表
//初始化,初始化迷宫参数
Maze(){
mMap = new int [num][num];
visit = new boolean[num][num];
for (int i = 0; i < num; i = i+2) {//初始化地图的空格
for (int j = 0; j < num; j=j+2){
mMap[i][j] = wall;//其余均为墙
visit[i][j] = false;
}
}
for (int i = 1; i < num; i = i+2) {//初始化地图的空格
for (int j = 1; j < num; j=j+2){
mMap[i][j] = road;//奇数行奇数列的格子均为路
visit[i][j] = false;
}
}
visit[start.x][start.y] = true;
mMap[start.x][start.y] = road;
cur = start; //将当前格标记为开始格
movePerson=new Node(start.x-1,start.y);
drawPath=false;
createMaze();
this.addKeyListener(this);
this.setFocusable(true);
}
public void init(){//第一轮结束后,再次初始化地图
mMap = new int [num][num];
visit = new boolean[num][num];
for (int i = 0; i < num; i = i+2) {//初始化地图的空格
for (int j = 0; j < num; j=j+2){
mMap[i][j] = wall;//其余均为墙
visit[i][j] = false;
}
}
for (int i = 1; i < num; i = i+2) {//初始化地图的空格
for (int j = 1; j < num; j=j+2){
mMap[i][j] = road;//奇数行奇数列的格子均为路
visit[i][j] = false;
}
}
visit[start.x][start.y] = true;
mMap[start.x][start.y] = road;
cur = start; //将当前格标记为开始格
movePerson=new Node(start.x-1,start.y);//设置移动的起始点
drawPath=false;
xPath.clear();//清空行走的路径坐标
yPath.clear();
openList.clear();
closeList.clear();
createMaze();
this.setFocusable(true);
repaint();
}
//深度优先遍历
void createMaze() {
path.push(cur); //将当前格压入栈
while(!path.empty()) {
ArrayList<Node> mNei=notVisitedNei(cur);
if(mNei.size()==0){//如果该格子没有可访问的邻接格,则跳回上一个格子
cur = path.pop();
continue;
}
next = mNei.get(new Random().nextInt(mNei.size()));//随机选取一个邻接格
int x = next.x;
int y = next.y;
if(visit[x][y]){//如果该节点被访问过,则回到上一步继续寻找
cur = path.pop();
}
else{//否则将当前格压入栈,标记当前格为已访问,并且在迷宫地图上移除障碍物
path.push(next);
visit[x][y] = true;
mMap[x][y] = road;
mMap[(cur.x + x) / 2][(cur.y + y) / 2] = road; //打通当前格与下一格
cur = next;//当前格等于下一格
}
}
mMap[start.x-1][start.y]=1;//设置入口
mMap[end.x+1][end.y]=1;//设置出口
}
public ArrayList<Node> notVisitedNei(Node node)//寻找未访问的邻接节点
{
int []nei={2,0,-2,0,2};
ArrayList<Node> list = new ArrayList<Node>();
for(int i = 0; i < nei.length-1; i++)
{
int x = node.x + nei[i];
int y = node.y + nei[i+1];
if( x >= 0 && x < num && y >= 0 && y < num)
{
if(!visit[x][y])//未访问,则入数组
list.add(new Node(x,y));
}
}
return list;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
this.setBackground(Color.WHITE);
g.setColor(Color.black);//画墙
for(int i=0;i<num;i++){
for(int j=0;j<num;j++){
if(mMap[i][j]==0){
g.fillRect(10+i*width,10+j*width,width,width);
}
}
}
if(drawPath){//画出A*算法求得的路径
g.setColor(Color.yellow);
Node parent = findPath(start, end); //父节点
ArrayList<Node> arrayList = new ArrayList<Node>();
while (parent != null) {
arrayList.add(new Node(parent.x, parent.y));
parent = parent.parent;
}
for (int i = 0; i <num; i++) {
for (int j = 0; j < num; j++) {
if (exists(arrayList, i, j)) {
g.fillOval(10+i*width+width/4 , 10+j*width+width/4,
width / 2, width / 2);
}
}
}
g.fillOval(10+(num-1)*width+width/4, 10+width*(num-2)+width/4, width / 2, width / 2);//终点上色
}
g.setColor(Color.pink);//画移动的轨迹
for(int i=0;i<xPath.size();i++){
g.fillOval(10+xPath.get(i)*width+width/4 , 10+yPath.get(i)*width+width/4,
width / 2, width / 2);
}
g.setColor(Color.RED);//画点的移动
g.fillOval(10+movePerson.x*width+width/4 , 10+movePerson.y*width+width/4,
width / 2, width / 2);
}
private boolean isOutOfBorder(int x, int y) {//越界检测
if (x > num-1 || y >num-1 || x < 0 || y < 0) {
return true;
}
return false;
}
private void checkWin() {//通关检测
if (movePerson.x==num-1 && movePerson.y==num-2) {
Object[] options = {"再来一局", "退出"};
int response = JOptionPane.showOptionDialog(this, " 恭喜通关", "Game Over", JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE, null,
options, options[0]);
if (response == 0) {//选再来一局的话
init();
} else {//选择退出
System.exit(0);
}
}
}
//A*算法
public Node findMinFNodeInOpenList() {//寻找最小移动开销的节点
Node tempNode = openList.get(0);
for (Node node : openList) {
if (node.F < tempNode.F) {
tempNode = node;
}
}
return tempNode;
}
public ArrayList<Node> findNeighborNodes(Node currentNode) {//找上下左右四个方向的邻居节点
ArrayList<Node> arrayList = new ArrayList<Node>();
int topX = currentNode.x;
int topY = currentNode.y - 1;
if (!isOutOfBorder(topX, topY) && !exists(closeList, topX, topY) && mMap[topX][topY]==1) {
arrayList.add(new Node(topX, topY));
}
int bottomX = currentNode.x;
int bottomY = currentNode.y + 1;
if (!isOutOfBorder(bottomX, bottomY) && !exists(closeList, bottomX, bottomY) && mMap[bottomX][bottomY]==1) {
arrayList.add(new Node(bottomX, bottomY));
}
int leftX = currentNode.x - 1;
int leftY = currentNode.y;
if (!isOutOfBorder(leftX, leftY) && !exists(closeList, leftX, leftY) && mMap[leftX][leftY]==1) {
arrayList.add(new Node(leftX, leftY));
}
int rightX = currentNode.x + 1;
int rightY = currentNode.y;
if (!isOutOfBorder(rightX, rightY) && !exists(closeList, rightX, rightY) && mMap[rightX][rightY]==1) {
arrayList.add(new Node(rightX, rightY));
}
return arrayList;
}
public Node findPath(Node startNode, Node endNode) {
openList.add(startNode);// 把起点加入 open list
while (openList.size() > 0) {
Node currentNode = findMinFNodeInOpenList();// 遍历 open list ,查找 F值最小的节点,把它作为当前要处理的节点
openList.remove(currentNode);// 从open list中移除
closeList.add(currentNode);// 把这个节点移到 close list
ArrayList<Node> neighborNodes = findNeighborNodes(currentNode);//寻找邻居节点
for (Node node : neighborNodes) {
if (exists(openList, node)) {//如果邻居节点在open列表中
foundPoint(currentNode, node);//更新列表中父节点和估价函数信息
} else {
notFoundPoint(currentNode, endNode, node);//如果邻居节点不在open列表中,则将该点加入open列表中
}
}
if (find(openList, endNode) != null) {//如果找到尾节点,则返回尾节点
return find(openList, endNode);
}
}
// return find(openList, endNode);
return null;
}
private void foundPoint(Node tempStart, Node node) {
int G = calcG(tempStart, node);
if (G < node.G) {
node.parent = tempStart;
node.G = G;
node.calcF();
}
}
private void notFoundPoint(Node tempStart, Node end, Node node) {
node.parent = tempStart;
node.G = calcG(tempStart, node);
node.H = calcH(end, node);
node.calcF();
openList.add(node);
}
private int calcG(Node start, Node node) {
int G = sValue;
int parentG = node.parent != null ? node.parent.G : 0;
return G + parentG;
}
private int calcH(Node end, Node node) {
int step = Math.abs(node.x - end.x) + Math.abs(node.y - end.y);
return step * sValue;
}
public static Node find(List<Node> nodes, Node point) {
for (Node n : nodes)
if ((n.x == point.x) && (n.y == point.y)) {
return n;
}
return null;
}
public static boolean exists(List<Node> nodes, Node node) {//判断节点是否存在某一list中
for (Node n : nodes) {
if ((n.x == node.x) && (n.y == node.y)) {
return true;
}
}
return false;
}
public static boolean exists(List<Node> nodes, int x, int y) {//判断节点是否存在某一list中
for (Node n : nodes) {
if ((n.x == x) && (n.y == y)) {
return true;
}
}
return false;
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {//响应键盘
int keyCode = e.getKeyCode();
int x=movePerson.x;
int y=movePerson.y;
switch (keyCode){
case KeyEvent.VK_SPACE: //空格键选择是否路径的显示的情况
if (drawPath) {
drawPath = false;
} else {
drawPath = true;
}
repaint();
break;
case KeyEvent.VK_LEFT: //根据键盘控制移动
x--;
break;
case KeyEvent.VK_RIGHT:
x++;
break;
case KeyEvent.VK_UP:
y--;
break;
case KeyEvent.VK_DOWN:
y++;
break;
}
if(!isOutOfBorder(x,y)&&mMap[x][y]==1){
xPath.add(movePerson.x);
yPath.add(movePerson.y);
movePerson.x=x;
movePerson.y=y;
}
repaint();
checkWin();
}
@Override
public void keyReleased(KeyEvent e) {
}
public static void main(String[] args) {
JPanel p = new Maze();
Image icon=Toolkit.getDefaultToolkit().getImage("maze.png");//设置最小化的图标
JFrame frame = new JFrame("MAZE(按空格键显示或隐藏提示路径)");
frame.setIconImage(icon);
frame.getContentPane().add(p);//添加画布
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//使用 System exit 方法退出应用程序。
frame.setSize(480, 500);//设置窗口大小
frame.setLocation(550, 150);//窗口在屏幕中的位置
frame.setVisible(true);
}
}
运行界面: