扫雷游戏简易版——控制台实现
你好我是芝士很久没有更新啦那就来简单记录一下最近学习的内容吧!今天让我们一起来探讨扫雷游戏这里涵盖的内容非常丰富,并包含多种功能以及各种控制结构由于之前的模块未被分享没关系啦~那就让我们一起学习如何从零开始构建完整的扫雷游戏代码吧!
目录
-
游戏规则
- 思路解析
- 2.1 前期分析
- 2.2 子程序拆解
-
- 菜单模块
-
- 棋盘初始化步骤包括创建二维数组并填充初始棋盘数据
-
- 棋盘显示界面用于可视化当前棋局状态
-
- 雷区标记符放置于预先确定的潜在地雷位置
-
- 地雷位置检查流程用于验证所有地雷配置是否符合安全要求
-
-
3. 代码模块
-
-
3.1 核心框架
-
3.2 功能模块
-
- 1.功能菜单
- 2. 棋盘创建流程
- 3. 棋盘输出流程
- 4. � winding 雷区布置流程
*5. 核对雷区流程
-
3.4 优化及完整代码
-
-
1. 游戏规则
在完成对简易版扫雷游戏的实现之前,请您先了解一下具体的玩法与基本规则吧。本次简易版扫雷游戏主要通过我们电脑端中的控制台来进行操作,请确保您熟悉了这一界面的操作方式后进行下一步操作。(如图1所示)

从上图可以看出,在扫雷游戏中棋盘由9×9个方格组成。玩家可以选择输入0或1来决定是否进入游戏流程。一旦玩家决定启动游戏(即输入1),接下来的任务就是通过输入相应的坐标来识别并消除地雷。例如,在某次操作中玩家可能输入2,5作为目标坐标。

在区间(2,5)内出现了数字0表示该位置周围坐标的雷的数量为零。再次输入一个新的坐标值:(2,\ 8)

在这里触发了地雷之后
- 当某个位置未被标记为 mine 时,则会显示其周围有多少颗未被标记的 mine。
- 一旦某块区域标记为含有 mine 的情况下,则会导致该区域内的所有方块自动爆炸并触发 game over。
- 通过排除已知存在的 10 个 mine 的情况下识别出剩余的所有非 mine 格子。
总结起来,在扫雷游戏中存在以下基本规则:
2. 思路解析
为了达成这个简易版扫雷游戏的构建目标,我们首先对问题解决思路进行系统性分解,并明确具体所需要素;随后将其划分为两个主要阶段来进行详细规划。
2.1 前期分析
当点击选定的位置时系统会进行判断操作:如果选定的位置没有地 mines,则会计算并显示周围 mines 的数量;这一过程实际上是一种数据统计与计算的工作流程。考虑到数组的基本特性我们可以明显得出这样一个单一的一维数组无法满足我们的需求 因此我们需要采用另一种数据结构方案:即使用两个同样尺寸(9 \times 9)的数据存储空间分别用于记录 mine 的位置信息以及最终展示的结果信息 这样就能够有效地完成整个游戏的核心逻辑设计工作

经过一番思考后, 我们再次审视了第三个部分: 数组规模. 如图所示, 在游戏中, 我们需要计算周围的雷的数量.

为了计算方便起见,在统计某个特定位置周围的雷数时(此处指位于棋盘边缘的位置),我们需要考虑该位置周围的所有相邻格子是否存在雷。对于位于棋盘边缘的一些特殊位置(如2号和3号),在9×9的小棋盘中难以准确判断是否存在雷。这会导致统一性计算方法难以实现。一种可行的方法是将棋盘扩展为11×11的新棋盘,并重新进行概率评估以确保所有情况都能得到准确判断。扩展后的棋盘情况如下所示:

为了更好地展示效果,在具体展示的时候我们仅呈现的是一个9×9的小规模棋盘。然而,在实际演示时我们只展示了这一小部分棋盘结构,并未扩展整个棋盘范围。
在计算过程中使用整型数组确实是一个可行的选择但在后续的数据处理过程中可能会出现数据溢出等问题因此我们需要进一步优化我们的数据存储方案以确保计算过程中的数据完整性与准确性
基于上述分析为了实现高效的数据处理机制我们将采用以下解决方案:即构建一个11×11的char型二维数组并将其所有元素初始化为字符'0'(空格)。这种设置不仅简化了后续处理流程还能有效避免因数据类型不匹配导致的程序运行错误
2.2 子程序拆解
在对前期相关逻辑进行梳理后, 为了实现目标功能, 我们需要考虑哪些子程序——函数, 以便完成目标功能的基础工作。具体来说,
1. 菜单模块
当我们在进行游戏时需要设计一个界面

该界面便于用户进行操作选择,并标识出退出与启动的不同状态(数值0用于退出游戏流程而数值1则启动游戏流程)。在开发过程中将此功能模块划分至独立的Menu()子程序中以提高代码可维护性。该子程序的主要职责是生成相应的界面内容,并在此基础上完成绘图工作无需向该子程序传递参数或获取返回值。
2. 棋盘初始化
基于之前的分析,在开发井字 minesweeper 游戏时
3. 棋盘打印
在运行代码测试时,在初始阶段我们需反复输出相应的棋盘以便观察其设定大小,并对系统显示的界面进行详细审视以确保布局正确

由于棋盘打印对于程序运行至关重要,并且我们将此功能拆分为一个独立的子程序PrintBoard()中进行处理。接下来对其传入参数及返回类型进行详细分析表明:该函数无需返回任何数据。此外,在该函数中接受的一个一维数组将包含多行多列的数据结构
4. 布置雷
有了棋盘打印功能后,在相应棋盘中展示存储的数据也是必要的一步。在安排好地雷的过程中,在其中一个存放地雷的一维数组中完成地雷数量和位置的设定是关键步骤。假设存在一个名为SetBoard()的功能块用于配置地雷的位置,并对其参数进行分析可知该功能块接受三个必要参数:待配置的地雷数组、所在的行索引以及列索引,并且该功能块设计为无返回值类型。在放置地雷时需注意以下几点:采用随机算法生成均匀分布的地核布局,并确保每次游戏开始时的地核布局都是不同的。为此,在主程序入口处调用 srand() 函数来初始化随机数种子,并根据当前时间生成不同的初始分布模式。
5. 排查雷
布置完毕后真正的游戏开始前必须先做好全面排查工作确保所有可能出现的风险都已经确定无误我们假设有这样一个核心功能模块FineBoard()用于实现棋盘状态的有效扫描与风险评估过程对于该功能模块的基本参数设置包括输入数据区域及其对应行列索引位置等信息但并不需要设置任何返回值变量在实际运行过程中必须涉及具体的数值统计工作因此在功能模块内部我们会设计并实现一个专门负责数值统计的小程序不妨命名为ComputeBoard()它的主要职责就是根据当前棋盘状态快速准确地计算出各类关键指标数据这些指标数据将被用作后续决策的重要依据同时我们也需要考虑该核心功能模块所需处理的数据类型及运算规则等具体内容可参考附图部分

从图中可以看出,在计算(x,y)坐标的值时需要考虑周围坐标的分布情况以便于对ComputeBoard()函数的参数进行设置必须确定坐标的行序号与列序号
3. 代码实现
对于这个游戏的具体代码主要分成以下几个方面进行详细介绍:
3.1 框架逻辑
1. 文件个数及命名

我们应当首先认识到这个扫雷游戏的基本逻辑框架:在运行程序时首先要打开相应的菜单栏目,并读取玩家输入的数字来决定是否开始游戏。为此我们可以采用do while循环结构并结合switch语句来进行流程控制其中建议设计一个名为createGame()的功能模块该功能模块将负责调用之前分析的各种辅助函数以实现界面的基本功能包括初始化必要的数据结构如棋盘布局等。具体来说我们需要建立一个9x9大小的实际棋盘以满足基本的游戏需求但由于程序设计考虑全面性和可维护性我们建议在头文件中定义两个变量Row和Col分别表示棋盘的实际行数与列数Rows值设为Row+2 Cols值设为Col+2这样我们就可以通过简单的修改即可适应不同大小的游戏区域需求
#include <stdio.h>
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2
根据以上逻辑的梳理书写代码程序如下:
#include "game.h"
void Game()
{
char Mine[Rows][Cols];
char Show[Rows][Cols];
//初始化棋盘
//棋盘打印
//布置雷
//排查雷
}
int main()
{
int input = 0;
do
{
Menu();//菜单模块
printf("请输入:\n");
scanf("%d", &input);
switch (input)
{
case 1:
Game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("请输入正确的数字\n");
break;
}
} while (input);
return 0;
}
不论是在C/C++编程还是其他编程语言中,
都需要特别注意
类型转换操作符的行为,
因为这直接影响程序运行时的行为。
这一点必须严格遵守,
否则可能导致逻辑错误。
3.2 函数实现
对于我们需要分装的函数以下几块:
1.菜单模块
在之前的分析中, 我们了解到, 该软件函数的主要作用是输出内容, 它无需返回任何数据, 因此针对该函数的实践与编码练习也相应展开.
void Menu()
{
printf("******************************\n");
printf("************0 exit************\n");
printf("************1 exit************\n");
printf("******************************\n");
}
需要注意之处在于,该代码位于game_1.c文件中,并且还需要在game.h文件中进行必要的声明。
2. 棋盘初始化
在布置雷之前必须先对棋盘进行初始化设置,在初始化棋盘时自然会想到使用循环结构,请参考以下具体的代码
//初始化棋盘
void InitBoard(char arr[Rows][Cols], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
在处理for循环时需要特别注意以下几点:首先,在循环体内部变量i和j都必须小于对应的rows和cols参数值。这是因为这里的实参中rows和cols的值是固定的数值(例如假设均为整数),而数组的数据结构要求其索引范围从零开始计算。因此,在这种情况下意味着数组中的索引范围是从零开始一直到十(即包含从零到十的所有整数),总共有十一项数据。
3. 棋盘打印
当有了初始化棋盘功能模块之后
//棋盘打印
void PrintBoard(char arr[Rows][Cols], int row, int col)
{
printf("----------扫雷----------\n");
for (int i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("----------扫雷----------\n");
}
这里将实际存放雷的数组和展示的数组都打印,呈现出的效果如下所示:

4. 布置雷
当实现了棋盘打印功能后
//布置雷
void SetBoard(char arr[Rows][Cols], int row, int col)
{
int count = 10;//布置10个雷
while (count)
{
//限制数组中x和y的取值为1~row(col)这里是9,因为实参Row和Col是9
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
基于先前设计好的棋盘打印函数,在布设好雷区的棋盘上运行该功能模块后,结果显示布设雷区的状态如下:

因为这里采用了当前系统的运行时间作为随机种子源,并确保了每次程序调用时都会有不同的布局效果出现。
5. 排查雷
在布置好雷之后,则需要我们开始排查。我们可以进一步分析FindBoard函数的参数设置。为了便于后续处理结果展示的需求,在这里我们需要明确两个关键数组:一个是存放当前棋盘上所有地雷的位置信息;另一个则是用于存储排出来后实际显示的地盘布局信息。此外,在排雷过程中涉及的一个重要问题是关于如何获取具体的坐标信息。该函数定义了两个主要参数:一个是存放当前棋盘上地雷位置的信息数组;另一个则接收来自键盘输入的具体坐标数值,并通过这些数值来确定具体操作的位置点。需要注意的是,在排雷过程中我们需要根据输入坐标的值来计算周围区域的地 mines数量,并将这一逻辑模块封装到ComputeBoard()函数中进行处理。对于这一核心功能的具体实现细节,则将在后续章节中逐一展开讨论。为了确保能够彻底排查出所有的地 mines点,在循环条件的设计上需要考虑每次迭代后的剩余待查坐标数量。当剩余待查坐标的数量降为零时,则表示已经完成了全部的地 mines点排查任务
//排查雷
void FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols],int x, int y)
{
//总共需要排查9*9-10个位置
//Row*Col -10
x = 0;
y = 0;
int Count = Row * Col - 10;
while (Count)
{
printf("请输入排查坐标: ");
scanf("%d %d", &x, &y);
if (Mine[x][y] == '0')
{
//计算该位置周围雷的坐标
int value=ComeputeBoard(Mine, x, y);
//将该坐标的值放到Show数组中
Show[x][y] = value + '0';
PrintBoard(Show, Row, Col);
Count--;
}
else
{
printf("游戏结束,被炸死了\n");
PrintBoard(Mine, Row, Col);
break;//跳出循环
}
}
if (Count == 0)
{
printf("恭喜你,排雷成功\n");
PrintBoard(Mine, Row, Col);
}
}
针对这里的ComputeBoard函数,在对前面内容的深入分析后,我们得出了其完整的计算逻辑代码。
int ComeputeBoard(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');
}
这里我们可以实验一下,得到以下结果

3.4 优化及完整代码
针对当前编写的代码,我们可以对其进行进一步优化
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2
#define COUNT 10
//菜单打印
void Menu();
//初始化棋盘
void InitBoard(char arr[Rows][Cols], int rows, int cols, char set);
//棋盘打印
void PrintBoard(char arr[Rows][Cols], int row, int col);
//布置雷
void SetBoard(char arr[Rows][Cols], int row, int col);
//计算
int ComeputeBoard(char Mine[Rows][Cols], int x, int y);
//排查雷
void FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols], int x, int y);
game_1.c
#include "game.h"
//菜单模块
void Menu()
{
printf("******************************\n");
printf("************0 exit************\n");
printf("************1 exit************\n");
printf("******************************\n");
}
//初始化棋盘
void InitBoard(char arr[Rows][Cols], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
//棋盘打印
void PrintBoard(char arr[Rows][Cols], int row, int col)
{
printf("----------扫雷----------\n");
for (int i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("----------扫雷----------\n");
}
//布置雷
void SetBoard(char arr[Rows][Cols], int row, int col)
{
int count = COUNT;//布置10个雷
while (count)
{
//限制数组中x和y的取值为1~row(col)这里是9,因为实参Row和Col是9
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
//计算数值
int ComeputeBoard(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 FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols],int x, int y)
{
//总共需要排查9*9-10个位置
//Row*Col -10
x = 0;
y = 0;
int Count = Row * Col - COUNT;
while (Count)
{
printf("请输入排查坐标: ");
scanf("%d %d", &x, &y);
if (Mine[x][y] == '0')
{
//计算该位置周围雷的坐标
int value=ComeputeBoard(Mine, x, y);
//将该坐标的值放到Show数组中
Show[x][y] = value + '0';
PrintBoard(Show, Row, Col);
Count--;
}
else
{
printf("游戏结束,被炸死了\n");
PrintBoard(Mine, Row, Col);
break;//跳出循环
}
}
if (Count == 0)
{
printf("恭喜你,排雷成功\n");
PrintBoard(Mine, Row, Col);
}
}
main_1.c
#include "game.h"
void Game()
{
char Mine[Rows][Cols];
char Show[Rows][Cols];
//初始化棋盘
InitBoard(Mine, Rows, Cols, '0');
InitBoard(Show, Rows, Cols, '*');
//棋盘打印
PrintBoard(Mine, Row, Col);
PrintBoard(Show, Row, Col);
//布置雷
//为了使得每次生成的坐标不一样,这里调用srand函数
srand((unsigned int)time(NULL));
//srand参数类型是unsigned int因此这里对time函数使用强制类型转换
//time(NULL)表示获取当前时间作为srand的随机种子
SetBoard(Mine, Row, Col);
//PrintBoard(Mine, Row, Col);
//排查雷
FindBoard(Mine, Show, Row, Col);
}
int main()
{
int input = 0;
do
{
Menu();//菜单模块
printf("请输入:\n");
scanf("%d", &input);
switch (input)
{
case 1:
Game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("请输入正确的数字\n");
break;
}
} while (input);
return 0;
}
好了以上就是本次扫雷游戏简易版的分享,有不足的地方请批评指正。
