news 2026/10/2 12:40:45

Java 扫雷游戏实战:从随机布雷到真正“能玩“的扫雷

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 扫雷游戏实战:从随机布雷到真正“能玩“的扫雷

一、整体思路

扫雷的核心其实就是一张二维表格:

  1. 用一个LINE * LINE的二维数组保存数据,-1表示雷,0~8表示这个格子周围有几个雷;
  2. 随机生成坐标往数组里放雷(练习1);
  3. 遍历每个非雷格子,数一数它周围 8 个方向有几个雷,把结果存进数组(练习2);
  4. 遍历数组,把雷画成红色圆,把数字画成蓝色文字(练习2)。

数据(数组)和显示(绘制)是分开的:先算数据,再照着数据画图,这也是游戏开发里很常见的"数据与视图分离"思想。

二、涉及的知识点

知识点在本项目中的作用
JFrame窗口容器默认边框布局,承载整个游戏界面
JPanel面板中间区域的面板,同时作为画布
Graphics画笔画线(网格)、画圆(雷)、画字符串(数字)
二维数组保存雷区数据,一维是行(Y)、二维是列(X)
Math.random()生成随机坐标,实现随机布雷
边界处理Math.max / Math.min遍历周围格子时防止数组越界

三、关键代码讲解

publicclassminesweeper1{publicstaticclassMineSweeper{publicintLINE=10,X0=50,Y0=50,SIZE=30;// 行列数、网格原点、格子边长publicint[][]thunderArr=newint[LINE][LINE];// 雷区数据:-1表示雷,0~8表示周围雷数publicint[][]stateArr=newint[LINE][LINE];// 格子状态:0未翻开 1已翻开 2插旗publicGraphicsg;publicbooleangameOver=false;// 游戏是否结束publicintopenedCount=0;// 已翻开的非雷格子数量publicvoidinitUI(){// JFrame 默认是边框布局JFramejf=newJFrame();jf.setSize(800,800);jf.setTitle("扫雷游戏(左键翻开 / 右键插旗)");jf.setDefaultCloseOperation(3);// 退出进程jf.setLocationRelativeTo(null);// 居中显示JPaneljPanel=newJPanel();jPanel.setBackground(Color.GREEN);jf.add(jPanel,BorderLayout.CENTER);jf.setVisible(true);// 必须先让窗口可见,才能拿到画笔// 获取画笔g=jPanel.getGraphics();// 延时:等窗口真正显示完成,否则可能画不出东西try{Thread.sleep(100);}catch(InterruptedExceptione){thrownewRuntimeException(e);}// 绘制网格:画 LINE+1 条横线和竖线,围出 LINE * LINE 个格子drawGrid();setThunder();// 练习1:随机布雷setNumber();// 练习2:计算雷周围格子的数字drawCoverAll();// 把所有格子盖上"盖子",模拟真实扫雷的初始状态printArr();// 控制台打印雷区数据(作弊用的^-^,验证结果)// 鼠标监听器:处理左键翻开、右键插旗jPanel.addMouseListener(newMouseAdapter(){@OverridepublicvoidmouseClicked(MouseEvente){if(gameOver){return;// 游戏结束后不再响应}// 像素坐标换算成数组下标:列是x轴,行是y轴intcol=(e.getX()-X0)/SIZE;introw=(e.getY()-Y0)/SIZE;// 点在网格外面,直接忽略if(row<0||row>=LINE||col<0||col>=LINE){return;}// 右键:插旗 / 取消旗if(e.getButton()==MouseEvent.BUTTON3){toggleFlag(row,col);// 左键:翻开格子}elseif(e.getButton()==MouseEvent.BUTTON1){// 点到雷:游戏结束if(thunderArr[row][col]==-1){boom();}else{openCell(row,col);checkWin();}}}});}/** 画网格:LINE+1 条横线和竖线围出 LINE*LINE 个格子 */publicvoiddrawGrid(){for(inti=0;i<=LINE;i++){g.drawLine(X0,Y0+i*SIZE,X0+LINE*SIZE,Y0+i*SIZE);// 横线g.drawLine(X0+i*SIZE,Y0,X0+i*SIZE,Y0+LINE*SIZE);// 竖线}}/** 给所有格子盖上灰色"盖子",模拟真实扫雷的开局 */publicvoiddrawCoverAll(){for(inti=0;i<LINE;i++){for(intj=0;j<LINE;j++){drawCover(i,j);}}}/** 画一个格子的"盖子" */publicvoiddrawCover(introw,intcol){g.setColor(Color.GRAY);g.fillRect(X0+col*SIZE+1,Y0+row*SIZE+1,SIZE-2,SIZE-2);}/** * 练习1:随机布雷 * 随机生成坐标(x, y),如果该位置没有雷(不等于-1)就放一个雷 * 已有雷的位置跳过,保证恰好放 LINE 个雷 */publicvoidsetThunder(){intthunder=0;// 记录放了多少个雷while(thunder<LINE){intx=(int)(Math.random()*LINE);// 列:0 ~ LINE-1inty=(int)(Math.random()*LINE);// 行:0 ~ LINE-1if(thunderArr[y][x]!=-1){thunderArr[y][x]=-1;// -1 表示雷thunder++;}}}/** * 练习2:给每个不是雷的格子计算周围 8 个方向雷的数量 * 关键点:遍历周围格子时要防止数组越界 * 行的范围:Math.max(0, i-1) ~ Math.min(LINE-1, i+1) * 列的范围:Math.max(0, j-1) ~ Math.min(LINE-1, j+1) */publicvoidsetNumber(){for(inti=0;i<LINE;i++){// 行for(intj=0;j<LINE;j++){// 列if(thunderArr[i][j]==-1){continue;// 雷的格子不需要数字}intcount=0;// 记录周围雷的数量// 检查 (i, j) 周围一圈(包括自己,但自己不是雷,不影响计数)for(intr=Math.max(0,i-1);r<=Math.min(LINE-1,i+1);r++){for(intc=Math.max(0,j-1);c<=Math.min(LINE-1,j+1);c++){if(thunderArr[r][c]==-1){count++;}}}thunderArr[i][j]=count;}}}/** * 翻开一个格子(递归实现"空白扩散") * 如果翻开的格子是 0(周围没有雷),就把周围一圈也自动翻开, * 周围如果还是 0 就继续扩散——这就是真实扫雷里的"一键展开" */publicvoidopenCell(introw,intcol){// 越界或者已经翻开/插旗的格子,直接返回(递归的出口)if(row<0||row>=LINE||col<0||col>=LINE){return;}if(stateArr[row][col]!=0){return;// 0=未翻开才继续,已翻开(1)或插旗(2)都不动}// 翻开这个格子stateArr[row][col]=1;openedCount++;drawOpen(row,col);// 周围没有雷 -> 自动翻开周围一圈(递归扩散)if(thunderArr[row][col]==0){for(intr=Math.max(0,row-1);r<=Math.min(LINE-1,row+1);r++){for(intc=Math.max(0,col-1);c<=Math.min(LINE-1,col+1);c++){openCell(r,c);}}}}/** 把翻开的格子画出来:擦掉灰盖子,露出背景,有数字就画数字 */publicvoiddrawOpen(introw,intcol){// 先用背景色把"盖子"擦掉g.setColor(Color.GREEN);g.fillRect(X0+col*SIZE+1,Y0+row*SIZE+1,SIZE-2,SIZE-2);// 数字大于 0 才画(真实扫雷里 0 就是空白)if(thunderArr[row][col]>0){g.setColor(Color.BLUE);g.setFont(newFont("宋体",Font.BOLD,16));intpx=X0+col*SIZE+SIZE/2;// 格子中心的 xintpy=Y0+row*SIZE+SIZE/2;// 格子中心的 yg.drawString(thunderArr[row][col]+"",px-5,py+6);}}/** 右键:插旗 / 取消旗 */publicvoidtoggleFlag(introw,intcol){intpx=X0+col*SIZE+SIZE/2;// 格子中心intpy=Y0+row*SIZE+SIZE/2;if(stateArr[row][col]==0){// 未翻开 -> 插旗:画一面小红旗(旗杆 + 三角旗面)stateArr[row][col]=2;g.setColor(Color.BLACK);g.fillRect(px-1,py-8,2,16);// 旗杆g.setColor(Color.RED);g.fillPolygon(newint[]{px+1,px+9,px+1},// 三角旗面newint[]{py-8,py-4,py},3);}elseif(stateArr[row][col]==2){// 已插旗 -> 取消,重新盖上盖子stateArr[row][col]=0;drawCover(row,col);}// 已翻开的格子(1)右键无效}/** 点到雷了:翻开所有雷,显示游戏结束 */publicvoidboom(){gameOver=true;// 把所有的雷都显示出来:自己踩的画红色,其它的画黑色for(inti=0;i<LINE;i++){for(intj=0;j<LINE;j++){if(thunderArr[i][j]==-1){intpx=X0+j*SIZE+SIZE/2;intpy=Y0+i*SIZE+SIZE/2;g.setColor(Color.BLACK);g.fillOval(px-8,py-8,16,16);}}}g.setColor(Color.RED);g.setFont(newFont("宋体",Font.BOLD,30));g.drawString("砰!你踩到雷了,游戏结束",X0,Y0-15);}/** 判断是否获胜:翻开的非雷格子数 == 总格子数 - 雷数 */publicvoidcheckWin(){if(openedCount==LINE*LINE-LINE){gameOver=true;g.setColor(Color.RED);g.setFont(newFont("宋体",Font.BOLD,30));g.drawString("恭喜你,扫雷成功!",X0,Y0-15);}}// 测试:打印二维数组publicvoidprintArr(){for(inti=0;i<thunderArr.length;i++){// 调用二维长度for(intj=0;j<thunderArr[0].length;j++){System.out.print(thunderArr[i][j]+" ");}System.out.println();}System.out.println();}publicstaticvoidmain(String[]args){MineSweeperms=newMineSweeper();ms.initUI();}}}

四、总结

这篇文章完整实现了一个"能玩"的扫雷,几个关键收获:

  1. 拿画笔的时机:setVisible(true)之后再getGraphics(),并适当延时;
  2. 网格要画 N+1 条线才围出 N 个格子;
  3. 随机布雷要先判断后放置,避免重复覆盖;
  4. 周围遍历用Math.max/Math.min收紧边界,一行代码防越界;
  5. 数组下标与屏幕坐标是行列互换的,绘制和鼠标点击的换算互为逆运算;
  6. 双数组设计:thunderArr存答案、stateArr存进度,数据与视图分离;
  7. 空白扩散用递归:出口(越界/已翻开)+ 单步翻开 + 周围递归,注意用状态数组防止死循环。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 12:39:44

通达信起爆启动点指标公式源码副图

EMA13:EMA(C,13);EMA34:EMA(C,34);EMA55:EMA(C,55);EMA120:EMA(C,120);ACB1:(C-REF(C,1))/REF(C,1)*100>0.5;ACB2:V>MA(V,5)*0.6;ACB3:COUNT(crOSS(EMA13,EMA55),3)1;ACB4:Abs(EMA34-EMA55)/EMA55< 0.03;ACB5:EMA55<EMA120 AND EMA55>REF(EMA55,1);峰火:FILTER(…

作者头像 李华
网站建设 2026/10/2 12:39:06

驭龙社徐一带领团队参与生物医药产业转化峰会

江苏苏州的生物医药产业园里&#xff0c;一场聚焦创新药产业转化的投资对接会正在举办&#xff0c;没有对外公开大规模宣传&#xff0c;所有参会者都是深耕生物医药产业多年的一线研发人员和产业投资人。徐一带着团队全程参与了这场没有媒体在场的深度交流&#xff0c;大家不用…

作者头像 李华
网站建设 2026/10/2 12:38:28

铜管表冷器进出水温差对制冷影响大吗?

铜管表冷器进出水温差对制冷效果影响极大&#xff0c;温差是否合理直接决定换热效率、降温速度与除湿能力&#xff0c;是表冷器稳定运行、发挥最佳制冷性能的核心关键。The inlet and outlet water temperature difference of copper tube cooling coils has a great impact on…

作者头像 李华
网站建设 2026/10/2 12:38:02

小团定制游旅行社怎么选 正规资质齐全的服务商挑选全攻略

近年来&#xff0c;江西旅游热度持续攀升&#xff0c;庐山云雾、婺源花海、景德镇窑火、武功山云海成为社交平台上的热门目的地。随着游客消费升级&#xff0c;越来越多人不再满足于上车睡觉、下车拍照的传统大团模式&#xff0c;转而搜索江西小团定制游江西纯玩不购物旅行社江…

作者头像 李华
网站建设 2026/10/2 12:35:36

搭建一个AI智能体用什么技术?TaoToken统一Key接入实战拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华