需求分析
(1)迷宫游戏是非常经典的游戏,在该题中要求随机生成一个迷宫,并求解迷宫;
(2)要求游戏支持玩家走迷宫,和系统走迷宫路径两种模式。玩家走迷宫,通过键盘方向键控制;
(3)系统走迷宫路径基于逆二叉树搜索,输出走迷宫的路径并显示。
(4)设计交互友好的游戏图形界面
编程语言及开发工具
java 基于IDEA
利用深度遍历的思想。访问到一个节点时,搜索这个节点没有被访问过的相邻节点,选择一个继续做同样的操作,直到没有邻节点为止再回溯到上一个访问的节点,并选择另外的邻节点。
算法思想
- 从第一个单元开始,检查当前单元是否堵塞(即周围四个单元都是已被访问或不可访问)
- 若不堵塞,则随机选择一个相邻单元作为下一单元,检查是否可访问
- 若可访问,则打破当前单元与下一单元之间的墙壁,将当前单元入栈,将下一单元作为当前单元;若不不可访问,则回到步骤2
- 若当前单元堵塞,则回退至上一单元
- 如此遍历,直到所有的单元都被访问
面板设计
- 利用Java Swing的相关函数,进行实线的绘画,以线为墙,实心圆为角色,空白部分为路来画迷宫。
- 利用Java函数addKeyListener来监听按键↑↓←→的输入,只有当角色周围是路上,才能移动。当角色到达终点时进入下一关。
package com.company;
import java.awt.Color; //颜色相关操作的类;
import java.awt.Graphics; //用以基本计何图形的绘制;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random; //随机类
import java.util.Stack; //栈
import javax.swing.*;
import javax.imageio.ImageIO;
class grid {
static final int InTree = 1;
static final int NotIntree = 0;
private int x = -1;
private int y = -1;
private int flag = NotIntree;
private grid father = null;
public grid(int xx, int yy) { //构造函数;
x = xx;
y = yy;
}
public int getX() { //获取该方格的横坐标;(由于不可直接用对象访问私有变量);
return x;
}
public int getY() { //获取纵坐标;
return y;
}
public int getFlag() { //获取标志位;
return flag;
}
public grid getFather() { //获取方格对象的父节点;
return father;
}
public void setFather(grid f) { //修改方格对象的父节点;
father = f;
}
public void setFlag(int f) {
flag = f; //修改标志位;
}
public String toString() { //以字符串形式导出坐标;
return new String("(" + x + "," + y + ")n");
}
}
public class Main extends JPanel {
private static final long serialVersionUID = -8300339045454852626L;
private int NUM, width, padding; // NUM为界面总边长,width为每个格子的边长;padding为内边距;
private grid[][] maze;
private int myX, myY; //定义了两个点;
int sum = 0; //记录步数;
int rand = 0; //记录关数;
//JButton Level1,Level2,Level3;
private boolean drawPath = false; //路径标志位;
Main(int n, int w, int p) { //Main的构造方法;
NUM = n; //窗口边长;
width = w; //子方格边长;
padding = p; //边线长度
maze = new grid[NUM][NUM]; //调用Lattice的构造函数,对尾端的小方格构造;
for (int i = 0; i <= NUM - 1; i++) //对除最后一格外的每一个坐标认定为一个小方格对象并构造;
for (int j = 0; j <= NUM - 1; j++)
maze[i][j] = new grid(i, j); //每个在窗口内具有整数坐标的点被视为一个方格对象;
createMaze();
setKeyListener();
this.setFocusable(true);
}
private void init() { //定义一个私有的默认构造函数;
for (int i = 0; i <= NUM - 1; i++)
for (int j = 0; j <= NUM - 1; j++) {
maze[i][j].setFather(null); //设置每个小方格的父节点为null;
maze[i][j].setFlag(grid.NotIntree); //设置每个方格的标志位;
}
myX = 0;
myY = 0;
drawPath = false; //路径绘制标识;
createMaze();
this.setFocusable(true); //使能控件获得焦点能力
repaint(); //重新绘制;
}
public int getCenterX(int x) {
return padding + x * width + width / 2; //内边距属性;第x+1位的小方格中心点横坐标位置;
}
public int getCenterY(int y) {
return padding + y * width + width / 2; //内边距属性;第y+1位的小方格中心点纵坐标位置;
}
public int getCenterX(grid p) {
return padding + p.getY() * width + width / 2; //得到方格对象P的第x+1位的小方格中心点横坐标位置;
}
public int getCenterY(grid p) {
return padding + p.getX() * width + width / 2; //得到方格对象P的第x+1位的小方格中心点纵坐标位置;
}
private void checkIsWin() {
if (myX == NUM - 1 && myY == NUM - 1) { //如果当前方格位置坐标对于末尾方格的坐标;
JOptionPane.showMessageDialog(null, "你走出了迷宫 !"+"一共走了"+sum+"步");
rand++;
sum = 0;
// Object[] options ={ "继续", "退出" }; //自定义按钮上的文字
int m = JOptionPane.show/confirm/iDialog(null, "你已经通过"+rand+"关,是否要继续闯关?", "是否继续",JOptionPane.YES_NO_OPTION); //返回值为0或1
// int m = JOptionPane.showOptionDialog(null, "你是否要继续闯关?", "第"+rand+"关",JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
if(m == 0) {
init();
}
if(m == 1){
JOptionPane.showMessageDialog(null, "本次你一共通过了"+rand+"关,下次继续挑战");
}
}
}
synchronized private void move(int c ) {
int tx = myX, ty = myY;
switch (c) {
case KeyEvent.VK_LEFT : //监控按键,对应按键有对应的当前位置坐标的修改;
ty--;
sum++;
// g.drawLine(myX,myY,tx,ty);
break;
case KeyEvent.VK_RIGHT :
ty++;
sum++;
break;
case KeyEvent.VK_UP :
tx--;
sum++;
break;
case KeyEvent.VK_DOWN :
tx++;
sum++;
break;
case KeyEvent.VK_ENTER: //如果是回车键;
if (drawPath == true) { //如果路径绘制标识符为1,就置0;
drawPath = false;
} else {
drawPath = true; //否则就把路径标识符置1;
}
break;
default :
}
if (!isOutOfBorder(tx, ty) && (maze[tx][ty].getFather() == maze[myX][myY]
|| maze[myX][myY].getFather() == maze[tx][ty])) { //是否越界判断;
myX = tx; //越界则不动;
myY = ty;
}
}
private void setKeyListener() { //设置按键监视器;
this.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) { //按键事件;
int c = e.getKeyCode(); //获取当前按键信息;
move(c); //根据按键信息继续坐标操作;
repaint(); //将操作后的信息修正展示;
checkIsWin(); //检测是否赢了;
}
});
}
private boolean isOutOfBorder(grid p) { //子方格对象调用的越界判断;
return isOutOfBorder(p.getX(), p.getY());
}
private boolean isOutOfBorder(int x, int y) { //坐标调用的越界判断;
return (x > NUM - 1 || y > NUM - 1 || x < 0 || y < 0) ? true : false;
}
private grid[] getNeis(grid p) {
final int[] adds = {-1, 0, 1, 0, -1}; // 顺序为左上右下 一个数组;
if (isOutOfBorder(p)) { //未越界不操作;
return null;
}
grid[] ps = new grid[4]; // 顺序为上右下左
int xt;
int yt;
for (int i = 0; i <= 3; i++) {
xt = p.getX() + adds[i];
yt = p.getY() + adds[i + 1];
if (isOutOfBorder(xt, yt)) //遍历点的左上右下四个方向,如果越界则不操作;
continue;
ps[i] = maze[xt][yt]; //将可能的方向存入对象数组中;
}
return ps; //将得到的全部方向返回;
}
private void createMaze() { //迷宫建立;
Random random = new Random(); //创建随机对象;
int rx = Math.abs(random.nextInt()) % NUM; //生成【0,num】间的随机数做横纵坐标;
int ry = Math.abs(random.nextInt()) % NUM;
Stack s = new Stack(); //生成栈;
grid p = maze[rx][ry]; //由随机坐标生成随机方格对象;
grid neis[] = null; //生成方格对象数组neis;
s.push(p); //随机方格对象压栈;
while (!s.isEmpty()) { //如果栈非空;
p = s.pop();
p.setFlag(grid.InTree); //将之前随机生成的点都修改标志位;
neis = getNeis(p); //获取当前方格的全部可行方向,存入neis中;
int ran = Math.abs(random.nextInt()) % 4; //产生【0,3】的随机整数;
for (int a = 0; a <= 3; a++) {
ran++;
ran %= 4; //每次修改ran的值并保证其在【0,4】区间
if (neis[ran] == null || neis[ran].getFlag() == grid.InTree)
continue; //如果不存在或者已经标记,就不操作;
s.push(neis[ran]); //对于可用的点都入栈操作;
neis[ran].setFather(p); //如果存在,又未入树则标记其父节点
}
}
}
//清除掉书的枝干上的节点;;
private void clearFence(int i, int j, int fx, int fy, Graphics g) {
int sx = padding + ((j > fy ? j : fy) * width), //取较大的设置
sy = padding + ((i > fx ? i : fx) * width),
dx = (i == fx ? sx : sx + width),
dy = (i == fx ? sy + width : sy);
if (sx != dx) {
sx++;
dx--;
} else {
sy++;
dy--;
}
g.drawLine(sx, sy, dx, dy); //绘制栅栏边界;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g); //super 调用父类的方法;
for (int i = 0; i <= NUM; i++) {
g.drawLine(padding + i * width, padding,padding + i * width, padding + NUM * width);
} //需要两个坐标(x1,y1)和(x2,y2)绘制全局格框(注意空出出入口);
for (int j = 0; j <= NUM; j++) {
g.drawLine(padding, padding + j * width, padding + NUM * width, padding + j * width);
}
g.setColor(this.getBackground());
for (int i = NUM - 1; i >= 0; i--) {
for (int j = NUM - 1; j >= 0; j--) {
grid f = maze[i][j].getFather(); //取出个方格对象的父节点;
if (f != null) {
int fx = f.getX(), fy = f.getY(); //如果父节点存在(父节点其实就是一个对象)获取父节点的坐标;
clearFence(i, j, fx, fy, g); //清除掉树枝干上的边框;
}
}
}
g.drawLine(padding, padding + 1, padding, padding + width - 1);
int last = padding + NUM * width;
g.drawLine(last, last - 1, last, last - width + 1); //补充绘制外边框;
//插入图片表示玩家当前所在位置
BufferedImage block = null;
try {
block = ImageIO.read(this.getClass().getResourceAsStream("green.png"));
g.drawImage(block,getCenterX(myY) - width / 3, getCenterY(myX) - width / 3,width / 2, width / 2,null);
} catch (IOException e) {
e.printStackTrace();
}
if (drawPath == true)
drawPath(g);
}
//绘制迷宫路径;
private void drawPath(Graphics g) {
//完整路径寻路;逆二叉树搜索;
if (drawPath == true)
g.setColor(new Color(122,188,50)); //显示正确的路径;
else
g.setColor(this.getBackground()); //不需要答案提示情况下显示背景颜色(白色)
grid p = maze[NUM - 1][NUM - 1]; //生成对象P为最后一个对象位置;
while (p.getFather() != null) { //当存在父节点时;
p.setFlag(2); //将节点标志位置2;
p = p.getFather(); //再将当前节点更迭至其父节点处
}
//经过一遍遍历,从末尾格局父节点唯一原则,生成了一条从末尾遍历至开头的通路,路上全标记为2;
p = maze[0][0]; //对p置首位;
while (p.getFather() != null) { //存在父节点时;
if (p.getFlag() == 2) { //如果对象标志位为2
p.setFlag(3); //对象标志位置为3;
g.setColor(new Color(236,236,236));
}
g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()), getCenterY(p.getFather()));
p = p.getFather(); //将p点对象更迭为其父类对象位置;
}
g.setColor(new Color(122,188,50)); //绘制颜色(颜色覆盖);
p = maze[NUM - 1][NUM - 1]; //
while (p.getFather() != null) { //该点对象存在父节点;
if(p.getFlag() == 3)
break;
g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()), getCenterY(p.getFather()));
p = p.getFather();
}
}
public static void main(String[] args) {
final int n=20,width = 600, padding = 18, LX = 400, LY = 100; // n为边界的方格数目,width设置窗口边长;
JPanel p = new Main(n, (width - padding - padding) / n, padding);
Jframe frame = new Jframe("迷宫游戏");
frame.getContentPane().add(p);
frame.setDefaultCloseOperation(Jframe.EXIT_ON_CLOSE); //设置窗口可关闭;
frame.setSize(width + padding, width + padding + padding); //设置窗口大小;
frame.setLocation(LX, LY); //设置窗口位置;
frame.setVisible(true); //通过 setVisible()并设置参数为true,把内存中的窗口显示在屏幕上;
}
}
四、程序功能演示
初始界面如下,玩家按下回车键后会出现路径提示
再次按下回车键可关闭提示。
到达终点后显示总共步数。
按下确定后提示选择“继续”或是“结束”
由于时间关系本项目仍存在bug,例如不继续闯关会停留在终点,下次可以在终点继续闯关……
面板设计方面,还未添加难度选择模块。
在期末考试结束后,有时间我会继续改进和维护这个项目的。



