扫雷游戏设计及代码实现


扫雷

  • ?扫雷介绍
  • 本文内容前提
  • ?test.c文件代码分析
    • 游戏开始。
    • 打印菜单函数
    • 实现game()函数
  • game.h文件代码分析
    • 文件包含
    • define标识符常量
    • ?函数声明
  • game.c文件代码分析
    • 初始化函数
    • 打印棋盘函数
    • ?♂?埋雷函数
    • 排查雷函数
    • ?♂?计算坐标周围雷个数
  • 总结
  • PS
          • test.c
          • game.c
          • game.h

?扫雷介绍
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
扫雷网页版界面:
扫雷游戏设计及代码实现
文章图片

扫雷VS2019编译器实现界面:
扫雷游戏设计及代码实现
文章图片

本文内容前提
  1. 网页版中的扫雷是通过鼠标点击实现,本文是通过输入坐标实现,所以在打印棋盘时加上了每行每列的序号,方便输入。
  2. 本文省略了避免第一次被炸死、非雷处展开以及计算时间等功能,属于扫雷基础,实现基础的底层逻辑,但余下的功能都不难实现,有时间兴趣可以深入研究。
代码封装:
为了实现代码的封装处理,工程中给出三个不同的文件:
  1. test.c文件 - 实现开始进入界面以及简单函数调用等等基础功能。
  2. game.c文件 - 实现主要进行扫雷过程的功能函数,完成test.c文件中调用函数的实现。
  3. game.h文件 - 存放预编译指令、函数声明等等。
?test.c文件代码分析 test.c文件中实现整个游戏的代码进程,将每一个功能封装成函数形式,但不给出函数实现逻辑,使代码精简。
游戏开始。 游戏开始时需要注意:
要在开始打印出游戏菜单,供用于选择。
  1. 输入1 - 进入游戏;
  2. 输入0 - 退出游戏;
  3. 输入其他 - 给出警告,并通过循环让玩家重新输入。
int main() { int input = 0; do {menu(); printf("请选择输入:(0/1)"); scanf("%d", &input); switch (input) {case 1: game(); break; case 0: printf("您已退出游戏\n"); break; default: printf("选择错误,请重试\n"); break; } } while (input); return 0; }

注意细节问题,进入game()函数、退出游戏后要break跳出switch。
这里调用了一个menu函数,用于打印菜单。
打印菜单函数 由于本文介绍的是基于C语言实现的扫雷工程,只制作一个较为简易的菜单,如果有学前端的朋友可以自行搭建较为美观的UI界面。
void menu() { printf("***********************\n"); printf("******1. play******\n"); printf("******0. exit******\n"); printf("***********************\n"); }

【扫雷游戏设计及代码实现】展示效果:
扫雷游戏设计及代码实现
文章图片

实现game()函数 因为这里是test.c文件,所以不会在此文件中将所有的功能实现代码展示,只展示需要实现的功能而调用的函数。
void game() { char mine[ROWS][COLS] = { 0 }; //存放雷的信息 char show[ROWS][COLS] = { 0 }; //存放排查铺的雷的信息 //初始化数组 init_board(mine, ROWS, COLS, '0'); // 0 init_board(show, ROWS, COLS, '*'); // * //打印棋盘 display_board(show, ROW, COL); //埋雷 set_mine(mine, ROW, COL); //display_board(mine, ROW, COL); //开始排雷 find_mine(mine, show, ROW, COL); }

下面一一介绍每个函数实现的功能 :
  • 初始化数组
init_board(mine, ROWS, COLS, '0'); // 0 init_board(show, ROWS, COLS, '*'); // *

由于扫雷需要两个棋盘:
  1. 一个存放雷的底层棋盘。
  2. 一个展示给用户的顶层棋盘。
所以需要定义两个二维数组表示两个棋盘,也就需要分别给这两个二维数组进行初始化,但实现初始化的函数只需要一个,所以这里调用两次同一个函数,但传的参数不同。
提前说明:
这里的ROWS和COLS表示11,ROW和COL表示9。
我们定义的棋盘是9 ? 9规模的,但是在进行排雷的时候,需要在排查坐标上下左右8个坐标进行排查,如果该坐标位于边界,则会导致数组越界的情况,所以需要在定义数组时多定义两行两列。
其中mine数组表示后台存放雷的数组。 - 初始化时全设为 ’ 0 ';
show数组表示展示给用户的数组。 - 初始化时全设为 ’ * ';
棋盘中0表示无雷,而1表示有雷。
注意,这里的0和1是字符。
将mine数组初始化为0表示当前棋盘全部无雷。
  • 打印棋盘函数。
display_board(show, ROW, COL);

打印棋盘函数需要对棋盘做一些处理调整,让他成为我们想要的样子。
比如:
  1. 用数字表示每行每列的位置。
  2. 用 - - - 分割线将每行隔开。
  3. 用 | 分割线将每列隔开。
其中,传参的时候只需要传ROW和COL即可,而需要传ROWS和COLS,因为打印棋盘展示的应该是show数组,是给用户看的所以不需要把我们额外加的两行两列展示出来。
  • 设置雷函数。
set_mine(mine, ROW, COL);

这里要实现的功能是将雷埋进我们已经初始化好了的mine数组中,因为玩家玩游戏时,使用的是9?9的棋盘,所以这里我们传参的时候也只需要传ROW和COL即可。
将雷 - 也就是字符 ‘ 1 ’埋进数组也就是实现替换,后面将详细介绍。
  • 排雷函数。
find_mine(mine, show, ROW, COL);

排雷函数依然是用户执行操作,对想要排查雷的位置输入坐标,对象是9?9的棋盘,传参依然只需要传ROW和COL即可。
注意这里需要传两个数组,因为需要在用户输入坐标后判断改坐标在mine数组里有没有雷,做出相应反应后在show数组上展示并打印在界面上。
game.h文件代码分析 game.h文件包含:
  1. 预处理指令。
  2. 文件包含。
  3. 全局变量。
  4. #define标识符常量等等。
文件包含
#include #include #include

解释:
  1. 标准输入输出函数几乎任何都会用到,所以在game.h文件里包含,其他文件只需要包含game.h文件即可。
  2. “ stdlib.h ”在设置伪随机值得时候会用到,后面介绍game.h文件的时候细讲。
  3. “ time.h ”是在设置伪随机值时使用的时间戳函数,需要包含该文件之后使用。
define标识符常量
#define ROW 9 #define COL 9 #define ROWS ROW + 2 #define COLS COL + 2#define EASY_COUNT 10

ROW、COL、ROWS和COLS在前面已经提到:
  1. ROW - COL :表示棋盘真正的大小。
  2. ROWS - COLS :为了不让数组越界而多设定了两行两列的数组大小。
  3. 所谓EASY_COUNT就是简单难度下的雷的个数,这里设置为10个雷。
?函数声明
//初始化数组 void init_board(char board[ROWS][COLS], int rows, int cols, char set); //打印棋盘 void display_board(char board[ROWS][COLS], int row, int col); //埋雷 void set_mine(char board[ROWS][COLS], int row, int col); //开始排雷 void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

将所有的函数声明放在game.h文件中,其他文件包含game.h文件,一劳永逸,非常方便。
注意:函数的声明需要和函数的定义一模一样!!
game.c文件代码分析 这个文件中存放的代码是真正实现扫雷程序功能的代码,实现每个函数的实现逻辑,以完成相对应的功能。
初始化函数 初始化函数就是将我们定义的两个棋盘初始化为我们想要的样子。
  1. 将mine数组内的元素全部初始化为0,表示整个棋盘中没有雷,将
  2. 将show数组内的元素全部初始化为’ * ',表示所有位置都没有被排查过。
只需要通过两层循环即可遍历到二维数组中的全部元素,将其元素全部改变为我们想要的即可。
void init_board(char board[ROWS][COLS], int rows, int cols, char set) { for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {board[i][j] = set; } } }

这里的参数需要多一个set,也就是我们想要将其初始化的内容。
打印棋盘函数 先展示一下棋盘的样子:
扫雷游戏设计及代码实现
文章图片

棋盘中的‘ * ’为我们上一个函数初始化后的元素,表示全都待排查。
上面棋盘可以看出,我们需要实现的功能:
  1. 打印出每一行和每一列的行标和列表。
  2. 打印出show数组中每个元素的内容。
  3. 打印出 “ — ”分割线和 ‘ | ’分割线。
void display_board(char board[ROWS][COLS], int row, int col) { //打印第一行的列标以及分割线 for (int i = 0; i <= row; i++) {printf(" %d ", i); if (i < row) {printf("|"); } } printf("\n"); //控制分割线 for (int i = 0; i <= row; i++) {printf("---"); if (i < row) {printf("|"); } } printf("\n"); //打印数据及控制分隔线 for (int i = 1; i <= row; i++) {printf(" %d |", i); for (int j = 1; j <= col; j++) {printf(" %c ", board[i][j]); if (j < col) {printf("|"); } } printf("\n"); if (i < row) {for (int k = 0; k <= col; k++) {printf("---"); if (k < col) {printf("|"); } } } printf("\n"); } }

需要注意这里的打印数据部分,为了控制打印行标从1开始,所以循环变量控制是从1开始的,后面用户输入控制也是从1开始的。
只需要在控制的时候仔细一点就可以完成!
?♂?埋雷函数 这个函数的功能实现:
  1. 在初始化好的mine棋盘上进行随机埋雷,埋雷个数由EASY_COUNT控制。
  2. 随机数由srand和rand函数通过时间戳产生,需要调用“ stdlib.h ”和“ time.h ”头文件;
  3. 需要注意如果当前产生随机坐标在之前已经放过雷了,就需要重新再生成随机坐标一次,直到放进为止才算成功一次。
void set_mine(char mine[ROWS][COLS], int row, int col) { int cnt = EASY_COUNT; while (cnt > 0) {int x = rand() % row + 1; //1-9 int y = rand() % col + 1; //1-9 if ('0' == mine[x][y]) {mine[x][y] = '1'; cnt--; } } }

在上一个函数已经提到过,用户眼中的棋盘坐标是从1开始的,而我们控制的棋盘也是从1开始的,所以这里在对row和col取模之后需要加一。
排查雷函数 排查雷函数比较复杂,也是最为关键的一步,我们需要通过循环控制一直排雷;
排查结果如下:
  1. 首先我们需要定义一个变量win,表示成功排查出雷的个数,当排查出的雷的个数等于坐标总数减去雷的个数时才算胜利。
  2. 如果排查的坐标是字符 ’ 1 ',也就是我们的雷,就要提醒用户游戏失败,并将埋雷情况展示,也就是打印埋好雷的mine数组。
  3. 如果排查的坐标不是雷,则代表成功排查一次,使win变量自增1, 并且通过函数算出该坐标周围八个位置的雷的个数,把该值放进对应坐标的show数组里,提供给玩家。
  4. 如果玩家输入的坐标不在棋盘内,则应该提醒玩家输入坐标非法,并通过循环使其重新输入,依次增加代码的健壮性。
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0; while (win < row * col - EASY_COUNT) {printf("请输入您要查找的坐标:>"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) {if (mine[x][y] == '0') {int count = get_mine_count(mine, x, y); show[x][y] = count + '0'; win++; display_board(show, ROW, COL); } else {printf("抱歉,你被炸死了\n"); display_board(mine, ROW, COL); break; } } else {printf("坐标非法,请重新输入\n"); } } if (win == row * col - EASY_COUNT) {printf("恭喜您,排雷成功。\n"); } }

这里我们注意get_mine_count函数的返回值是int型,但我们定义的数组是字符类型,所以需要加上字符 ‘ 0 ’以让其转换为字符,如数字3转换为字符‘ 3 ’。
?♂?计算坐标周围雷个数 该函数用于计算提供的数组坐标上下左右8个位置的雷的个数,因为雷是用字符‘ 1 ’设置的,而无雷是用字符‘ 0 ’设置的,是需要将8个位置的坐标元素加起来再减去8个字符‘ 0 ’即可得到字符‘ 1 ’的个数。
int get_mine_count(char mine[ROWS][COLS], int x, int y) { return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0'; }

注意这里传过来的实参是字符坐标,所以形参也需要对应。
并且该函数是在game.c文件内被调用且是在该文件内使用的,所以只需要将该函数定义在被调用函数之前即可,不需要被声明。

总结
以上就是本文全部内容,整个游戏代码源码将会放在全文最后ps部分,如果有讲的不对或者不充分的地方希望大家可以在评论区留言,如果觉得博主写的还行可以留下各位的点赞+关注+?收藏?嗷~
PS test.c
#define _CRT_SECURE_NO_WARNINGS 1#include "game.h"void menu() { printf("***********************\n"); printf("******1. play******\n"); printf("******0. exit******\n"); printf("***********************\n"); }void game() { char mine[ROWS][COLS] = { 0 }; //存放雷的信息 char show[ROWS][COLS] = { 0 }; //存放排查铺的雷的信息 //初始化数组 init_board(mine, ROWS, COLS, '0'); // 0 init_board(show, ROWS, COLS, '*'); // * //打印棋盘 display_board(show, ROW, COL); //埋雷 set_mine(mine, ROW, COL); //display_board(mine, ROW, COL); //开始排雷 find_mine(mine, show, ROW, COL); }int main() { srand((unsigned int)time(NULL)); int input = 0; do {menu(); printf("请选择输入:(0/1)"); scanf("%d", &input); switch (input) {case 1: game(); break; case 0: printf("您已退出游戏\n"); break; default: printf("选择错误,请重试\n"); break; } } while (input); return 0; }

game.c
#define _CRT_SECURE_NO_WARNINGS 1#include "game.h"void init_board(char board[ROWS][COLS], int rows, int cols, char set) { for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {board[i][j] = set; } } }void display_board(char board[ROWS][COLS], int row, int col) { //打印第一行的列标以及分割线 for (int i = 0; i <= row; i++) {printf(" %d ", i); if (i < row) {printf("|"); } } printf("\n"); //控制分割线 for (int i = 0; i <= row; i++) {printf("---"); if (i < row) {printf("|"); } } printf("\n"); //打印数据及控制分隔线 for (int i = 1; i <= row; i++) {printf(" %d |", i); for (int j = 1; j <= col; j++) {printf(" %c ", board[i][j]); if (j < col) {printf("|"); } } printf("\n"); if (i < row) {for (int k = 0; k <= col; k++) {printf("---"); if (k < col) {printf("|"); } } } printf("\n"); } }void set_mine(char mine[ROWS][COLS], int row, int col) { int cnt = EASY_COUNT; while (cnt > 0) {int x = rand() % row + 1; //1-9 int y = rand() % col + 1; //1-9 if ('0' == mine[x][y]) {mine[x][y] = '1'; cnt--; } } }int get_mine_count(char mine[ROWS][COLS], int x, int y) { return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0'; }void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0; while (win < row * col - EASY_COUNT) {printf("请输入您要查找的坐标:>"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) {if (mine[x][y] == '0') {int count = get_mine_count(mine, x, y); show[x][y] = count + '0'; win++; display_board(show, ROW, COL); } else {printf("抱歉,你被炸死了\n"); display_board(mine, ROW, COL); break; } } else {printf("坐标非法,请重新输入\n"); } } if (win == row * col - EASY_COUNT) {printf("恭喜您,排雷成功。\n"); } }

game.h
#define _CRT_SECURE_NO_WARNINGS 1#include #include #include #define ROW 9 #define COL 9 #define ROWS ROW + 2 #define COLS COL + 2#define EASY_COUNT 10//初始化数组 void init_board(char board[ROWS][COLS], int rows, int cols, char set); //打印棋盘 void display_board(char board[ROWS][COLS], int row, int col); //埋雷 void set_mine(char board[ROWS][COLS], int row, int col); //开始排雷 void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

    推荐阅读