0
点赞
收藏
分享

微信扫一扫

扫雷游戏代码

small_Sun 2022-05-05 阅读 65

若上面的象棋游戏较难于进行理解和记忆,下面再次讲解一个较为简单的游戏(扫雷小游戏),基本运行界面如下:

 

 游戏功能的介绍:如最上方的介绍,首先是难度选择界面,通过点击三个不同的选项来实现不同难度游戏的选择,点击简单后显示如下方图片所示,大小为9*9的格子,其内包含10个地雷,可以通过左键的单机实现格子的翻开,如果翻开的是空白格子,则表示其周围8个格子内没有雷,将会递归的打开此8个格子,直到无法再次打开为止,例如上方除3外的格子都是点击了空白格子实现的,而点开的格子如果是数字格子,则将直接被翻开,如果左点击的是雷则将直接使游戏结束。同时通过右键可以实现插旗以及取消插旗的功能,如果右击数字格子,当该格子周围的8个格子的雷已经被棋子点出来则8个格子内的其它格子将会被掀开,如果其中有空白格子,也将会被递归掀开,同时插旗的旗的数目是固定的,当旗子数目恰好等于雷的数目时,如果插旗地方恰好都是雷的位置,则表明游戏胜利,否则将使游戏结束,当游戏结束时,会(除插了棋子格子除外)将所有格子都掀开,对于插了棋子的格子,如果插的正确,则仍为插旗状态,否则将显示插错旗的图片(笑脸),且在游戏进行中,随时可以点击滚轮来调解游戏的难度,而左上角为显示棋子的数目,右上角为显示用了多长时间,当点击最上方图片时,将直接重启本难度的游戏。

功能介绍完毕:下面为代码的介绍.

具体代码包含七大类:

首先为GameWin类:这是代码运行的主体结构包含主函数.

package com.sxt;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

public class GameWin extends JFrame {//只有继承了JFrame类才有监听鼠标键盘的功能
//实际上只要继承了该Frame类的类都已经有了窗口只是没有设置为可见而已
int wigth=2*GameUtil.OFFSET+GameUtil.MAP_W*GameUtil.SQUARE_LENGTH;//窗口的宽度为:两边偏移量加横轴累格子数目诚意格子长度
int height=4*GameUtil.OFFSET+GameUtil.MAP_H*GameUtil.SQUARE_LENGTH;//窗口的高度理解同上

Image offScreenImage=null;//初始化一个新组件(画布)

MapBottom mapBottom=new MapBottom();

MapTop mapTop=new MapTop();//新建一个地图顶层类的对象必须放在底层类的下方(因为只有当底层完成后才能进行顶层的覆盖操作)

GameSelect gameSelect=new GameSelect();//新建一个地图选择类的对象用来控制地图选择
//是否开始 false:未开始 true:开始
boolean begin=false;//用来传递难度选择状态时是否点击到了难度框架
void launch(){//该方法用来创建窗口

        //由于此刻标志着游戏正式开始
        GameUtil.START_TIME=System.currentTimeMillis();//记录当前时间
        this.setVisible(true);//this就是该继承了JFrame类的GameWin类的某个对象
        //只有在整个窗口可见之后才能将游戏选择选项加上去
        if(GameUtil.state==3){//表示此时游戏处于难度选择状态
            this.setSize(500,500);//令选择窗口大小为500*500
        }else{
            this.setSize(wigth,height);//当不是难度选择状态时,设置窗口大小为由定义的整个框架得到的变化的大小
        }

        this.setLocationRelativeTo(null);//表示窗口为居中显示
        this.setTitle("扫雷游戏");//设置该窗口的标题
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);//添加关闭窗口的方法当用户单击窗口的关闭按钮时程序执行的操作方法
        //由于鼠标点击要在窗口中因此鼠标事件的创建也放在创建窗口的方法launch()中
        //鼠标事件
        this.addMouseListener(new MouseAdapter() {//为该窗口添加鼠标事件
            @Override
            public void mouseClicked(MouseEvent e) {//当事件参数为MouseAdapter时自动选择添加
                super.mouseClicked(e);
                //作用是点点击鼠标后将触发的事件
                switch (GameUtil.state){
                    case 0://只有当游戏状态为0也就是游戏中鼠标的左右点击才能实现作用
                        if(e.getButton()==1){//表示为鼠标左键被点击
                        GameUtil.MOUSE_X=e.getX();
                        GameUtil.MOUSE_Y=e.getY();//记录此时鼠标点击处的x与y坐标
                        GameUtil.LEFT=true;//修改此时状态为左点击为true
                        }
                        if(e.getButton()==3){//鼠标右键被点击
                            GameUtil.MOUSE_X=e.getX();
                            GameUtil.MOUSE_Y=e.getY();//记录此时鼠标点击处的x与y坐标
                            GameUtil.RIGHT=true;//修改此时状态为右点击为true
                        }
                    case 1:
                    case 2://删除了case 0与1的break语句表明就算在游戏中和游戏胜利状态下点击了重新开始键也会重置游戏
                        if(e.getButton()==1){//在游戏失败的情况下点击了左键
                            if(e.getX()>GameUtil.OFFSET +GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W/2) &&
                            e.getX()<GameUtil.OFFSET +GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W/2)+GameUtil.SQUARE_LENGTH
                            && e.getY()>GameUtil.OFFSET
                            && e.getY()<GameUtil.OFFSET+GameUtil.SQUARE_LENGTH){
                                //表示如果在游戏已经失败的状态下,点击了重置游戏就分别调用底层和顶层的重置方法
                                mapBottom.reGame();
                                mapTop.reGame();
                                GameUtil.FLAG_NUM=0;//重新开始时释放棋子数量(但是实际棋子数量不决定绘画结果只是为了方便显示雷剩余个数的一部分计算)
                                GameUtil.START_TIME=System.currentTimeMillis();//若点击了重置游戏也要更新游戏开始时间,表示新游戏开始了
                                GameUtil.state=0;//将游戏状态设置为0即再次令游戏处于游戏中的状态
                            }
                        }
                        //此时判断为实现不论游戏处于什么状态都能再次选择难度
                        if(e.getButton()==2){//表示鼠标点击了滚轮
                            GameUtil.state=3;
                            begin=true;
                        }
                        break;
                    case 3://表示当前游戏状态为难度选择
                        if(e.getButton()==1){//表示为鼠标左键被点击
                            GameUtil.MOUSE_X=e.getX();
                            GameUtil.MOUSE_Y=e.getY();//记录此时鼠标点击处的x与y坐标
                            begin=gameSelect.hard();
                        }
                        break;
                    default:
                }

            }
        });
        while(true){//这样做的原因是能够一直绘图显示出来不用点击来实现
            repaint();//为用户主动调用paint方法目的是一直调用绘图方法
            begin();//该方法时刻都在调用只要当begin为true就返回难度选择界面
            try{
                Thread.sleep(40);//添加延时,防止刷新过快
            }catch (InterruptedException e){
                e.printStackTrace();//抛出异常
            }
        }
    }

    void begin(){
        if(begin){//表示点击到了难度选择的其中之一的框架游戏开始
            begin=false;//重新复制令begin为false用于下次进行难度选择时的判定
            gameSelect.hard(GameUtil.level);//由不同的难度等级设计不同难度的雷数以及雷区大小
            dispose();//当难度选择窗口选择完毕后就进行关闭
            GameWin gameWin=new GameWin();
            GameUtil.START_TIME=System.currentTimeMillis();//重置此时的开始时间
            GameUtil.FLAG_NUM=0;//令旗数重置为0
            //如果不重置且上局游戏中设置了棋子下局游戏时,左上角的地雷数目会发生改变,因为地雷数目的显示是由:总雷数-旗子数
            //即为RAY_MAX-FLAG_NUM得到的
            mapBottom.reGame();
            mapTop.reGame();//重新开始游戏
            gameWin.launch();//调用主要的lunch方法
        }
    }
    //对于该paint方法是继承于JFrame的方法只要是GameWin的对象系统都会自动调用该重写的方法但是如果不是该类的对象不会自动调用
    @Override
    public void paint(Graphics g) {//这个方法是已经定义完成的重写方法,只有使用Graphics类才能实现划线的操作
        if(GameUtil.state==3){
            g.setColor(Color.white);//令画笔画出来的颜色为白色否则根本看不出框架(都是黑色)
            g.fillRect(0,0,500,500);
            //表示画笔g在画布上绘制已填充的矩形,其中x与y表示矩形左上角的x与y坐标另外两个表示矩形的宽度与高度
            //如果没有该句就容易看不见整个矩形易于被画布遮住
            gameSelect.paintSelf(g);//画出来主体框架
        }else {
            offScreenImage = this.createImage(wigth, height);//初始化该组件与窗口的宽与高一致
            Graphics gImage = offScreenImage.getGraphics();//初始化一个画笔用来画该组件
            //设置背景颜色
            gImage.setColor(Color.orange);
            //令该画笔画出来的颜色为黄色
            gImage.fillRect(0, 0, wigth, height);//令该笔效果填充为整个画布而非整个组件
            mapBottom.paintSelf(gImage);//为mapBotton的循环画网格的方法至此雷区的以及对应的雷和数字绘制完毕
            mapTop.paintSelf(gImage);//为mapTop的画覆盖集的方法将所有应该覆盖在顶层的图片加载完毕
            //先将顶层与顶层元素都绘制在该新建画布中
            g.drawImage(offScreenImage, 0, 0, null);//最后将设置好的画布再放入窗口中(这样闪动现象即消失了)

            //实际上此时会出现闪烁问题,先在底层上绘制图片,再在顶层上绘制覆盖,但是paint方法是重复调用不停绘制因此会出现闪烁问题
            //解决方法:先新建一个新的组件,先将底层图片覆盖在该空组件上,再将顶层图片覆盖于底层图片上,最后将整个组件放回原来窗口上
        }
    }
    //注意网格是许多线可以使用循环绘制来实现

    public static void main(String[] args) {//主函数
        GameWin gameWin=new GameWin();//声明GameWin类的对象gamewin
        gameWin.launch();//调用定义的launch()方法也就是从此刻开始游戏正式开始
    }
}

其次为GameUtil类这类主要包含着程序内主要的定义的静态变量便于其它类调用

package com.sxt;
import java.awt.*;

/**
 * 工具类
 * 存放静态参数
 * 工具方法
 */
public class GameUtil {
    //定义地雷个数
    static int RAY_MAX=100;
    static int MAP_W=36;//地图的宽(横的格子数量)
    static int MAP_H=17;//地图的高(竖的格子数)
    static int OFFSET=45;//雷区偏移量(雷区到边框的距离)
    static int SQUARE_LENGTH=50;//格子的边长
    //插旗数量
    static int FLAG_NUM=0;//表示初始时,棋子数目为0

    //与鼠标相关的参数
    //坐标
    static int MOUSE_X;
    static int MOUSE_Y;
    //状态
    static boolean LEFT=false;
    static boolean RIGHT=false;

    //游戏状态 0:游戏中  1:胜利 2:失败 3:难度选择
    static int state=3;//表示进入游戏时首先进入难度选择界面

    //游戏难度
    static int level;

    //倒计时
    //分贝用来记录开始与结束时间
    static long START_TIME;
    static long END_TIME;

    //存储底层的元素 -1为雷 0为空  1-8表示对应的数字
    static int[][] DATA_BOTTOM=new int[MAP_W+2][MAP_H+2];//之所以令横纵坐标都加二是为了让整个区域大上一层容易实现数字的判断防止边界判断的进行

    //存储顶层的元素 -1为无覆盖(直接为雷或数字图片) 0为覆盖(一般格子覆盖)  1表示插旗(插旗覆盖) 2表示插错棋(插错旗图片进行覆盖)
    static int[][] DATA_TOP=new int[MAP_W+2][MAP_H+2];

    //载入图片
    static Image lei=Toolkit.getDefaultToolkit().getImage("imgs/lei.png");//直接利用工具方法载入了该图片
    static Image top=Toolkit.getDefaultToolkit().getImage("imgs/top.gif");//表示开始游戏时候各个格子都被覆盖的图片
    static Image flag=Toolkit.getDefaultToolkit().getImage("imgs/flag.gif");//推测该位置有雷并右击后,覆盖的图片
    static Image noflag=Toolkit.getDefaultToolkit().getImage("imgs/noflag.png");//猜错为无雷区并右击后,覆盖的图片
    static Image face=Toolkit.getDefaultToolkit().getImage("imgs/face.png");//表示游戏正在进行中
    static Image over=Toolkit.getDefaultToolkit().getImage("imgs/over.png");//表示游戏失败显示的图片
    static Image win=Toolkit.getDefaultToolkit().getImage("imgs/win.png");//表示游戏胜利显示的图片

    static Image[] images = new Image[9];//用来存放1-8的图片其实0不存在图片
    static {//静态代码块(首先代码块是只要调用该类就会执行,而静态表示永不改变)
        for(int i=1;i<=8;i++){
            images[i]=Toolkit.getDefaultToolkit().getImage("imgs/num/"+i+".png");//用来将1-8的数字图片载入图片数组中
        }
    }

    static void drawWord(Graphics g,String str,int x,int y,int size,Color color){
        //之所以将之封装为一个方法是因为将多次调用
        //绘制数字 剩余雷数
        g.setColor(color);
        g.setFont(new Font("仿宋",Font.BOLD,size));//设置字体为仿宋,加粗,字号为30
        g.drawString(str,x,y);//表示绘画一个字符串位置为x与y
    }
}

之后是难度选择类:GameSelect类里面包含创建难度选择页面的绘画方法以及是否点击到三个选择项的判定和顶级到某个选择项之后改变雷数以及区域大小的操作

package com.sxt;
import java.awt.*;
/**
 * 难度选择类
 */
public class GameSelect {

    //用来判断是否点击到难度选项
    boolean hard(){
        if(GameUtil.MOUSE_X>100 && GameUtil.MOUSE_X<400) {//三个矩形横坐标一致可以封装为一个if语句
            if(GameUtil.MOUSE_Y>50 && GameUtil.MOUSE_Y<150){
                GameUtil.level=1;//表示难度为简单
                GameUtil.state=0;//令游戏状态从3的难度选择变为0的游戏中
                return true;
            }
            if(GameUtil.MOUSE_Y>200 && GameUtil.MOUSE_Y<300){
                GameUtil.level=2;//表示难度为普通
                GameUtil.state=0;//令游戏状态从3的难度选择变为0的游戏中
                return true;
            }
            if(GameUtil.MOUSE_Y>350 && GameUtil.MOUSE_Y<450){
                GameUtil.level=3;//表示难度为苦难
                GameUtil.state=0;//令游戏状态从3的难度选择变为0的游戏中
                return true;
            }
        }
        return false;//表示没有点击但难度选择框架
    }

    void paintSelf(Graphics g){//用来绘制难度选择页面
        g.setColor(Color.black);//设定画出来的三个选择矩形的边框为黑色易于与外边整体的边框进行区别
        g.drawRect(100,50,300,100);
        //绘制矩形的方法,四个参数分别为其所在的x,y坐标矩形宽度与高度
        GameUtil.drawWord(g,"简单",220,100,30,Color.black);
        //显示该矩形框的具体内容字符串

        g.drawRect(100,200,300,100);
        GameUtil.drawWord(g,"普通",220,250,30,Color.black);

        g.drawRect(100,350,300,100);
        GameUtil.drawWord(g,"困难",220,400,30,Color.black);
    }
    void hard(int level){
        switch (level){
            case 1:
                GameUtil.RAY_MAX=10;//简单难度设置地雷数目为10个
                GameUtil.MAP_W=9;
                GameUtil.MAP_H=9;//地图大小为9*9
                break;
            case 2:
                GameUtil.RAY_MAX=40;//普通难度设置地雷数目为40个
                GameUtil.MAP_W=16;
                GameUtil.MAP_H=16;//地图大小为16*16
                break;
            case 3:
                GameUtil.RAY_MAX=99;//简单难度设置地雷数目为99个
                GameUtil.MAP_W=30;
                GameUtil.MAP_H=16;//地图大小为10*16
                break;
            default:
        }
    }
}

之后为MapTop类主要为实现地图顶层(覆盖在底层之上的格子,棋子以及差错棋图片)的设计与具体规则和绘制方法。

package com.sxt;
import java.awt.*;

/**
 * 顶层地图类
 * 绘制顶层组件
 * 判断逻辑
 */
public class MapTop {
    //格子位置(鼠标点击位置与格子位置不是一个意思,某个格子位置是固定的,鼠标点击一个格子内的坐标是不定的)
    int temp_x;
    int temp_y;

    //用来重置游戏的方法
    void reGame(){
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<=GameUtil.MAP_H;j++){//依旧是双重for循环遍历雷区所有格子
                GameUtil.DATA_TOP[i][j]=0;//将所有的顶层先都变为一般覆盖(恢复到原来游戏的状态)
            }
        }
    }

    //判断逻辑方法一直被paint方法调用,会根据游戏规则持续更新格子状态,实现图片的更新
    void logic(){
        //实际上其作用是在执行过一次点击事件后就进行判断将发生什么结果
        temp_x=0;
        temp_y=0;//之所以设置一个初值为0是当鼠标点击时候,当点击为边界时如果没有设初值此temp_x与temp_y是之前上一次点击非边界的结果
        if(GameUtil.MOUSE_X>GameUtil.OFFSET && GameUtil.MOUSE_Y>3*GameUtil.OFFSET){
            //只有当鼠标点击的区域不是边界时才进行转化
            temp_x=(GameUtil.MOUSE_X-GameUtil.OFFSET)/GameUtil.SQUARE_LENGTH+1;//由点击的坐标得到对应格子的坐标具体看之前设计的图片进行理解
            temp_y=(GameUtil.MOUSE_Y-3*GameUtil.OFFSET)/GameUtil.SQUARE_LENGTH+1;
        }
        if(temp_x>=1 && temp_x<=GameUtil.MAP_W && temp_y>=1 && temp_y<=GameUtil.MAP_H){
            //因为之前窗口区域扩大了一层,所以此时只有当鼠标点击的格子在窗口内才进行鼠标点击后事件的执行
            if(GameUtil.LEFT==true){//表示左键被点击
                //表示如果是覆盖则翻开
                if(GameUtil.DATA_TOP[temp_x][temp_y]==0){//表示点击的位置为初始点击格子为一般格子覆盖情况
                    GameUtil.DATA_TOP[temp_x][temp_y]=-1;
                    //表示变成无覆盖情况因为画图是一直执行(每隔40ms休息一次人眼无法看出来)但是一旦改变状态画图时候就不再绘画出来
                }
                spaceOpen(temp_x,temp_y);//表示当点击的格子为空的就递归打开它所在的3*3矩阵
                GameUtil.LEFT=false;//释放左键状态
            }
            if(GameUtil.RIGHT==true){//表示右键被点击
                //格子状态为覆盖则插旗
                if(GameUtil.DATA_TOP[temp_x][temp_y]==0) {//表示点击的位置为初始点击格子为一般格子覆盖情况
                    GameUtil.DATA_TOP[temp_x][temp_y] = 1;//表示该棋子状态为插旗在40ms后会自动补充上棋子图片
                    GameUtil.FLAG_NUM++;//表示令棋子数目加1
                }
                //格子状态为插旗则取消
                else if(GameUtil.DATA_TOP[temp_x][temp_y]==1){
                    GameUtil.DATA_TOP[temp_x][temp_y]=0;//40ms后再次画图时,会直接取消该插旗恢复为一般覆盖状态
                    GameUtil.FLAG_NUM--;//表示棋子数量减1
                }
                else if(GameUtil.DATA_TOP[temp_x][temp_y]==-1){//表示右键点击的区域为没有覆盖的区域
                    numOpen(temp_x,temp_y);//直接调用判断
                }
                GameUtil.RIGHT=false;//释放右键状态
            }
        }
        //判断该次事件点击后游戏是否失败
        boom();
        victory();//判断点击事件后游戏是否成功
    }
    //数字翻开(主要针对右键如果改数字与周围棋子数目一致则执行)
    void numOpen(int x,int y){
        //局部变量用来记录某数字格子3*3矩阵的棋子数量
        int count=0;
        if(GameUtil.DATA_BOTTOM[x][y]>0){//保证翻开的格子一定是数字
            for(int i=x-1;i<=x+1;i++) {//遍历该数字所在的3*3矩阵统计其中棋子数量
                for (int j = y - 1; j <= y + 1; j++) {
                    if(GameUtil.DATA_TOP[i][j]==1){
                        count++;//如果此3*3矩阵中有棋子则令记录变量count+1
                    }
                }
            }
            if(count==GameUtil.DATA_BOTTOM[x][y]){//该数字3*3矩阵内部棋子数量等于该数字大小(即其周围的雷已经全部判断完毕)
                //可以直接翻开未插旗的棋子(因为所有的雷都判断完毕)
                for(int i=x-1;i<=x+1;i++) {//遍历该数字格子为中心的3*3矩阵
                    for (int j = y - 1; j <= y + 1; j++) {
                        if(GameUtil.DATA_TOP[i][j]!=1){//表示如果3*3矩阵内的某格子顶部没有插旗(即不是雷)就直接开启
                            GameUtil.DATA_TOP[i][j]=-1;//直接赋值为未插旗状态下一个40ms完成绘图
                        }
                        //如果打开的格子为空格还要递归打开其附近矩阵的棋子
                        if(i>=1 && j>=1 &&i<=GameUtil.MAP_W && j<=GameUtil.MAP_H){
                            //目的是对于要打开的格子必须处于雷区当中
                            //防止下标越界,也就是防止在点击到边界后也会打开其四周区域,因为边界的格子都是空的
                            //为递归的方法
                            spaceOpen(i,j);//对打开的空格子再次递归调用打开空格的方法,直到再也没有连着的空格可以被打开位置
                        }
                    }
                }
            }
        }

    }
    //失败判定方法:true表示失败 false表示未失败
    boolean boom(){
        if(GameUtil.FLAG_NUM==GameUtil.RAY_MAX){
            for(int i=1;i<=GameUtil.MAP_W;i++){
                for(int j=1;j<=GameUtil.MAP_H;j++){
                    if(GameUtil.DATA_TOP[i][j]==0){
                        GameUtil.DATA_TOP[i][j]=-1;
                    }
                }
            }
        }
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<GameUtil.MAP_H;j++){//循环遍历整个雷区
                if(GameUtil.DATA_BOTTOM[i][j]==-1 && GameUtil.DATA_TOP[i][j]==-1){
                    //表示如果雷区中的某格子为底层为雷而顶层无覆盖的情况则判定点击到了类判定游戏失败
                    GameUtil.state=2;//将游戏对应状态变为2
                    seeBoom();
                    //表示如果失败则显示所有无棋子覆盖的雷区格子,以及将棋子覆盖错误的格子进行插错棋子的覆盖
                    return true;//表示游戏失败
                }
            }
        }
        return false;
    }
    //失败显示(当游戏失败也就是点击到雷后,就把游戏里的所有雷显示出来)
    void seeBoom(){
        for(int i=1;i<=GameUtil.MAP_W;i++){//循环遍历雷区的所有格子
            for(int j=1;j<=GameUtil.MAP_H;j++){
                if(GameUtil.DATA_BOTTOM[i][j]==-1 && GameUtil.DATA_TOP[i][j]!=1){
                    //表示如果该格子底层是雷且顶层不是棋子的情况下
                    GameUtil.DATA_TOP[i][j]=-1;//直接令该格子状态为无覆盖即可
                }
                if(GameUtil.DATA_BOTTOM[i][j]!=-1 && GameUtil.DATA_TOP[i][j]==1){
                    //表示如果该格子底层不是雷且顶层是棋子的情况下显示插错棋
                    GameUtil.DATA_TOP[i][j]=2;//直接令该格子状态为插错旗覆盖即可
                }
            }
        }
    }
    //胜利判断 true:表示胜利  false:表示未胜利
    boolean victory(){
        //统计未打开的格子数目
        int count=0;
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<=GameUtil.MAP_H;j++){
                if(GameUtil.DATA_TOP[i][j]!=-1){//如果顶层不是打开状态(即顶层存在覆盖)
                    count++;
                }
            }
        }
        if(count==GameUtil.RAY_MAX){
            GameUtil.state=1;//将游戏状态变为1当执行底层画图时将会使该图片加载到自己设计的位置
            for(int i=1;i<=GameUtil.MAP_W;i++) {
                for (int j = 1; j <= GameUtil.MAP_H; j++) {
                    //令未翻开的格子都变成棋子覆盖
                    if(GameUtil.DATA_TOP[i][j]==0){//表示如果未翻开区域为一般覆盖则改成棋子覆盖
                        GameUtil.DATA_TOP[i][j]=1;
                    }
                }
            }
            return true;//表示若未翻开格子数与雷数一致则游戏胜利
        }
        return false;//其它情况为false表示游戏尚未胜利但也未失败
    }
    //打开空格的方法
    void spaceOpen(int x,int y){//参数为坐标
        if(GameUtil.DATA_BOTTOM[x][y]==0){//表示当底层为空时(即不是雷也不是数字)
            //底层为空时,其所在3*3矩阵一定都没雷直接全部掀开即可
            for(int i=x-1;i<=x+1;i++){//遍历该空格所在的3*3矩阵
                for(int j=y-1;j<=y+1;j++){
                    //覆盖,才递归
                    if(GameUtil.DATA_TOP[i][j]!=-1){
                        if(GameUtil.DATA_TOP[i][j]==1){GameUtil.FLAG_NUM--;}
                        //这里特殊说明一下,插旗不一定是正确的因为是人们自己猜测的,故而其下面也可能是空的,所以在打开
                        //空格的方法中,如果该旗子下方是空的被打开的同时,也要令记录旗子数的FLAG——NUM--;
                        GameUtil.DATA_TOP[i][j]=-1;//表示其状态为没有覆盖情况,只要一设定该情况下一个40ms再次画图时候就没有覆盖
                        if(i>=1 && j>=1 &&i<=GameUtil.MAP_W && j<=GameUtil.MAP_H){
                            //目的是对于要打开的格子必须处于雷区当中
                            //防止下标越界,也就是防止在点击到边界后也会打开其四周区域,因为边界的格子都是空的
                            //为递归的方法
                            spaceOpen(i,j);//对打开的九个格子再洗递归调用打开空格的方法,直到再也没有连着的空格可以被打开位置
                        }
                    }
                }
            }
        }

    }
    //绘制方法
    void paintSelf(Graphics g){//传入参数画笔g
        logic();
        //画地雷
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<=GameUtil.MAP_H;j++){
                //覆盖
                if(GameUtil.DATA_TOP[i][j]==0) {//表示如果第i行第j列所存储的为顶层(top)
                    g.drawImage(GameUtil.top,
                            GameUtil.OFFSET + (i - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的x坐标(+1是为了让红线漏出来)
                            GameUtil.OFFSET * 3 + (j - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的y坐标
                            GameUtil.SQUARE_LENGTH - 2,//图片的宽度
                            GameUtil.SQUARE_LENGTH - 2,//图片的高度
                            null);
                }
                //插旗
                if(GameUtil.DATA_TOP[i][j]==1) {//表示如果第i行第j列所存储的为插旗(flag)
                    g.drawImage(GameUtil.flag,
                            GameUtil.OFFSET + (i - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的x坐标(+1是为了让红线漏出来)
                            GameUtil.OFFSET * 3 + (j - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的y坐标
                            GameUtil.SQUARE_LENGTH - 2,//图片的宽度
                            GameUtil.SQUARE_LENGTH - 2,//图片的高度
                            null);
                }
                //插错旗
                if(GameUtil.DATA_TOP[i][j]==2) {//表示如果第i行第j列所存储的为插错旗(noflag)
                    g.drawImage(GameUtil.noflag,
                            GameUtil.OFFSET + (i - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的x坐标(+1是为了让红线漏出来)
                            GameUtil.OFFSET * 3 + (j - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的y坐标
                            GameUtil.SQUARE_LENGTH - 2,//图片的宽度
                            GameUtil.SQUARE_LENGTH - 2,//图片的高度
                            null);
                }

            }
        }
    }
}

之后类似的有MapButtom类主要针对的则是地图的底层设计类:

package com.sxt;
import java.awt.*;

/**
 * 底层地图类:
 * 主要负责绘制游戏相关的组件
 */
public class MapBottom {
    BottomRay bottomRay=new BottomRay();//新建一个地雷类用于绘制地雷的图像
    BottomNum bottomNum=new BottomNum();//新建一个底层数据类用于非地雷格子数字的判断,必须在雷的生成下方
    {
        bottomRay.newRay();
        bottomNum.newNum();//之所以用程序块是因为只要调用了该MapBottom类建立对象就会自动执行,而非方法那种通过调用来实现
    }
    //用来重置游戏的方法
    void reGame(){
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<=GameUtil.MAP_H;j++){//依旧是双重for循环遍历雷区所有格子
                GameUtil.DATA_BOTTOM[i][j]=0;//将所有的底层先都变为空的(即既不是数字也不是雷)
            }
        }
        bottomRay.newRay();
        bottomNum.newNum();//重新生成雷和对应的数字格子
    }
    //绘制自己的方法
    void paintSelf(Graphics g){//传入参数画笔g
        g.setColor(Color.red);
        //两个画线完成雷区的设计框架
        //画竖线
        for(int i=0;i<=GameUtil.MAP_W;i++){
            //具体由自己设计的页面图片就可以理解两个点为何如此设计
            g.drawLine(GameUtil.OFFSET+i*GameUtil.SQUARE_LENGTH,
                    3*GameUtil.OFFSET,
                    GameUtil.OFFSET+i*GameUtil.SQUARE_LENGTH,
                    3*GameUtil.OFFSET+GameUtil.MAP_H*GameUtil.SQUARE_LENGTH);
        }
        //画横线
        for(int i=0;i<=GameUtil.MAP_H;i++){
            g.drawLine(GameUtil.OFFSET,
                    3*GameUtil.OFFSET+i*GameUtil.SQUARE_LENGTH,
                    GameUtil.OFFSET+GameUtil.MAP_W*GameUtil.SQUARE_LENGTH,
                    3*GameUtil.OFFSET+i*GameUtil.SQUARE_LENGTH);
        }
        //画地雷
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for(int j=1;j<=GameUtil.MAP_H;j++){
                //雷的绘制
                if(GameUtil.DATA_BOTTOM[i][j]==-1) {//表示如果第i行第j列所存储的为雷则将该雷画出来
                    g.drawImage(GameUtil.lei,
                            GameUtil.OFFSET + (i - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的x坐标(+1是为了让红线漏出来)
                            GameUtil.OFFSET * 3 + (j - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的y坐标
                            GameUtil.SQUARE_LENGTH - 2,//图片的宽度
                            GameUtil.SQUARE_LENGTH - 2,//图片的高度
                            null);
                }
                //数字绘制
                if(GameUtil.DATA_BOTTOM[i][j]>=0) {//表示如果第i行第j列所存储的为数字则将该雷画出来
                    g.drawImage(GameUtil.images[GameUtil.DATA_BOTTOM[i][j]],//此时表示图片来源为image类型在GameUtil类中已完成初始化
                            GameUtil.OFFSET + (i - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的x坐标
                            GameUtil.OFFSET * 3 + (j - 1) * GameUtil.SQUARE_LENGTH + 1,//x为图片要添加的y坐标(+1是为了让红线漏出来)
                            GameUtil.SQUARE_LENGTH-2,
                            GameUtil.SQUARE_LENGTH-2,
                            null);
                }
            }
        }
        //绘制数字 剩余雷数实际为页面左上方显示的剩余雷数的字符串
        GameUtil.drawWord(g,""+(GameUtil.RAY_MAX-GameUtil.FLAG_NUM),GameUtil.OFFSET,2*GameUtil.OFFSET,30,Color.red);
        //绘制数字  倒计时(即显示本次游戏执行了多久)
        GameUtil.drawWord(g,""+(GameUtil.END_TIME-GameUtil.START_TIME)/1000,//注意此时时间单位为ms要除以1000换算为s
                GameUtil.OFFSET+GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W-1),
                2*GameUtil.OFFSET,30,Color.red);//设置游戏进行了多少时间的字符显示窗口的位置,大小,内容,字体,和颜色

        switch (GameUtil.state){
            case 0://当游戏状态为0时,将表示游戏中的face图片加载到自己设计的位置也要注意大小也得设计因为自己找的图片过大
                GameUtil.END_TIME=System.currentTimeMillis();//表示当游戏进行时不断更新结束时间
                g.drawImage(GameUtil.face,
                        GameUtil.OFFSET+GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W)/2,
                        GameUtil.OFFSET,
                        GameUtil.SQUARE_LENGTH-2,
                        GameUtil.SQUARE_LENGTH-2,
                        null);
                break;
            case 1://此时1与2的位置与0相同只是将对应图片替换
                g.drawImage(GameUtil.win,
                        GameUtil.OFFSET+GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W)/2,
                        GameUtil.OFFSET,
                        GameUtil.SQUARE_LENGTH-2,
                        GameUtil.SQUARE_LENGTH-2,
                        null);
                break;
            case 2:
                g.drawImage(GameUtil.over,
                        GameUtil.OFFSET+GameUtil.SQUARE_LENGTH*(GameUtil.MAP_W)/2,
                        GameUtil.OFFSET,
                        GameUtil.SQUARE_LENGTH-2,
                        GameUtil.SQUARE_LENGTH-2,
                        null);
                break;
            default:
        }
    }
}

还有BottomRay类主要为实现底层雷的设计的类

package com.sxt;
/**
 * 底层地雷类
 * 初始化地雷
 */
public class BottomRay {
    //存放坐标
    static   int[] rays=new int[GameUtil.RAY_MAX*2];//该数组中用相邻两个数代表数组租表故数组大小为地雷数的2倍
    //直接改成静态的大小刚开始就是最大的不用担心当地雷数目变大后数组越界的情况,因为地雷数初始值就是最大的
    //地雷坐标
    int x,y;
    //是否放置 true表示可以放置 false表示不可以放置
    boolean isPlace=true;

    //用来生成雷(主要是让重置游戏时候调用)
    void newRay(){
        //代码块用来生成地雷坐标(代码块只要生成对象就会执行)此时改变为函数类型(为了生成第一次刚打开时候的游戏生成对象同时还要调用该方法)
        for(int i=0;i<GameUtil.RAY_MAX*2;i=i+2){//循环最大地雷数次用来生成所需要的地雷数
            x=(int)(Math.random()*GameUtil.MAP_W+1);//随机生成地雷的x坐标(+1是让随机数从0-11(此时宽度为11)变为1-12)
            y=(int)(Math.random()*GameUtil.MAP_H+1);//(1-12)12取不到
            //判断生成的随机坐标是否已经存在
            for(int j=0;j<i;j=j+2){//表示遍历已经生成的坐标查看新生成的坐标是否已经存在(用来防止雷的位置重复)
                if(x==rays[j] && y==rays[j+1]){
                    i=i-2;//表示令i回退重新生成坐标
                    isPlace=false;
                    break;//去重新执行坐标的生成
                }
            }
            //将坐标放入数组
            if(isPlace){//如果新生成的地雷坐标不重复则放入存储地雷坐标的数组rays中
                rays[i]=x;
                rays[i+1]=y;//将生成的坐标赋值到数组中去
            }
            isPlace=true;//不论最终是否可以防止都需要令放置可行否则逻辑错误
            //至此重复雷区的问题得到解决
        }
        for(int i=0;i<GameUtil.RAY_MAX*2;i=i+2){
            GameUtil.DATA_BOTTOM[rays[i]][rays[i+1]]=-1;//该二维数组用来存储对应位置是什么的标记此时表示坐标为(rays[i],rays[i+1])的位置是雷
        }
    }
}

最后还包含一个底层数字类,主要用于完成底层地雷的有关设计BottomNum类

package com.sxt;
/**
 * 底层数字类
 */
public class BottomNum {
    //生成对应的数字底层
    void newNum(){
        for(int i=1;i<=GameUtil.MAP_W;i++){
            for (int j = 1; j <=GameUtil.MAP_H ; j++) {
                if(GameUtil.DATA_BOTTOM[i][j]==-1){//判断对应区域是否是类之所以从1,1开始是因为0代表为边界区域
                    for(int k=i-1;k<=i+1;k++){
                        for(int l=j-1;l<=j+1;l++){//遍历该雷的3*3矩阵每个格子+1表示该格子所在3*3矩阵中雷数加1
                            if(GameUtil.DATA_BOTTOM[k][l]>=0){//雷为-1 >=0表示一定不是雷
                                GameUtil.DATA_BOTTOM[k][l]++;//令该位置存储数加1
                            }
                        }
                    }
                }
            }
        }
    }
}

主要对于主类GameWin类进行分析设计:

首先该GameWin类只要继承了JFrame类就已经自动创建了一个窗口只是暂时不可见而已,可以通过this.setvisible(true)来使之可见。定义的wigth与height为定义的整个窗口的宽度与高度,具体是通过GameUtil中的静态变量来实现的可以自己看注释.具体的设计图片如下所示:

 其中的A为OFFSET即偏移量每个格子的边长为SQUARE—LENGTH而MAP—H与MAP—W表示具体的每行与每列的格子数目,之后定义了一个图片类型空组件offScreenImage具体是用来设计难度选择页面,并新建了MapBottom类与MapTop类的两个对象mapbottom与maptop和GameSelect类对象gameselect同时定义boolean类型变量begin用来表示难度三个选择项是否有一个被点击中.对于launch()方法是用来具体创建窗口,首先定义令GameUtil类的START—TIME为当前系统的时间,之后判定游戏状态state为多少若为3则表示处于难度选择界面,令该页面窗口大小为固定的500*500像素,用来形成难度选择界面,如果不是3则由定义的wigth与height来形成对应的窗口大小(对应不同难度下)之后设置为居中显示,标题为"扫雷游戏",添加关闭该窗口的方法

this.setDefaultCloseOperation(EXIT_ON_CLOSE);即当前几窗口的✖时关闭该窗口.

之后添加一个鼠标事件方法用于实现当鼠标点击的方法能够实现当调用该方法所在方法launch时(不论什么时候点击鼠标都能够运行该方法)具体添加方法如下:

this.addMouseListener(new MouseAdapter() {//为该窗口添加鼠标事件
            @Override
            public void mouseClicked(MouseEvent e) {//当事件参数为MouseAdapter时自动选择添加
            super.mouseClicked(e);
            //执行体中间的执行体则是具体如何执行的操作
            }
 }

必须要记住该方法,这是添加鼠标事件的具体方法,之后通过switch(state)来实现不同状态下的具体功能,如果当前state为0表示为游戏中,之后判断当前鼠标点击的是哪个键,若为左键则获取当前鼠标点击的X与Y坐标并将此时的左点击状态LEFT设为true.如果点击的为右键同样获取其点击的X与Y坐标并令有点击状态RIGHT为true.为1时表示游戏胜利不进行任何状态的保存,为2时表示处于失败状态,此时左击鼠标只能是重置游戏,通过点击位置区域的判定来确定点击到了重置键,执行mapBottom与mapTop对象的reGame()方法来事项令图层的底层与顶层都设置为新游戏的状态(具体代码看具体类的方法即可),重启后令FLAG—NUM即旗子数置为0,START—TIME即开始时间重新定义为当前时间(由END_TIME为游戏进行时的当前时间减去START—TIME来实现得到游戏计时的功能),之后游戏失败时候还能点击滚轮,如果点击之后,就将state置为3表示重回游戏选择页面,并将begin置为true。并此时再加上break具体原因代码上注释已讲述.之后若state为3表示处于难度选择状态,若点击左键则获取当前的X与Y坐标并作为参数传入gameSelect方法来判定是否点击到了三个功能键之一将结果返回给begin;此时switch结束.所有的点击判断都已经完成。

之后加上一个while(true)语句令该线程永远执行下去,首先调用repaint();即主动调用paint()方法(若非此种调用则为由系统使用该类新建对象时自动调用)之所以将之加进来是为了实现不断绘画的目标,利用try{Thread.sleep(40)}catch语句令该线程每隔40ms执行一次即用来更新操作,即通过鼠标的点击改变了具体的参数(如FLAG_NUM当右击一个格子使得其加一,同时paint()方法是通过调用具体的静态变量来改变图形的,故而当该变量改变时下一个40ms后就会更细图形(且40ms的间隔人眼无法区分))此时点击事件到此结束。

之后为begin函数表示如果点击到了选择页面要进行的操作,其内只有一个判断语句if(begin)表示当点击到了某个选项,之后令begin为false当下次再次进到该选择页面时候再令begin为true,之后调用

gameSelect.hard(GameUtil.level);由对应的不同难度等级level进行总雷数以及地图格子数目的设计。

注意之后要执行diapose()操作令该窗口在选择完毕后就关闭,否则打开游戏界面后该界面将仍会存在,之后由于难度选择相当于重开了一局游戏故而重新获取开始时间并令棋子数目FLAG—NUM为0,重新调用两个reGame()方法,并调用launch()方法实现具体的页面设计以及规则设计.

最后为最重要的paint方法的叙述,首先该方法的表达形式必须如下所示:

@Override
    public void paint(Graphics g) {//这个方法是已经定义完成的重写方法,只有使用Graphics类才能实现划线的操作
}

该paint()方法会由repaint方法不断调用,来实现绘图,绘制的具体图像会随着具体参数的改变而在40ms后进行改变,对于该paint()方法也要先进行游戏状态的判定若state为3表示处于难度选择状态,此时令画笔颜色为白色,并用g.fillRect(0,0,500,500)表示该白色画笔要在图上画出左上角坐标为(0,0)的像素为500*500的一个矩形就是难度选择界面,之后调用gameSelect.painSelf(g)即用该设置好的画笔画出该难度选择页面的其它东西,具体代码参考GameSelect类的具体代码。如果此时游戏状态不是难度选择状态,则先对之前设计的Image类的对象offScreenImage进行初始化为大小为wigth*higth的窗体,其实该对象就是整个页面的背景板,之后的底层与顶层的设计都是在这个背景上通过划线以及设定图片位置和图片是否可见来实现的,之后为该背景板自己定义一个画笔gImage设其颜色为橘色orange表示画出来的背景板就是黄色的,之后代码gImage.fillRect(0,0,wigth,height)是为了令橘色的效果能够填满整个背景板,之后调用mapBotton.paintSelf(gImage)与mapTop.paintSelf(gImage)来实现对于该窗口的底层以及顶层的绘制,具体绘制方法开具体MapBottom类以及MapTop类的paintSelf(g)方法的代码实现,其会根据不断变化的静态变量来绘制对应的图形,但是注意此时画布位置与窗口的位置并没有对其,将会出现闪烁现象,即因为两个窗口不重合导致,故而使用g.drawImage(offScreenImage,0,0,null)表示将创建好的背景板放到与窗口位置一样的区域(也是左上角为(0,0)且二者大小一致故而重合了)此时整个窗口甚至程序其实都已经完毕。

只差利用主方法main()调用GameWin类的对象以及其方法launch()来进行程序的运行.

具体如下:

public static void main(String[] args) {//主函数
        GameWin gameWin=new GameWin();//声明GameWin类的对象gamewin
        gameWin.launch();//调用定义的launch()方法也就是从此刻开始游戏正式开始
    }

其实最主要的还是在调用gameWin一旦发生鼠标点击,将自动执行其中的鼠标点击相关事件,令相应的状态做出改变,通过状态的改变来实现图层的改变,该鼠标事件只是令其他paintSelf()事件可以知道是否有鼠标进行点击且点击的位置是哪里而已,主要还是通过MapButtom与MapTop类的相关规则来实现具体图层由于变量的改变而进行的变化。其中的launch()除了先有窗口的设计,还包含鼠标事件其中是循环调用paint()方法而paint()方法则包括底层与顶层的具体绘制方法。从而实现该程序的设计,具体的规则看代码与规则的设计即可。

扫雷问题即得到结局!具体的在于paint()方法(为绘图的实现方式)与 

this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {(该方法是当发生鼠标点击事件将要发生的事件)

主要就是通过这两个方法进行的实现,其它的具体代码,看代码块进行了解即可。(想要图片的可以联系,也可以自己在网上进行搜索)

通过该游戏小程序的设计:达到了对于绘图以及图形的绘画的具体了解,实现由鼠标点击根据一定规则来实现图形变化的操作!达到了自己的要求!

举报

相关推荐

扫雷游戏源代码

扫雷小游戏

扫雷游戏初学

C++——扫雷游戏

扫雷小游戏详解

0 条评论