扫雷作为Windows系统自带的经典小游戏,凭借简单的规则和极强的趣味性,成为很多人接触编程后想要复刻的入门项目。本文将基于纯C语言实现一款标准的9×9扫雷游戏,完整复现"初始化棋盘-布置雷-排查雷-胜负判断"的核心流程,同时拆解代码结构与关键逻辑,帮助新手理解如何用C语言的数组、函数、随机数等基础知识点构建完整项目。
先上完整可运行代码(已修复跨编译器兼容问题),再逐模块拆解分析:
一、完整代码实现
1. 头文件 game.h(函数声明与宏定义)
cpp
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9 //行数
#define COL 9 //列数
#define ROWS ROW+2 //行数+上下两行
#define COLS COL+2 //列数+左右两列
#define EASY_COUNT 10 //雷的数量
//初始化棋盘
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char arr[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char arr[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row ,int col);
2. 游戏逻辑实现 game.c
cpp
#include<stdio.h>
#include"game.h"
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 DisplayBoard(char arr[ROWS][COLS], int rows, int cols)
{
//打印列号
printf("-----扫雷游戏------\n");
for (int i = 0; i <= cols; i++)
{
printf("%2d ", i);
}
printf("\n");
for (int i = 1; i <= rows; i++)
{
printf("%2d ", i);
for (int j = 1; j <=cols; j++)
{
printf("%2c ", arr[i][j]);
}
printf("\n");
}
printf("\n");
}
//布置雷
void SetMine(char arr[ROWS][COLS], int row, int col)
{
int count=EASY_COUNT;
while (count)
{
//随机生成雷的位置
int x = rand()%row + 1;//雷的范围是1-9
int y = rand()%col + 1;
if (arr[x][y] == '0')//判断重复布置没
{
arr[x][y] = '1';
count--;
}
}
}
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int a = mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1]+
mine[x + 1][y] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
return a;
//int count = 0;
//for(int i=x-1;i<=x+1;i++)
//for (int j = y - 1; j <= y + 1; j++)
// {
// count+=mine[i][j] - '0';
// }
//return count;
}
//排查雷
void FindMine(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_s("%d %d", &x, &y);
//判断输入的坐标是否合法
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
if (mine[x][y] == '1')
{
printf("很遗憾,你输了!\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//该坐标不是雷,统计周围一圈有几个雷
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("该坐标已经被标记过,请重新输入!\n");
}
}
else
{
printf("坐标非法、请重新输入!\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,你赢了!\n");
DisplayBoard(mine, ROW, COL);
}
}
3. 主函数与菜单 main.c
cpp
#include"game.h"
void menu()
{
printf("扫雷游戏开始界面\n");
printf("*****************\n");
printf("******1.play*****\n");
printf("******0.exit*****\n");
printf("*****************\n");
}
void game()
{
//完成扫雷游戏
char mine[ROWS][COLS] = {0};//布置好雷的信息
char show[ROWS][COLS] = {0};//存放排查出的雷的消息
//初始化棋盘
InitBoard(mine,ROWS,COLS,'0');//数组全部初始化为'0'
InitBoard(show, ROWS, COLS,'*');//数组全部初始化为'*'
//布置雷
//在9*9的棋盘上随机布置10个雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL); //测试雷布置成功没
//打印棋盘
DisplayBoard(show, ROW, COL);
//排查雷
FindMine(mine,show,ROW,COL);
}
void test()
{
int input = 0;
srand((long long)time(NULL));
do
{
menu();
printf("请选择:");
scanf_s("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出\n");
break;
default:
printf("输入错误,请重新输入\n");
}
}while (input);
}
int main()
{
test();
return 0;
}
二、核心设计思想说明
本扫雷游戏的代码实现遵循"模块化、高内聚低耦合、逻辑清晰可扩展"的核心设计思想,通过合理的函数拆分、数据结构设计和边界处理技巧,让代码既易于理解,又便于后续功能扩展。以下是关键设计思想的详细说明:
1. 模块化设计:函数职责单一化
代码将游戏流程拆解为多个功能独立的函数,每个函数仅负责一项具体任务,实现"高内聚":
-
初始化相关:
InitBoard仅负责棋盘数组的初始化,通过传入不同的set参数,可同时复用给mine数组和show数组,避免代码冗余; -
界面展示相关:
DisplayBoard仅负责棋盘的打印,集中处理格式对齐(行列号、元素间距),让交互界面更规范; -
核心逻辑相关:
SetMine仅负责布雷、FindMine仅负责排查雷与胜负判断,GetMineCount仅负责雷数统计,函数职责边界清晰; -
流程控制相关:
menu负责菜单展示、game负责游戏核心流程串联、test负责游戏循环控制,分层管理流程,降低代码复杂度。
这种设计的优势在于:后续修改某一功能(如调整棋盘打印格式、修改布雷规则)时,只需改动对应函数,不会影响其他模块,提升代码可维护性。
2. 数据与显示分离:双数组分工设计
采用"后台数据数组+前台显示数组"的分离设计,是本实现的核心亮点之一:
-
mine数组(后台数据):仅存储雷的真实信息,不对外展示,确保游戏数据的安全性和唯一性,避免用户直接获取雷的位置;
-
show数组(前台显示):仅负责向用户展示排查状态,不存储真实雷信息,用户的所有操作都通过修改show数组反馈,实现"数据层"与"展示层"的解耦。
这种分离设计的优势:一是逻辑清晰,避免数据和显示混杂导致的错误(如误修改真实雷信息);二是扩展性强,后续添加"标记雷"功能时,只需修改show数组的显示字符(如将'*'改为'!'),无需改动mine数组的核心数据。
3. 边界处理简化:11×11数组冗余设计
针对扫雷游戏中"边缘格子雷数统计易越界"的问题,设计了"11×11数组存储9×9游戏区"的冗余结构,核心思路是"用空间换简洁":
-
在9×9有效游戏区的上下左右各增加1行/列空白边界,这些边界格子初始化为'0'(无雷);
-
统计任意格子(包括边缘格子)周围8个格子的雷数时,无需额外判断"是否超出数组范围",直接遍历8个相邻格子即可,极大简化了边界处理逻辑,减少代码冗余和出错概率。
这种设计虽占用了少量额外内存,但对于9×9的小游戏而言,内存开销可忽略不计,却能显著提升代码的简洁性和可读性,是嵌入式、小游戏开发中常见的优化思路。
4. 可扩展设计:宏定义参数化
代码中通过宏定义(ROW、COL、EASY_COUNT)将游戏核心参数参数化,而非直接使用硬编码:
-
修改游戏难度时,无需改动核心逻辑代码,只需调整宏定义的值(如将
ROW、COL改为16,EASY_COUNT改为40,即可实现16×16中等难度); -
后续扩展多难度模式时,可通过添加宏定义(如
MID_ROW、HARD_COUNT),配合条件判断即可实现,降低扩展成本。
5. 封装性设计:静态函数隐藏内部逻辑
将雷数统计函数GetMineCount定义为静态函数(static),使其仅能在game.c文件内部被调用,不对外暴露:
-
隐藏了雷数统计的具体实现细节,外部模块(如main.c)无需关心"如何统计雷数",只需调用
FindMine即可,降低模块间的耦合度; -
避免了外部模块误调用该函数导致的逻辑错误,提升代码的安全性和健壮性。
三、核心代码逻辑拆解
1. 为什么要用11×11的数组存储9×9的游戏区?
这是扫雷实现的核心技巧之一!我们定义的ROWS=ROW+2、COLS=COL+2,本质是给9×9的游戏区"加了一圈边界"。这样做的目的是:当统计棋盘边缘格子(如(1,1)、(9,9))周围的雷数时,不需要额外判断"是否越界"------直接遍历8个相邻格子即可,极大简化了边界处理逻辑。
比如统计(1,1)周围的雷数,若用9×9数组,会访问(0,0)、(0,1)等无效下标;而11×11数组中,这些边界格子本身被初始化为'0'(无雷),不影响雷数统计,代码更简洁。
2. 两个核心数组的分工
代码中用了两个二维数组,职责完全分离,这是模块化设计的体现:
-
mine数组:"后台数据区",存储雷的真实位置,用户看不到。用'1'表示有雷,'0'表示无雷,初始化时全设为'0',之后通过SetMine函数随机修改10个位置为'1'。
-
show数组:"前台显示区",展示给用户看的界面。初始化时全设为'*'(表示未排查),用户排查后,若不是雷,则更新为周围的雷数(如'3'表示周围有3个雷)。
3. 随机布雷的实现逻辑
布雷的核心是"随机且不重复":
-
用
rand()生成随机数,通过rand()%row + 1将范围限定在1-9(对应游戏区的有效坐标); -
用
count记录剩余需要布置的雷数,每成功布置一个雷就count--,直到count=0; -
布置前判断
mine[x][y] == '0',避免在同一位置重复布雷。
注意:srand((unsigned int)time(NULL))必须只调用一次(放在test函数中),若放在game函数里,每次开始游戏的时间戳可能相同,导致布雷位置重复。
4. 雷数统计的巧妙实现
GetMineCount函数是统计周围8个格子雷数的核心,代码用了一个很巧妙的计算方式:
因为mine数组中存储的是字符'0'或'1',而字符的ASCII码值是连续的------'1'-'0'的结果是1,'0'-'0'的结果是0。所以直接将8个相邻格子的字符值相加,再减去8个'0'的ASCII码和,就能得到雷的数量,比用循环遍历更简洁(代码中也保留了循环遍历的注释版本,逻辑完全一致)。
5. 胜负判断逻辑
游戏的胜负条件很明确:
-
失败:用户输入的坐标对应mine数组中的'1'(踩雷),直接打印所有雷的位置,游戏结束;
-
胜利:用户排查的非雷格子数量(win)等于"总格子数 - 雷数"(9×9-10=71),说明所有非雷格子都已排查完毕,打印所有雷的位置,游戏胜利。
关键注意事项
-
兼容性问题 :原代码中的
scanf_s是微软VS编译器的特有函数,在GCC、Clang等编译器下无法运行,因此修改为标准的scanf,确保跨编译器兼容; -
坐标输入:用户需要输入"行号 列号"(如"3 5"表示第3行第5列),范围必须在1-9,否则会提示"坐标非法";
-
重复排查:若输入已排查过的坐标(show数组中对应位置不是'*'),会提示"该坐标已经被标记过"。
四、可选优化方向(扩展功能)
当前代码已实现标准扫雷的核心功能,若想进一步优化,可以添加以下功能:
-
难度选择:增加中等(16×16,40个雷)、困难(30×16,99个雷)模式,通过修改宏定义实现;
-
标记雷功能:支持用户用特定字符(如'!')标记疑似雷的位置,避免误踩;
-
空白区域自动展开:当排查到周围无雷的格子时,自动展开相邻的空白格子,提升游戏体验;
-
计时功能:记录游戏开始到结束的时间,增加竞技性。
五、总结
这款扫雷游戏的实现,核心是利用"11×11数组处理边界""双数组分离数据与显示""字符ASCII码计算雷数"这三个关键技巧,将C语言的数组、函数、循环、条件判断等基础知识点串联起来。代码结构清晰,模块化设计让后续扩展功能变得简单,非常适合新手作为C语言项目练习。
如果你是刚接触C语言的新手,建议先理解"双数组分工"和"边界处理"的逻辑,再逐行拆解其他函数;如果想提升,可以尝试实现上面的优化功能,进一步巩固编程思路。