摘要:本文将通过一个完整的C语言扫雷项目,讲解控制台小游戏的开发思路与实现细节。项目采用多文件编程,支持三种难度级别,涵盖二维数组操作、随机数生成、递归算法等核心知识点。
一、游戏设计思路
1.1 核心规则
-
9x9方格(简单模式)埋藏10个雷
-
玩家通过坐标翻开格子
-
踩雷即失败,显示全部雷区
-
非雷格子显示周围8格雷数
-
标记所有雷即胜利
二、关键技术实现
2.1 双棋盘设计
// 使用ROWS/COLS宏定义解决边界检测问题
#define ROWS 20
#define COLS 30
char mine[ROWS][COLS]; // 雷区实际数据
char show[ROWS][COLS]; // 玩家可见界面
优势:通过创建两个二维数组,分别存储真实雷区数据和玩家可见界面,实现数据与显示的分离。
2.2 智能初始化
void InitBoard(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;
}
}
}
说明:通过参数化初始化字符,灵活创建不同状态的棋盘('0'表示无雷,'1'表示有雷,'*'表示未翻开)
三、核心算法解析
3.1 随机布雷
void SetMine(char board[ROWS][COLS], int row, int col) {
int count = MINES;
while(count) {
int x = rand()%row +1;
int y = rand()%col +1;
if(board[x][y] == '0'){
board[x][y] = '1';
count--;
}
}
}
关键点:通过循环+随机数生成实现不重复布雷,确保首步安全(可优化点)
3.2 雷数计算
int GetMineCount(char mine[ROWS][COLS], int x, int y) {
return (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');
}
创新点:利用ASCII码特性将字符计算转换为数值计算
四、功能扩展建议
4.1 递归展开空白区域
void ExpandEmpty(char mine[ROWS][COLS], char show[ROWS][COLS],
int x, int y) {
if(x<1 || x>ROW || y<1 || y>COL) return;
if(show[x][y] != '*') return;
int count = GetMineCount(mine, x, y);
if(count > 0) {
show[x][y] = count + '0';
return;
}
show[x][y] = ' ';
// 递归展开周围8格
ExpandEmpty(mine, show, x-1, y);
ExpandEmpty(mine, show, x+1, y);
// ...其他6个方向
}
效果:实现类似Windows扫雷的空白区域自动展开效果
4.2 优化建议列表
-
增加标记旗帜功能
-
添加计时器和计步器
-
实现保存/读取游戏进度
-
使用图形库升级界面
五、编译与测试
5.1 环境要求
-
Windows/Linux系统
-
支持C99标准的编译器
-
建议使用VS2019/CLion进行开发
5.2 常见问题排查
# 遇到数组越界错误时检查:
1. 全局变量ROW/COL是否正确初始化
2. 数组索引是否在有效范围内
3. 边界格子的雷数计算是否正确
六、功能扩展
6.1 增加标记旗帜功能
方法一 通过增加选择步骤实现
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int action = 0; // 新增:操作类型变量
while (win < row * col - MINES)
{
printf("请选择操作:\n1.排雷 2.标记/取消标记\n");
printf("请输入操作编号:>");
scanf("%d", &action);
if (action != 1 && action != 2) {
printf("无效操作,请重新输入\n");
continue;
}
printf("请输入要操作的坐标(x y):>");
scanf("%d %d", &x, &y);
if (x < 1 || x > row || y < 1 || y > col) {
printf("坐标非法,重新输入\n");
continue;
}
switch (action)
{
case 1: // 排雷操作
if (show[x][y] == 'F') {
printf("该位置已标记,请先取消标记\n");
break;
}
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, row, col);
return;
} else {
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, row, col);
win++;
}
break;
case 2: // 标记操作
if (show[x][y] == '*') {
show[x][y] = 'F'; // 标记为旗帜
} else if (show[x][y] == 'F') {
show[x][y] = '*'; // 取消标记
} else {
printf("该位置无法标记\n");
}
DisplayBoard(show, row, col);
break;
default:
break;
}
}
if (win == row * col - MINES) {
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, row, col);
}
}
方法二 通过输入负坐标实现
在 FindMine 中增加对旗帜标记的处理
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
int win = 0, flag_count = 0;
printf("提示:输入负坐标(如 -3 5)标记旗帜,输入正坐标(如 3 5)排雷。\n");
while (win < (row * col - MINES) || flag_count < MINES)
{
printf("请输入坐标(x y):>");
scanf("%d %d", &x, &y);
// 处理负坐标标记旗帜
if (x < 0)
{
x = -x;
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*') // 仅允许在未翻开的位置标记
{
show[x][y] = 'F';
flag_count++;
printf("已标记旗帜\n");
}
else if (show[x][y] == 'F') // 撤销标记
{
show[x][y] = '*';
flag_count--;
printf("撤销标记旗帜\n");
}
else
{
printf("该位置无法标记\n");
}
DisplayBoard(show, row, col);
}
else
{
printf("坐标非法,重新输入\n");
}
}
// 处理正坐标排雷
else if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == 'F')
{
printf("该位置已标记,请先取消标记\n");
}
else 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");
}
// 检查胜利条件
if (CheckWin(show, row, col, MINES, flag_count))
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, row, col);
break;
}
}
}
在 DisplayBoard
中显示旗帜
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("--------扫雷游戏-------\n");
for (i = 0; i <= col; i++) {
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
// 显示旗帜标记
if (board[i][j] == 'F')
{
printf("F ");
}
else
{
printf("%c ", board[i][j]);
}
}
printf("\n");
}
}
在 main.c
中记录旗帜标记计数
void game()
{
char mine[ROWS][COLS]; // 存放布置好的雷
char show[ROWS][COLS]; // 存放排查出的雷的信息
int flag_count = 0; // 添加旗帜计数
// 初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
// 打印棋盘
DisplayBoard(show, ROW, COL);
// 布置雷
SetMine(mine, ROW, COL);
// 排查雷
FindMine(mine, show, ROW, COL);
}
新增检查胜利条件的函数
int CheckWin(char show[ROWS][COLS], int row, int col, int mines, int flags)
{
int unopened = 0;
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (show[i][j] == '*' || show[i][j] == 'F')
{
unopened++;
}
}
}
return (unopened == mines && flags == mines);
}
功能性对比
方案一
-
优点:通过菜单选择操作类型(排雷/标记),逻辑分离清晰,适合需要明确操作步骤的场景。
-
缺点:
-
未实现完整的胜利条件:仅依赖排雷完成判定胜利,未验证标记是否全部正确(传统扫雷中正确标记所有雷也是胜利条件之一)。
-
操作冗余:用户需要多一步选择操作类型(输入 1 或 2),增加了操作成本。
-
方案二
-
优点:
-
完整的胜利条件:要求用户正确标记所有雷且排完所有非雷区域,符合经典扫雷规则。
-
输入更简洁:通过负坐标(如
-3 5
)触发标记功能,无需额外选择操作类型,减少用户输入步骤。
-
-
缺点:
-
负坐标可能不够直观:用户需要学习输入负坐标的规则,可能对新手不友好
-
项目源码:Project seek bomb/Project seek bomb · lumos_tq/C language - 码云 - 开源中国
大概半年内会将建议优化列表想办法慢慢完善