我在不要更新挑战中坚持了一年,你也来试试吧(咕咕咕)!

  好言归正传,本次更新带来的是经典游戏扫雷,基于JavaFX实现。篇幅有限,文章主要介绍核心操作实现,不会列出所有代码。需要完整源码或是想预览最终效果,可以点击下方链接。后续会逐步更新细节实现方面的内容,将来吧反正(肯定不鸽!)

 视频演示:

    https://www.bilibili.com/video/BV1jh4y1u7ad

  源码(项目所使用的JDK为1.8版本):

    1. GitHub:https://github.com/xiao-qi-w/MineSweeper
    2. 百度网盘&提取码:https://pan.baidu.com/s/1GqGbfCdluc1yrrniAGh7SQ?pwd=abcd

  如果您已经看过视频,或是成功运行代码,相信对本项目和扫雷已经有了初步认知。如果您是直接阅读的本篇文章,这里也提供了在线的扫雷入口,方便您快速了解:扫雷游戏网页版 - Minesweeper(非本人制作,仅分享)

  怎么样,是否找回了那些年在微机课上偷偷玩扫雷的快乐。总之不管您之前有没有玩过,我建议先熟悉下它的规则和操作,本项目主要是围绕这些内容编写。

规则:

  1. 扫雷游戏是在一个方格网格中进行的,其中包含了地雷和数字。
  2. 目标是清除所有非地雷方格而不触发地雷。

操作:

  1. 游戏开始时,你会看到一个方格网格,其中的方格是覆盖的。
  2. 你可以通过鼠标左键点击一个方格来揭开它。如果揭开的方格是地雷,游戏结束,你输了。
  3. 如果揭开的方格是数字,它会显示周围相邻方格中地雷的数量。
  4. 如果揭开的方格是空白方格(数字为0),它会自动揭开相邻的空白方格和数字方格,直到边界或者遇到数字方格为止。
  5. 如果你认为某个方格是地雷,你可以使用鼠标右键进行标记。标记的方格会显示一个旗帜图标,表示你认为该方格是地雷。
  6. 如果你揭开了所有非地雷方格,游戏胜利。

了解完这些,让我们尝试使用代码来实现它。

首先是数据来源的问题。每生成一局新游戏,都有对应的地雷数字分布记录,用于指导你推断哪些地方是数字,哪些地方是地雷。考虑到游戏界面行列整齐排放的格子,用二维数组存取对应数据最直观易懂。那么选定数据结构后,如何生成初始数据呢?鉴于每局游戏的数据几乎不会重复,如果只靠我们预输入的数据,没玩几局就腻了。为此可以采用随机生成数据的方式,我的做法如下:

/**
* 生成新游戏的地图数据
*/
public void init() {
// 用于记录地雷的位置, 避免重复选择
HashSet<Integer> set = new HashSet();
// 确定随机数据范围
int count = height * width;
// 开始随机
for (int rest = bomb; rest > 0; ) {
int index = rand.nextInt(count);
// 如果当前位置可以设置为地雷, 标记该位置, 地雷剩余个数减一
if (!set.contains(index)) {
set.add(index);
map[index / width][index % width] = BOOM;
rest -= 1;
}
}
// 统计地雷分布情况
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
if (map[i][j] != BOOM) {
map[i][j] = countBomb(i, j);
}
}
}
}

  :map是用于存储数据的二维数组;width和height分别表示横向和纵向的格子数,即map每个维度的长

  仅生成地雷位置还不够,我们还需要知道地雷周围对应的数字,上面代码中的countBomb方法负责完成这部分工作,具体实现如下:

/**
* 统计当前格子周围的地雷个数
*
* @param x 横坐标
* @param y 纵坐标
* @return count 地雷个数
*/
public int countBomb(int x, int y) {
int count = 0;
// 依次判断周围格子是否存在地雷
for (int i = 0; i < 8; ++i) {
int newX = x + positions[i][0];
int newY = y + positions[i][1];
if (newX > -1 && newX < height && newY > -1 && newY < width && map[newX][newY] == BOMB) {
count += 1;
}
}
return count;
}

  :positions为相对方位坐标数组,用于计算周围八个格子的坐标;BOMB是int常量,值为9,表示地雷

  这样就有了初始游戏数据,仅有这个还不够,我们最终要把它展示在屏幕上。不妨想一下,在绘制界面的过程中,我们可以根据数据的不同来确定某个格子具体显示为地雷,数字或是空白。比如0-8表示周围地雷个数统计,9表示地雷。可是游戏一开始全是未知的格子,难道我们要再设置一个相应的boolean数组记录格子是否被点开吗?这样做虽然可行,但我觉得较为麻烦,所以我是这样设计的:

// 数字常量 [0:空白格, 9:地雷]
public static final byte BLANK = 0;
public static final byte BOMB = 9;
// [20:旗帜标记判断, 40:问号标记判断]
public static final byte FLAG = 20;
public static final byte GUESS = 40;
// [99:边界标记, 超过这个数字代表当前格子已被点开]
public static final byte BOUND = 99;

  对于可能用于逻辑判断的量,将它们定义为常量,这样在代码中就不会出现 if ( 变量 == 9 ),却不清楚‘9’是什么含义的情况,避免降低可读性。其次是格子是否被点击过的问题,可以设置一个边界值进行区分。因为地雷和周围数字只占用了很少一部分整型数据,所以可以根据数据是否超过某个范围来判断是否被点击过。最后是右键标记问题,我印象里的操作是右键一次采用旗帜标记,两次采用问号标记,所以设置两个对应常量用于判断。下面是点击过程中的逻辑判断代码:

// 获取按钮
Button button = (Button) buttons.get(row * GAME.width + column);
// 根据左右键设置不同响应逻辑
if (event.getButton() == MouseButton.SECONDARY) {
// 定义图片路径
String imagePath = null;
// 右键对应行为
if (map[row][column] >= GUESS) {
// 不设置图片, 还原雷的数目
map[row][column] -= GUESS;
REST_FLAG += 1;
} else if (map[row][column] >= FLAG) {
// 如果已经被标记, 路径更换为问号图片, 表示不确定
imagePath = GUESS_IMG;
map[row][column] = map[row][column] - FLAG + GUESS;
} else {
// 未被标记过, 判断是否还有可用标记
if (REST_FLAG > 0) {
imagePath = FLAG_IMG;
map[row][column] += FLAG;
REST_FLAG -= 1;
}
}
button.setStyle("-fx-background-size: contain; -fx-background-image: url(" + imagePath + ")");
} else {
// 左键对应行为
if (map[row][column] <= BOUND && map[row][column] >= FLAG) {
// 如果被标记, 则先清空标记
map[row][column] -= map[row][column] >= GUESS ? GUESS : FLAG;
REST_FLAG += 1;
button.setStyle("-fx-background-size: contain; -fx-background-image: url(" + null + ")");
} else {
// 更新点击过的数据
mineSweeper.clickCell(row, column); if (STATE == UNSURE) {
// 统计非雷格子已点开数目
int count = 0;
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] > BOUND) {
Button btn = (Button) buttons.get(i * GAME.width + j);
count += 1;
int value = map[i][j] - 100;
if (value != BLANK) {
// 消除空白填充
btn.setPadding(new Insets(0.0));
// 设置粗体和字体颜色
btn.setFont(Font.font("Arial", FontWeight.BOLD, GAME.numSize));
btn.setTextFill(NUMS[value - 1]);
btn.setText(value + "");
}
btn.setStyle("-fx-border-color: #737373; -fx-opacity: 1; -fx-background-color: #ffffff");
btn.setDisable(true);
}
}
}
// 判断全部非雷格子是否全部点开
if (count + GAME.bomb == GAME.width * GAME.height) {
STATE = WIN;
}
} else if (STATE == LOSS) {
// 游戏失败, 显示所有地雷位置
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] == BOMB) {
Button btn = (Button) buttons.get(i * GAME.width + j);
btn.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + UNEXPLODED_IMG + ")");
}
}
}
button.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + EXPLODED_IMG + ")");
}
}
}

  注:阅读时请先忽略掉界面控件相关的操作,仅需关注map数据的变化

  这段代码根据左或右键点击来进行对应的操作,同时引出了新的问题,点击格子后不总是只更新它自身的数据,像操作中说的,如果它是空白格 (数据为0),还需要展开它周围的格子,这个过程是怎么进行的呢?它在上述代码中体现为mineSweeper.clickCell(row, column); 具体实现如下:

/**
* 展开与当前位置相连的所有空白区域, 包括包裹这层空白区域数字边界
*
* @param x 横坐标
* @param y 纵坐标
*/
public void clickCell(int x, int y) {
if (map[x][y] == BLANK) {
map[x][y] += 100;
// 点击到空白区域, 递归判断周围8个方向
for (int i = 0; i < 8; i += 1) {
int newX = x + positions[i][0];
int newY = y + positions[i][1];
if (newX > -1 && newX < height && newY > -1 && newY < width
&& map[newX][newY] != BOMB && map[newX][newY] < FLAG) {
// 递归展开非雷和未标记区域
clickCell(newX, newY);
}
}
} else if (map[x][y] == BOMB) {
// 点击到地雷, 游戏状态设置为失败
STATE = LOSS;
} else if (map[x][y] < BOUND) {
// 点击到数字格, 数值加100用于区分是否已被点开
map[x][y] += 100;
}
}

  至此,我们基本完成了扫雷的核心内容,剩余的功能如计时,成绩排行,难度设置,胜负判定等只能说是使这个玩法更像是完整的游戏。因为本文是概述性质的,所以这些功能和界面统一放在后续文章里结合着讲。

———————————————我———是———分———割———线——————————————

  隔了这么久再次写博客,都不知道从何写起讲些什么了。如果文章或者演示里有哪些不清楚的地方,还请留意后续更新。另外GitHub的代码我应该还会更新,如果有不足之处欢迎在issue里指出。这次的项目拖拖拉拉大概进行了一个月吧,实际用来写代码的时间也不能算多,拖延症大抵是没救了(悲)希望下次更新不是明年吧

基于JavaFX的扫雷游戏实现(一)——整体概述的更多相关文章

  1. 基于jQuery经典扫雷游戏源码

    分享一款基于jQuery经典扫雷游戏源码.这是一款网页版扫雷小游戏特效代码下载.效果图如下: 在线预览   源码下载 实现的代码. html代码: <center> <h1>j ...

  2. (转载)WinformGDI+入门级实例——扫雷游戏(附源码)

    本文将作为一个入门级的.结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游戏界面让我从 ...

  3. C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式

    C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...

  4. WinformGDI+入门级实例——扫雷游戏(附源码)

    写在前面: 本文将作为一个入门级的.结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游 ...

  5. 基于JavaFX图形界面演示的迷宫创建与路径寻找

    事情的起因是收到了一位网友的请求,他的java课设需要设计实现迷宫相关的程序--如标题概括. 我这边不方便透露相关信息,就只把任务要求写出来. 演示视频指路: 视频过审后就更新链接 完整代码链接: 网 ...

  6. Pomelo:网易开源基于 Node.js 的游戏服务端框架

    Pomelo:网易开源基于 Node.js 的游戏服务端框架 https://github.com/NetEase/pomelo/wiki/Home-in-Chinese

  7. 洛谷 P2670 扫雷游戏==Codevs 5129 扫雷游戏

    题目描述 扫雷游戏是一款十分经典的单机小游戏.在n行m列的雷区中有一些格子含有地雷(称之为地雷格),其他格子不含地雷(称之为非地雷格).玩家翻开一个非地雷格时,该格将会出现一个数字——提示周围格子中有 ...

  8. 基于HTML5的SLG游戏开发(序)

          2012年前后,HTML5游戏凭借跨平台.易移植.部署简单.节省成本等优点被炒的火热,经过一两年的快速发展,市场出现了一些成功地HTML5游戏产品,像磊友的<修仙三国>,神奇时 ...

  9. [置顶] 使用红孩儿工具箱完成基于Cocos2d-x的简单游戏动画界面

    [Cocos2d-x相关教程来源于红孩儿的游戏编程之路CSDN博客地址:http://blog.csdn.net/honghaier 红孩儿Cocos2d-X学习园地QQ3群:205100149,47 ...

  10. 原生javascript扫雷游戏

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

随机推荐

  1. [Shell] Windows上支持Linux Shell的工具/方法

    0 概述 1 方式一 : Windows Terminal 局限性: 不支持 xargs等命令 支持sed,find等命令 安装方式 安装Windows Terminal的最简单方法是通过Micros ...

  2. 四月十九号java基础知识

    1.总括:类的继承是使用已有的类为基础派生出新的类.通过类继承的方式,便能开发出新的类,而不需要编写相同的程序代码,所以说类的继承是程序代码再利用的概念抽象与接口都是类概念的扩展.通过继承扩展出的子类 ...

  3. day12:闭包函数&匿名函数(lambda)

    闭包函数 闭包函数的定义: 如果内函数使用了外函数的局部变量并且外函数把内函数返回出来的过程 叫做闭包里面的内函数是闭包函数 一个简单的闭包函数示例: def songyunjie_family(): ...

  4. CommunityToolkit.Mvvm系列文章导航

    包 CommunityToolkit.Mvvm (又名 MVVM 工具包,以前名为 Microsoft.Toolkit.Mvvm) 是一个现代.快速且模块化的 MVVM 库. 它是 .NET 社区工具 ...

  5. Kubernetes入门实践(Job/CronJob)

    基于Pod的设计理念,Kubernetes有两种对象Job和CronJob Job和CronJob组合了Pod,实现了对离线业务的处理.如Nginx和busybox,分别代表了Kubernetes里的 ...

  6. 安装vue-lic

    vue-cli是Vue.js开发的标准工具.它简化了程序员基于webppack创建工程化的Vue项目的过程.引用自vue-cli官网上的一句话:程序员可以专注在撰写应用上,而不必花好几天去纠结webp ...

  7. 开心档之MySQL 连接

    MySQL 连接 使用mysql二进制方式连接 您可以使用MySQL二进制方式进入到mysql命令提示符下来连接MySQL数据库. 实例 以下是从命令行中连接mysql服务器的简单实例: [root@ ...

  8. java指定时间失效Calendar

    获取第二天的1:30的毫秒数 public static Long getEveryDayTime() { Calendar calendar = Calendar.getInstance(); ca ...

  9. 你还弄不清xxxForCausalLM和xxxForConditionalGeneration吗?

    Part1基本介绍 大语言模型目前一发不可收拾,在使用的时候经常会看到transformers库的踪影,其中xxxCausalLM和xxxForConditionalGeneration会经常出现在我 ...

  10. Sql Server 数据库事务与锁,同一事务更新又查询锁?期望大家来解惑

    我有一个People表,有三行数据: 如果我们没详细了解数据库事务执行加锁的过程中,会不会有这样一个疑问:如下的这段 SQL 开启了事务,并且在事务中进行了更新和查询操作. BEGIN TRAN up ...