一、实验目的
开发一个迷宫游戏,要求迷宫是自动生成的,具有随机性。
二、实验工具
通过Java运行输出
三、实验内容要求:
1、迷宫随机生成;
2、玩家走迷宫;
3、系统用算法寻路,输出路径;
需要解决的问题:
1、如何显示迷宫的图形界面;
2、如何生成随机的迷宫;
3、怎样移动游戏中走迷宫的玩家;
预习:
在这里将会用到深度优先遍历的方法通过逆二叉树的方法来寻找入口到出口的路径。
思路:
迷宫由一个一个格子组成,格子之间画线看情况画线,要求从入口到出口只有一条路径。通过树实现是比较容易的,从根节点到每一个子节点都只有一条路径。假设入口是根节点,出口是树中某个子节点,那么,从根节点到该子节点的路径肯定是唯一的。所以构造一棵树把所有的格子都覆盖到,也就能够做出一个迷宫了。要求树的父节点和子节点必须是界面上相邻的格子。在界面显示时,父节点和子节点之间共用的边不画,其他的边都画出来,就能画出一个迷宫。
同时定义一个格子为入口,一个格子为出口。
1.树的表示:
要实现这棵树,那么每个树节点里就要存储一个坐标(X,Y)表示一个格子,另外还要存储四个指针。指针中有的为空,有的不为空,不为空的指针指向子节点,子节点保存邻居格子的坐标。
用二维数组表示迷宫的格子。每个数组元素存储一个指向父节点的引用,这样也可以形成一个虚拟的树。于是就用一个NN的二维数组,表示NN个格子,每个数组元素(Lattice)中有一个指向父节点的引用(father)。另外,为了能方便的获取格子的坐标,还要保存坐标信息。
2.构造树:
选定一个格子作为根节点。。我选择确定的一个坐标作为根节点。选择随机生成一个坐标作为根节点也可以。
每次扫描在当前树中找一个节点,看它的邻居格子是否在树中,如果还没在树中,就将该邻居格子加入树中,如果已在树中,就看下一个邻居格子,如果该节点所有邻居格子都在树中了,就找下一个节点,继续同样的操作。
3.走迷宫:
选定格子作为根节点,从它开始随机地深度搜索前进,开出一条路来,直到无路可走了,退回一步,换另一条路,再走到无路可走,回退一步,换另一条……如此循环往复,直到完全无路可走。
选择一个格子根节点,将它压进栈里。
然后在栈不为空的时候执行以下循环:
取出一个格子,将它的INTREE标志设置为1,然后将它的所有不在树中的邻居格子压进栈里(顺序随机),并且让这些邻居格子的father指向该格子。
代码:
import java.awt.Color; //颜色相关操作的类;
import java.awt.Graphics; //用以基本计何图形的绘制;
import java.awt.event.KeyAdapter; //提供按键事件相关操作的接口
import java.awt.event.KeyEvent; //按键事件
import java.util.Random; //随机类
import java.util.Stack; //栈
import javax.swing.JFrame; //控件,独立存在的顶级窗口;
import javax.swing.JOptionPane; //java的对话框类;
import javax.swing.JPanel; //Panel默认的布局管理器;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");
}
}
class Maze 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; //记录关数;
private boolean drawPath = false; //路径标志位;
Maze(int n, int w, int p) { //Maze的构造方法;
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() { //定义一个私有的默认构造函数;
// String name = JOptionPane.showInputDialog(null, "请输入:\n", "title", JOptionPane.PLAIN_MESSAGE); 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+"步",
// "恭喜过关。",JOptionPane.WARNING_MESSAGE);
JOptionPane.showMessageDialog(null, "你成功的走出了迷宫 !"+"你一共走了"+sum+"步");
rand++;
sum = 0;
// Object[] options ={ "继续", "退出" }; //自定义按钮上的文字
int m = JOptionPane.showConfirmDialog(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<grid> s = new Stack<grid>(); //生成栈;
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); //补充绘制外边框;
g.setColor(Color.blue);
g.fillOval(getCenterX(myY) - width / 3, getCenterY(myX) - width / 3, width / 2, width / 2);
//填充我们需要操作的小圆球的对象;Graphics中用当前颜色填充以指定矩形为边界的椭圆的方法;
if (drawPath == true)
drawPath(g);
}
//绘制迷宫路径;
private void drawPath(Graphics g) {
//完整路径寻路;逆二叉树搜索;
if (drawPath == true)
g.setColor(Color.blue); //显示正确的路径;
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(Color.blue);
}
// g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()), getCenterY(p.getFather()));
p = p.getFather(); //将p点对象更迭为其父类对象位置;
}
g.setColor(Color.blue); //绘制颜色(颜色覆盖);
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 = 10, width = 400, padding = 20, LX = 400, LY = 200; // n为边界的方格数目,width设置窗口边长;
JPanel p = new Maze(n, (width - padding - padding) / n, padding);
JFrame frame = new JFrame("迷宫游戏(按回车键提示获取提示)");
frame.getContentPane().add(p);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置窗口1点×可关闭;
frame.setSize(width + padding, width + padding + padding); //设置窗口大小;
frame.setLocation(LX, LY); //设置窗口位置;
frame.setVisible(true); //通过 setVisible()并设置参数为true,把内存中的窗口显示在屏幕上;
}
}