扫雷小游戏(两小时完成版)
游戏介绍:一共三个难度分别为简单(88,5颗雷),中等(1212,15颗雷),困难(5*15,30颗雷);
功能介绍:可进行难度选择、窗体内包括雷区、随机布置地雷、不能踩到地雷、鼠标单击重新开始可清零重新选择难度开始、游戏状态下可以实现通过单机难度选择用来改变难度、左键用于打开格子、右键是辅助功能插旗子、再次点击可以消除旗子、中键可以画鸭。
知识总结: 变量、数据类型、判断语句、循环结构、数组、二维数组、递归、简单窗口创建、图形图片绘制、鼠标事件
一、创建四个类(MineClearanceUI、MineListener、MinePanel、GameSelect)一个接口(Mineconfig)
(1)MineClearanceUI:窗口主函数入口
(2)MineListener:监听器,调用minePanel的画笔用来画地雷数字等;
(3)MinePanel:重绘地雷、数字、棋子等元素。
(4)Mineconfig;接口用来储存不变的定量()
//接口代码
public interface Mineconfig {
int X=20;
int Y=20;
//C代表行列 也代表难度选择
int C=8;
int R=8;
int C2=12;
int R2=12;
int C3=15;
int R3=15;
//间隔
int SIZE=25;
//雷
Image imagelei = new ImageIcon("D:\\图片\\扫雷\\HandGrenades.png").getImage();
//棋子
Image imageFlags = new ImageIcon("D:\\图片\\扫雷\\flag.gif").getImage();
//顶层的这苍梧
Image imagetop = new ImageIcon("D:\\图片\\扫雷\\top.gif").getImage();//顶层图片
//彭于晏
Image imagePYY = new ImageIcon("D:\\图片\\扫雷\\PYY.jpg").getImage();
//鸭子
Image imageNoflag = new ImageIcon("D:\\图片\\扫雷\\noflag.png").getImage();
}
二、简单窗口绘制,以及添加鼠标监听器和动作监听器
(1)绘制一个宽高为(260,330)的窗体
(2)往窗体上添加两个面板(MinePanell,btnPanel)
1)MinePanel以及btnPanel上添加鼠标监听器和动作监听器(ml),以及按钮(名称、颜色)
2)MinePanle以及btnPanel需要设置成边框布局(BorderLayout一北一南),其中南北的边框布局只能设置高度。
(3)在面板MinePanel上获取画笔,并把画笔传到MIneListener中用于画雷等
public class MineClearanceUI {
//主函数
public static void main(String[] args) {
MineClearanceUI mc=new MineClearanceUI();
mc.mineClearance();
}
//监听器对象
MineListener ml=new MineListener();
public void mineClearance(){
JFrame jf=new JFrame();
//限制窗口大小 可通过jfX,jfY来修改
jf.setSize(260,330);
jf.setTitle("扫雷");
//可关闭 jf.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
//minPanel 为扫雷画板,注意不要创建两个同个类
MinePanel minePanel=new MinePanel();//扫雷面板继承了JPanel所一不要同时JPanel MPnael同时存在,
// JPanel MPnael要是同时存在的话会想到那个于重新创建了一个对象每个JPanel的属性和方法并不是相通的,在MinePanel里就画不出网格。
//按钮面板
JPanel btnJpanel=new JPanel();
//尺寸
Dimension dim=new Dimension(0,250);
Dimension dim1=new Dimension(0,40);
minePanel.setPreferredSize(dim);
btnJpanel.setPreferredSize(dim1);
//设置颜色
minePanel.setBackground(Color.lightGray);
btnJpanel.setBackground(Color.pink);
//窗体添加两个面板:面板JPanel默认是流式布局:FlowLayout
//此时注意 窗体JFrame是边框布局:BorderLayout
jf.add(minePanel,BorderLayout.NORTH);
jf.add(btnJpanel,BorderLayout.SOUTH);
initJButton(btnJpanel,ml);//添加动作监听器
jf.setVisible(true);
//面板获取画笔
Graphics g=minePanel.getGraphics();
ml.g=g;
//扫雷面板获取鼠标监听器
minePanel.addMouseListener(ml);
//传数组以及获得该方法;
ml.steMinePanel(minePanel);
//传jf对象用来改变窗体大小
ml.jf=jf;
}
//封装添加按钮的方法
public void initJButton(JPanel jPanel, ActionListener al){
Dimension dim2=new Dimension(100,30);
JButton xbt=new JButton();
JButton rbt=new JButton();
xbt.setPreferredSize(dim2);
rbt.setPreferredSize(dim2);
String name1="选择难度";
String name2="重新开始";
//添加动作监听器
xbt.addActionListener(al);
rbt.addActionListener(al);
//添加名字
xbt.setText(name1);
rbt.setText(name2);
jPanel.add(xbt);//添加是两个组件添加起来
jPanel.add(rbt);
}
}
三、监听器MineListener
(1)定义该有的参数,以及添加监听(ActionListener,MouseListener)和接口(Mineconfig);
1)其中画数字:
(1)建立数组 imagesnum 同来储存(0~8)数字;并且把 图片地址获取;其中字符串+int类型的数字就会转换成对应数字的字符;
static Image[] imagesnum=new Image[9];//存图片
static{
for (int i=1;i<=8;i++){//字符串的加法,字符串+int类型的数字就会转换成对应数字的字符
imagesnum[i]=new ImageIcon("D:\\图片\\扫雷\\num\\"+i+".png").getImage();
}
}
2)传画笔( Graphics )以及把数组( leiArray、topArray)顶层坐标( topx、topy )传到MinePanel类里,到时会用来重绘
public void setGraphics(Graphics g){this.g=g;}
MinePanel minePanel;
public void steMinePanel( MinePanel minePanel){
this.minePanel=minePanel;
minePanel.leiArray=leiArray;
minePanel.topArray=topArray;
minePanel.topx=topx;
minePanel.topy=topy;
}
阶段代码
public class MineListener implements ActionListener, MouseListener, Mineconfig {
JFrame jf;//定义一个窗体,用来做改变窗体的大小
public Graphics g;//传画笔;
int[][] leiArray=new int[C+2][R+2];//用来记录雷区并且重绘雷
int[][] topArray=new int[C+2][R+2];//用来画顶层
int topx,topy;//用来翻牌坐标
int x,y;//获取坐标
int count=0;//用来计数//不能放click方法里面要不然会点一次从新创建一次
int leinum;//用来修改雷数
int countWin;//用来判断输赢
int jfX,jfY;//窗体的宽高
//雷图片
static Image[] imagesnum=new Image[9];//存图片
static{
for (int i=1;i<=8;i++){//字符串的加法,字符串+int类型的数字就会转换成对应数字的字符
imagesnum[i]=new ImageIcon("D:\\图片\\扫雷\\num\\"+i+".png").getImage();
}
}
public void setGraphics(Graphics g){this.g=g;}
MinePanel minePanel;
public void steMinePanel( MinePanel minePanel){
this.minePanel=minePanel;
minePanel.leiArray=leiArray;
minePanel.topArray=topArray;
minePanel.topx=topx;
minePanel.topy=topy;
}
//用来传递MineListenner的监听器
GameSelect gameSelect =new GameSelect();
3)为什么数组int[][] leiArray=new int[C+2][R+2]要加2如图所示
把数组放大2 这样就可以在 画数字的时候防止越界问题的发生,以及对topArray数组的坐标矫正方便很多!即只画上面7*7区域,最外围不画这样就巧妙的避免了越界问题1
(2)画雷(setMine)
1)创建一个新的画雷的封装方法
public void setMine(){
//System.out.println("leinum="+leinum);
//for循环<leinum:表示画雷的个数
for(int k=0;k<leinum;k++) {
//创建一个自由数的类
Random random = new Random();
//能取到的值(数组长度-2)+1由(0-7)到(1-8)这是避免取0,防止画数字时越界//地雷的行数
//这样就能(x,y)的值就能画满整个面板的值
//地雷的列数//为什么取8因为的的二维数组的长度为9,下标 只能是(0-8)
x=random.nextInt(leiArray.length-2)+1;
y=random.nextInt(leiArray.length-2)+1;
//防止雷的重复!!非常重要
if(leiArray[x][y]!=0){
k=k-1;
continue;
}
setBoom(g);//画雷
}
}
//遍历雷的封装
public void setBoom(Graphics g){
//把-1赋值给数组当前leiArray[x][y],并记录为-1
leiArray[x][y] = -1;
//如果上面的nexInt取就会超过该数组的长度,该数组的长度为arr1[9][9],意思是下标的取值范围为(0-8)//以简单为例
//把赋值给二维数组,为什么选10呢?一个空格或者一个数外围最多能够做0-8个;
//如果数组leiArray[x][y]=-1,就画雷
if (leiArray[x][y]==-1){
for (int i = 0; i < leiArray.length; i++) {
for (int j = 0; j < leiArray.length; j++){
g.drawImage(imagelei, x * SIZE, y * SIZE, 20, 20, null);
}
}
}
}
(4)画数字:
//数字封装
public void setNum(){
//+2的原因是因为遍历的数组要比8*8要大一圈,
for (int i=1;i<leiArray.length;i++){
for(int j=1;j<leiArray.length;j++){
if (leiArray[i][j]==-1){//如果在x,y坐标处有雷
setImagesnum(i,j,g);//遍历数字
}
}
}
}
//遍历数字封装
public void setImagesnum(int i,int j,Graphics g){
for (int k=i-1;k<i+2;k++) {//在他的周围(x-1,x+1),(y-1,y+1)遍历上数字
for (int l = j - 1; l < j + 2; l++) {
if (leiArray[k][l] >= 0) {
leiArray[k][l]++;
//相当于count++;下面这个数组储存的是1,就画1,如果没有储存数就不遍历;
if (k > 0 && k < 9 && l > 0 && l < 9&&leiArray[k][l]<9){
//取得值1但是数组的arr1[9][9]=1;这个时候还是会画1;因此数组的长度要<9.
g.drawImage(imagesnum[leiArray[k][l]], k * SIZE, l * SIZE, 15, 15, null);
}
}
}
}
}
阶段性成果:如下图
(5)画遮挡物
//遮挡物封装
public void setObstructions(){
for (int i=1;i<leiArray.length-1;i++){//+2的原因是因为遍历的数组要比8*8要大两圈
for(int j=1;j<leiArray.length-1;j++){
g.drawImage(imagetop,i * SIZE-5, j * SIZE-5, 26, 26, null);
topArray[i][j]=100;
}
}
}
(6)简易版扫雷(1)
把上面的画雷、画数字、画遮挡物的函数放到**public void actionPerformed(ActionEvent e) {}**函数中
public void actionPerformed(ActionEvent e) {
//点击开始游戏,再去绘制
String strs=e.getActionCommand();//鼠标监听获取操作命令
if (strs.equals("选择难度")) {
//画雷 -1代表雷
setMine();
//画数字 [i]代表数字
setNum();
//画遮挡物
setObstructions();
}
阶段性成果:如下图
这样我们简单的难度的底层地雷以及上层遮挡物已经做好了!
(7)创建一个选择难度窗口:选择难度
当点击选择难度时跳出这样的一个窗口。
注意:这里的监听器和原先界面的监听器时一样的
上面有三个按钮(如图):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jImyItqK-1639748156363)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1639742364831.png)]
(1)需要在类GameSelect里定义ActionListener al;//属性或者对象
public class GameSelect {
ActionListener al;//属性或者对象
(2)在类MineListener里面做新创建对象然后在点击“选择难度”的时候把这个窗口弹出
GameSelect gameSelect =new GameSelect();
@Override
public void actionPerformed(ActionEvent e) {
//点击开始游戏,再去绘制
String strs=e.getActionCommand();//鼠标监听获取操作命令
if (strs.equals("选择难度")) {
//this代表的是本类对象(mineListener)
gameSelect.al=this;//传监听器给GameSelect
gameSelect.gameSlect();//显示新创建的创口
}
(3)GameSelect窗口代码如下
//类能够调用方法和属性,对象调用方法必须要在函数里面
public class GameSelect {
ActionListener al;//属性或者对象
public void gameSlect(){
JFrame selectJF=new JFrame();
selectJF.setSize(200,210);
selectJF.setDefaultCloseOperation(2);
selectJF.setLocationRelativeTo(null);
setJButton(selectJF,al);//this 表示本类对象
selectJF.setVisible(true);
}
public void setJButton(JFrame selectJF,ActionListener al){
//尺寸
Dimension dim1=new Dimension(180,50);
JButton easy=new JButton("简单(8*8,5颗雷)");
JButton medium=new JButton("中等(12*12,15颗雷)");
JButton hard=new JButton("困难(15*15,30颗雷)");
//尺寸
easy.setPreferredSize(dim1);
medium.setPreferredSize(dim1);
hard.setPreferredSize(dim1);
//添加监听器
easy.addActionListener(al);
medium.addActionListener(al);
hard.addActionListener(al);
//一定要添加一个布局要不然会被覆盖,JFrame默认是边框布局 BorderLayoiut(南北只设置高度,东西只设置宽度),JPanel默认是流失布局Flowlyout
selectJF.setLayout(new FlowLayout());
selectJF.add(easy);
selectJF.add(medium);
selectJF.add(hard);
}
public void getEasy(){
System.out.println("获取简单难度");
}
public void getMedium(){
System.out.println("获取中等难度");
}
public void getHard(){
System.out.println("获取困难难度");
}
}
(8)实现 简单(88,5颗雷)、中等(1212,15颗雷)、困难(15*15,30颗雷)三中类型的窗口大小变换,以及雷区、数字、遮挡物的绘画
在类MineListener里
GameSelect gameSelect =new GameSelect();
@Override
public void actionPerformed(ActionEvent e) {
//点击开始游戏,再去绘制
String strs=e.getActionCommand();//鼠标监听获取操作命令
if (strs.equals("选择难度")) {
gameSelect.al=this;//传监听器给GameSelect
gameSelect.gameSlect();//显示新创建的创口
}
if (strs.equals("简单(8*8,5颗雷)")){
gameSelect.getEasy();
again();
//简单的数组
leiArray=new int[C+2][R+2];
topArray=new int[C+2][R+2];
//雷数
leinum=5;
//等于5时判赢
countWin=5;
//jf窗体大小
jfX=260;
jfY=320;
//尺寸
Dimension dim=new Dimension(0,240);
minePanel.setPreferredSize(dim);
}
if (strs.equals("中等(12*12,15颗雷)")){
gameSelect.getMedium();
again();
//中等的数组
leiArray=new int[C2+2][R2+2];
topArray=new int[C2+2][R2+2];
//雷数以及=6时判赢
leinum=15;
countWin=15;
//窗体大小以及画板的大小
jfX=370;
jfY=430;
Dimension dim=new Dimension(0,350);//尺寸
minePanel.setPreferredSize(dim);
}
if (strs.equals("困难(15*15,30颗雷)")){
gameSelect.getHard();
again();
//困难的数组
leiArray=new int[C3+2][R3+2];
topArray=new int[C3+2][R3+2];
leinum=30;
countWin=30;
//窗体大小
jfX=435;
jfY=490;
Dimension dim=new Dimension(0,410);//尺寸
minePanel.setPreferredSize(dim);
}
//画雷 -1代表雷
setMine();
//画数字 [i]代表数字
setNum();
//画遮挡物
setObstructions();
minePanel.leiArray=leiArray;
minePanel.topArray=topArray;
jf.setSize(jfX,jfY);//改变窗体大小
if (strs.equals("重新开始")){
//点击重新开始,所有数据清零
again();
}
minePanel.repaint();
}
//重新开始封装
public void again(){
for (int i=1;i<leiArray.length-1;i++) {
for (int j = 1; j < leiArray.length - 1; j++) {
leiArray[i][j] = 0;
topArray[i][j] = 0;
count = 0;
leinum = 0;
countWin = 0;
}
}
}
实现效果如图:
九、实现翻开功能
(1)翻开 空 时实现递归功能:
(2)翻到雷时实现炸雷效果:
(3)右键画旗子(再次右键点击旗子消失):
@Override
//先获取顶层的坐标
public void mousePressed(MouseEvent e) {
//鼠标的坐标
int x=e.getX();
int y=e.getY();
//取整矫正
topx=(x-X+SIZE)/SIZE;//取整用来点击翻开
topy=(y-Y+SIZE)/SIZE;
}
右键画旗子(再次右键点击旗子消失):
//画旗子 topArray[topx][topy]=-2;
if (e.getButton()==MouseEvent.BUTTON3){//点击右键
if (topArray[topx][topy] == 100) {
topArray[topx][topy]=-2;
if (topArray[topx][topy] == -2 && leiArray[topx][topy] == -1) {
count++;
//定义一个count用来判输赢,当雷的坐标和棋子坐标重合数为count的数时,则判赢
System.out.println("count=" + count);
}
//右键再次点击旗子,重绘遮挡物 -100表示遮挡物
}else {topArray[topx][topy] = 100;}
//判输赢countWin时能赢
if (count==countWin){
JOptionPane.showMessageDialog(minePanel,"YouWIN");
}
}
//画鸭子 topArray[topx][topy]=-3;
if (e.getButton()==MouseEvent.BUTTON2){//中键画鸭子
topArray[topx][topy]=-3;//画鸭子标识问号
}
minePanel.repaint();
}
打开递归,炸雷、功能的实现:
@Override
public void mouseClicked(MouseEvent e) {
//打开雷以及递归
if(e.getButton()==MouseEvent.BUTTON1){
setOpen(topx,topy);
}
//打开方法
public void setOpen(int topx,int topy){
//打开的是数字,数在leiArray[][]数组里的值肯定>0
if (leiArray[topx][topy]>0) {
//顶层的数组在该坐标的值变为0,即遮挡物消失
topArray[topx][topy]=0;
}
//当点到雷的时候
if(leiArray[topx][topy]==-1){
//顶层的所有遮挡物全部赋值为0即全部消失
for(int i=1;i<leiArray.length-1;i++){
for (int j=1;j< leiArray.length-1;j++){
//顶层的数组值全部为零
topArray[i][j]=0;
//在雷的位置画彭于晏(炸雷)
if (leiArray[i][j]==-1){
//把值-3赋值给雷数组这样就可以在消失瞬间在雷的位置,画彭于晏
leiArray[i][j]=-10;//-3表示炸出彭于晏
}
}
}
//弹出结束窗口
JOptionPane.showMessageDialog(minePanel,"你惨了,你坠入爱河了!");//放上面会那个重绘 数组的长度 遍
}
//递归的实现,如果leiArray[topx][topy]=0;即直接打开 该坐标范围x∈[ topx - 1,topx + 2),y∈[ topx - 1,topx + 2)一共九个坐标的值
//周围的
if(leiArray[topx][topy]==0&&topx< leiArray.length-1&&topy< leiArray.length-1) {//条件防越界topx,topy的值不能超过数组的范围
for (int i = topx - 1; i>=0&&i < topx + 2; i++) {
for (int j = topy - 1; j>=0&&j < topy + 2; j++) {
//递归,原理是如果顶层遮挡物的值!=0,赋值0给该坐标的数组,这样就能形成扩散现象.
//碰到了数字
if ( topArray[i][j] != 0) {
//打开该数字的顶层遮挡物
topArray[i][j] = 0;
//防止递归的时候超出雷区的范围
if(i>0&&j>0&&i<leiArray.length-1&&j<leiArray.length-1){
//再次调用set方法为了再次做判断
setOpen(i,j);
//递归实现
}
}
}
}
}
}
10、重绘的实现类MinePanel
public class MinePanel extends JPanel implements Mineconfig {
//二维数组用来遍历
public int[][] leiArray;
//画顶层的遮挡物
int[][] topArray;
int topx,topy;
static Image[] imagesnum=new Image[9];//存图片
static{
for (int i=1;i<=8;i++){
imagesnum[i]=new ImageIcon("D:\\图片\\扫雷\\num\\"+i+".png").getImage();
}
}
//重写paint方法
public void paint(Graphics g) {
super.paint(g);
//画线
for (int i = 0; i < leiArray.length-1; i++) {
//(leiArray.length-2)是因为要根据数组的长度来画线
g.drawLine(X, Y + i * SIZE, X + (leiArray.length-2) * SIZE, Y + i * SIZE);
g.drawLine(X + i * SIZE, Y, X + i * SIZE, Y + (leiArray.length-2) * SIZE);
}
//重绘雷 -1
for (int i=0;i<leiArray.length-1;i++){
for (int j=0;j<leiArray.length-1;j++){
if (leiArray[i][j]==-1) {
g.drawImage(imagelei, i * SIZE, j * SIZE, 20, 20, null);
}
//重绘彭于晏
if(leiArray[i][j]==-10){
g.drawImage(imagePYY, i * SIZE-5, j * SIZE-5, 25, 25, null);
}
}
}
//重绘数字
for (int i=1;i<leiArray.length-1;i++){
for (int j=1;j<leiArray.length-1;j++){
//或用双斜杠表示
if (leiArray[i][j]==1||leiArray[i][j]==2||leiArray[i][j]==3||leiArray[i][j]==4||leiArray[i][j]==5||leiArray[i][j]==6
||leiArray[i][j]==7||leiArray[i][j]==8) {
g.drawImage(imagesnum[leiArray[i][j]], i * SIZE, j * SIZE, 15, 15, null);
}
}
}
//重绘的遮挡物;topArray[i][j]==100
for (int i=0;i<topArray.length-1;i++){//+2的原因是因为遍历的数组要比8*8要大两圈
for(int j=0;j<topArray.length-1;j++){
if (topArray[i][j]==100) {
g.drawImage(imagetop, i * SIZE - 5, j * SIZE - 5, 26, 26, null);
}
}
}
//重绘旗子
for (int i=0;i<topArray.length-1;i++){//+2的原因是因为遍历的数组要比8*8要大两圈
for(int j=0;j<topArray.length-1;j++){
//子
if (topArray[i][j]==-2) {
g.drawImage(imageFlags, i * SIZE - 5, j * SIZE - 5, 26, 26, null);
}
//重绘鸭
if(topArray[i][j]==-3){
g.drawImage(imageNoflag, i * SIZE-5, j * SIZE-5, 25, 25, null);
}
}
}
}
}
11、解决提前看到雷 的方法
直接在MineListener把数字雷的 g.drawImage方法注释掉即可,原理是因为只赋值不画,点开的时候再在MinePanel的重绘!
//遍历雷的封装
public void setBoom(Graphics g){
leiArray[x][y] = -1;//如果上面的nexInt取就会超过该数组的长度,该数组的长度为arr1[9][9],意思是下标的取值范围为(0-8)
if (leiArray[x][y]==-1){//把赋值给二维数组,为什么选10呢?一个空格或者一个数外围最多能够做0-8个;
for (int i = 0; i < leiArray.length; i++) {
for (int j = 0; j < leiArray.length; j++) {
// g.drawImage(imagelei, x * SIZE, y * SIZE, 20, 20, null);
}
}
}
}
//遍历数字封装
public void setImagesnum(int i,int j,Graphics g){
for (int k=i-1;k<i+2;k++) {//在他的周围(x-1,x+1),(y-1,y+1)遍历上数字
for (int l = j - 1; l < j + 2; l++) {
if (leiArray[k][l] >= 0) {
leiArray[k][l]++;
//相当于count++;下面这个数组储存的是1,就画1,如果没有储存数就不遍历;
if (k > 0 && k < 9 && l > 0 && l < 9&&leiArray[k][l]<9){
//取得值1但是数组的arr1[9][9]=1;这个时候还是会画1;
// g.drawImage(imagesnum[leiArray[k][l]], k * SIZE, l * SIZE, 15, 15, null);
}
}
}
}
}
结束Good Luck for you!