Java扫雷游戏一例

本文介绍一个简单的扫雷游戏例子,屏幕抓图如下。

可执行的jar文件(j2sdk1.4.2_08编译打包,包括源代码):附件:jMine.jar(20K)

要解决的问题』
1. 地雷,标识棋等图形的绘制;
2. 游戏数据(地雷位置)的产生;
3. 非地雷格子显示数字的计算;
4. 游戏逻辑

『包中源文件列表』
 - hysun.minegame
    -- ConfigDialog.java
    -- FieldCell.java
    -- GameFrame.java
    -- GamePanel.java
    -- GraphicsUtil.java
  • ConfigDialog(extends JDialog)是配置游戏数据(雷场行列数,地雷数目)的对话框,就不多说了。
  • GameFrame(extends JFrame)只是提供一个应用窗口,也不说了。
  • GraphicsUtil提供图形绘制方法。
  • FieldCell代表一个格子。
  • GamePanel(extends JComponent implements MouseListener)代表整个雷场,并且控制游戏逻辑。

GraphicsUtil

该类提供static方法,绘制游戏中各种图形,并且将格子大小设成32x32。详情如下表所列:

未知区域
[蓝色区域]
    ....
    public static Color ukcolor = new Color(99, 130, 191);
    ....
    public static void drawUnknown(Graphics g, int x, int y) {
        g.setColor(ukcolor);
        g.fillRect(x, y, 32, 32);
    }
地雷
    ....
    public static Color mbcolor = new Color(90, 90, 90);
    ....
    public static void drawMine(Graphics g, int x, int y) {
        g.clearRect(x, y, 32, 32);
        g.setColor(mbcolor);
        g.fillOval(x+5, y+9, 21, 19);
        g.setColor(Color.black);
        g.fillRect(x+11, y+5, 10, 6);
    }
地雷标识旗
[小红旗]
    ....
    public static void drawFlag(Graphics g, int x, int y) {
        g.clearRect(x, y, 32, 32);
        g.setColor(Color.red);
        g.fillRect(x+8, y+8, 16, 10);
        g.setColor(Color.black);
        g.drawLine(x+8, y+8, x+8, y+24);
        g.drawLine(x+9, y+8, x+9, y+24);
    }
非地雷格数字(0-8)
[不同数字使用不同颜色]
    ....
    public static Color[] colorreg = new Color[] {
        null,                 // 0
        Color.blue,           // 1
        Color.green.darker(), // 2
        Color.red,            // 3
        Color.blue.darker(),  // 4
        Color.MAGENTA,        // 5
        Color.CYAN.darker(),  // 6
        Color.BLACK,          // 7
        Color.orange.darker() // 8
    };
    ....
    public static Font numfont = new Font("Verdana", Font.BOLD, 18);
    ....
    public static void drawNumber(Graphics g, int x, int y, int i) {
        g.clearRect(x, y, 32, 32);
        if (i == 0)
            return;
        g.setColor(colorreg[i]);
        g.setFont(numfont);
        FontMetrics fm = g.getFontMetrics();
        String s = String.valueOf(i);
        int sx = (32 - fm.stringWidth(s)) / 2;
        int sy = (32 - fm.getHeight()) / 2 + fm.getAscent();
        g.drawString(s, x+sx, y+sy);
    }
疑问标识旗
[小蓝旗,带问号]
    ....
    public static Font qnmfont = new Font("Verdana", Font.PLAIN, 10);
    ....
    public static void drawDoubt(Graphics g, int x, int y) {
        g.clearRect(x, y, 32, 32);
        g.setColor(colorreg[4]);
        g.fillRect(x+8, y+8, 16, 10);
        g.setColor(Color.black);
        g.drawLine(x+8, y+8, x+8, y+24);
        g.drawLine(x+9, y+8, x+9, y+24);
        g.setColor(Color.yellow);
        g.setFont(qnmfont);
        FontMetrics fm = g.getFontMetrics();
        String s = "?";
        int sx = (14 - fm.stringWidth(s)) / 2;
        int sy = (10 - fm.getHeight()) / 2 + fm.getAscent();
        g.drawString(s, x+sx+10, y+sy+8);
    }
叉叉
[game over时标柱错误的判断]
    ....
    public static void drawCross(Graphics g, int x, int y) {
        g.setColor(Color.black);
        g.drawLine(x+2, y+2, x+28, y+28);
        g.drawLine(x+2, y+3, x+28, y+29);
        g.drawLine(x+3, y+2, x+29, y+28);
        g.drawLine(x+2, y+28, x+28, y+2);
        g.drawLine(x+2, y+27, x+28, y+1);
        g.drawLine(x+3, y+28, x+29, y+2);
    }
好了,问题1圆满解决。

FieldCell

该类的关键在于格子相关的属性变量,如下表所列:

变量 详情
int state 代表该格子目前所处状态,取值范围是该类所定义的几个常数:UNKNOWN(该格子目前还是未知区域), FLAGGED(已经被标识为地雷), DOUBTED(被怀疑为地雷), REVEALED(已经被挖开), WRONG_F(game over时错误标识为地雷), WRONG_D(game over时错误怀疑为地雷)
boolean isMine 顾名思义,表明该格子是否埋有地雷
int number 只有当该格子没有地雷时,该变量才被用到,标识该格子周围的地雷数目,数目0-8
int gHint Graphics Hint,UI利用该信息为该格子画出适当的图形,每当格子状态改变时,gHint的值将根据以上三个变量做出相应的调整。gHint的数值和实际图形的映射可以参看源代码的注释。

该类对上述变量进行操作(get/set)的方法除外,还有一个方法public void draw(Graphics g, int x, int y)。此方法根据gHint的值利用GraphicsUtil提供的方法对自身的格子进行绘画,会被GamePanel调用到。

GamePanel

最后看看游戏的老大吧。该类中有一些游戏状态显示的代码,主要是根JLabel,JButton等相关的,就略去不提了。

------
GamePanel里面有一个2D的数组:FieldCell[][] cells。问题2和3是关于游戏前数据的初始化问题,其代码包含在public void setGameParam(int mineNum, int r, int c)方法里面。

地雷数据产生的原理很简单,不断随机产生0到总格子数之间的一个数,只到不重复的数目达到所需地雷数目。如下:

    ....
    int totalNum = r * c;
    cells = new FieldCell[r][c];
    for (int i = 0; i < r; i++) {
        for (int j = 0; j < c; j++) {
            cells[i][j] = new FieldCell();
        }
    }
        
    int count = 0;
    while (count < mineNum) {
        int s = (int) (Math.random() * totalNum);
        FieldCell fc = cells[s/c][s%c];
        if (!fc.isMine()) {
            fc.setMine(true);
            count++;
        }
    }

相邻地雷数目的计算就要一个格子一个格子的过一边了。原理也很简单,每个格子有8个相邻的格子(处于边界的格子除外),每个格子都检查一下是不是地雷。如下:

    ....
    for (int i = 0; i < r; i++) {
        for (int j = 0; j < c; j++) {
            if (!cells[i][j].isMine()) {
                int num = 0;
                if (i-1 >= 0) {
                    if (j-1 >= 0 && cells[i-1][j-1].isMine())
                        num++;
                    if (cells[i-1][j].isMine())
                        num++;
                    if (j+1 < c && cells[i-1][j+1].isMine())
                        num++;
                }
                { // i
                    if (j-1 >= 0 && cells[i][j-1].isMine())
                        num++;
                    if (cells[i][j].isMine())
                        num++;
                    if (j+1 < c && cells[i][j+1].isMine())
                        num++;
                }
                if (i+1 < r) { // i+1
                    if (j-1 >= 0 && cells[i+1][j-1].isMine())
                        num++;
                    if (cells[i+1][j].isMine())
                        num++;
                    if (j+1 < c && cells[i+1][j+1].isMine())
                        num++;
                }
                cells[i][j].setNumber(num);
            } // end Non-Mine cell if
        } // end for on j
    } // end for on i

至此,问题2,3也得到解决。另外需要提到的是GamePanel是一个JComponent的子类,它的图形是通过override下面这个方法实现的:

    ....
    protected void paintComponent(Graphics g) {
        g.setColor(Color.lightGray);
        g.fillRect(0, 0, w, h);
        for (int i = 0; i < r; i++) {
            for (int j = 0; j < c; j++) {
                //格子大小是32×32,这里用34就在每两个格子间留下2的空隙
                cells[i][j].draw(g, j*34, i*34);
            }
        }
    }

代码中的w, h为该面板的长,宽,根据格子数目设置(每两个格子中间留有一段空隙)。上面讲到FieldCell类的draw方法就是在这里被调用的。

------
游戏逻辑的解决是由对鼠标事件的处理完成的。GamePanel实现了MouseListener这个接口,不过这里只用到了mouseClicked这个方法。由于实际代码中牵涉到很多其他更新用户界面的方法,为求简练,这里将用pseudo code来解释:

    public void mouseClicked(MouseEvent e) {
        根据鼠标事件记录的位置找出相应的格子;
        if (该格子已经被挖开: FieldCell.REVEALED)
            return;
        if (左键点击) {
            if (被插了小红旗) //这是用来防止玩家误操作的
                return;
            if (踩到地雷了) {
                game over; //鼠标事件停止响应
                显示所有未挖出地雷,并且用叉叉标出错误的红旗和蓝旗;
            } else { //挖地雷,标柱蓝旗的格子可以挖开
                调用一个叫reveal(int i, int j)方法。 
                // reveal(int int) 是个递归的方法,首先将自己挖开,
                // 然后如果自己是个数字为0的格子,对相邻的8个格子调用reveal方法。
                // reveal(int int)每挖开一个格子,挖开格子计数加加。

                查看是否满足胜利条件(被挖开格子数+被插小红旗的格子数=总格子数);
                if (胜利)
                    恭喜玩家,停止鼠标事件响应;
            }
        } else if (右键点击) {  //右键用来控制插旗子
            if (格子状态==FieldCell.UNKNOWN) {
                if (插的小红旗数目 < 总地雷数) {
                    给格子插上红旗(设置状态);
                    红旗计数加加;
                    查看是否胜利(同上);
                } else {  //sorry, 你的红旗用完了,改用蓝旗吧。
                    给格子插上蓝旗(设置状态);
                }
            } else if (格子状态==FieldCell.FLAGGED) {  //红旗飘扬
                拔掉红旗,换成蓝旗;
                红旗计数减减;
            } else if (格子状态==FieldCell.DOUBTED) {  //蓝旗招摇
                回归未知区域;
            }
        }
        调用repaint()将雷场重画一边;//这个很关键,不然游戏对于玩家的操作将“无动于衷”。
    }

好了,问题4也解决了,大功告成!

你可能感兴趣的:(Java扫雷游戏一例)