1. C语言扫雷游戏开发概述
用C语言实现经典扫雷游戏是检验编程基本功的绝佳练手项目。这个看似简单的游戏背后涉及数组操作、递归算法、随机数生成、图形界面绘制等核心编程概念。我在大学时期完成的第一个扫雷版本只用了200行代码,但存在诸多bug;经过多年迭代,现在的实现版本不仅稳定可靠,还加入了难度选择和排行榜功能。
开发扫雷游戏主要解决三个核心问题:雷区生成、数字计算和点击处理。雷区用二维数组表示,每个格子需要记录是否有雷、周围雷数以及当前显示状态(隐藏/标记/已翻开)。数字计算本质上是对周围8个格子的遍历统计,而点击处理则涉及递归翻开空白区域的经典算法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 游戏数据结构设计
2.1 核心数据结构定义
c复制#define MAX_SIZE 30 // 最大支持30x30的雷区
#define MINE -1 // 用-1表示地雷
typedef struct {
int value; // -1表示雷,0-8表示周围雷数
int is_show; // 0未翻开,1已翻开,2标记为雷
} Cell;
Cell board[MAX_SIZE][MAX_SIZE];
int rows, cols; // 实际雷区行列数
int mine_count; // 总雷数
int remain_count; // 剩余未翻开的非雷格子数
这种结构设计有几点优势:
- 将格子状态与数值分离,便于扩展状态(如后期可添加问号标记)
- 使用固定大小数组避免动态内存分配带来的复杂度
- 通过remain_count实现快速胜利判断(当其为0时玩家获胜)
2.2 初始化函数实现
c复制void init_game(int difficulty) {
// 根据难度设置参数
switch(difficulty) {
case 1: rows=9; cols=9; mine_count=10; break; // 初级
case 2: rows=16; cols=16; mine_count=40; break; // 中级
case 3: rows=16; cols=30; mine_count=99; break; // 高级
}
remain_count = rows * cols - mine_count;
// 清空棋盘
memset(board, 0, sizeof(board));
// 随机布雷
srand(time(NULL));
int mines_placed = 0;
while(mines_placed < mine_count) {
int x = rand() % rows;
int y = rand() % cols;
if(board[x][y].value != MINE) {
board[x][y].value = MINE;
mines_placed++;
}
}
// 计算每个格子周围雷数
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
if(board[i][j].value == MINE) continue;
int count = 0;
for(int dx=-1; dx<=1; dx++) {
for(int dy=-1; dy<=1; dy++) {
if(dx==0 && dy==0) continue;
int nx = i + dx, ny = j + dy;
if(nx>=0 && nx<rows && ny>=0 && ny<cols
&& board[nx][ny].value == MINE) {
count++;
}
}
}
board[i][j].value = count;
}
}
}
注意:rand()函数生成的随机数质量有限,对于专业级游戏建议使用更高级的随机数算法如Mersenne Twister
3. 核心游戏逻辑实现
3.1 点击处理与递归展开
c复制// 处理左键点击
void handle_click(int x, int y) {
// 边界检查
if(x<0 || x>=rows || y<0 || y>=cols) return;
if(board[x][y].is_show != 0) return; // 已翻开或标记
board[x][y].is_show = 1;
if(board[x][y].value == MINE) {
game_over(false); // 踩雷游戏结束
return;
}
remain_count--;
if(remain_count == 0) {
game_over(true); // 获胜
return;
}
// 如果是空白格则递归展开周围
if(board[x][y].value == 0) {
for(int dx=-1; dx<=1; dx++) {
for(int dy=-1; dy<=1; dy++) {
if(dx==0 && dy==0) continue;
handle_click(x+dx, y+dy);
}
}
}
}
// 处理右键标记
void handle_mark(int x, int y) {
if(x<0 || x>=rows || y<0 || y>=cols) return;
if(board[x][y].is_show == 0) {
board[x][y].is_show = 2; // 标记为雷
} else if(board[x][y].is_show == 2) {
board[x][y].is_show = 0; // 取消标记
}
}
递归展开算法是扫雷最精妙的部分:
- 当点击空白格(value=0)时自动展开周围8格
- 展开过程中遇到数字格(value>0)会停止继续展开
- 通过is_show状态避免重复处理
3.2 游戏状态判断
c复制void game_over(int is_win) {
// 显示所有雷的位置
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
if(board[i][j].value == MINE) {
board[i][j].is_show = 1;
}
}
}
// 更新排行榜等操作
if(is_win) {
printf("恭喜获胜!用时:%d秒\n", (int)(time(NULL)-start_time));
} else {
printf("很遗憾,踩到地雷!\n");
}
}
4. 图形界面实现方案
4.1 基于控制台的简易实现
c复制void draw_board() {
system("clear"); // Linux/MacOS清屏,Windows用"cls"
// 打印列号
printf(" ");
for(int j=0; j<cols; j++) printf("%2d", j);
printf("\n");
for(int i=0; i<rows; i++) {
printf("%2d ", i); // 行号
for(int j=0; j<cols; j++) {
if(board[i][j].is_show == 0) {
printf(" ."); // 未翻开
} else if(board[i][j].is_show == 2) {
printf(" F"); // 标记为雷
} else {
if(board[i][j].value == MINE) {
printf(" *"); // 地雷
} else if(board[i][j].value == 0) {
printf(" "); // 空白
} else {
printf(" %d", board[i][j].value); // 数字
}
}
}
printf("\n");
}
}
4.2 基于EasyX的图形界面(Windows平台)
c复制#include <graphics.h>
void draw_graphic_board() {
initgraph(cols*30, rows*30); // 创建窗口
setbkcolor(RGB(192,192,192)); // 灰色背景
cleardevice();
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
// 绘制格子
setfillcolor(board[i][j].is_show ? WHITE : LIGHTGRAY);
fillrectangle(j*30, i*30, (j+1)*30, (i+1)*30);
// 绘制内容
if(board[i][j].is_show) {
if(board[i][j].value == MINE) {
setfillcolor(RED);
fillcircle(j*30+15, i*30+15, 10);
} else if(board[i][j].value > 0) {
char text[2] = {board[i][j].value+'0', 0};
outtextxy(j*30+10, i*30+5, text);
}
} else if(board[i][j].is_show == 2) {
setfillcolor(GREEN);
fillrectangle(j*30+5, i*30+5, j*30+25, i*30+25);
}
}
}
}
5. 常见问题与优化技巧
5.1 首次点击保护机制
原始实现可能出现首次点击就踩雷的情况,改进方案:
c复制void init_game_with_safe_start(int first_x, int first_y) {
// ...原有初始化代码...
// 确保首次点击位置周围无雷
for(int dx=-1; dx<=1; dx++) {
for(int dy=-1; dy<=1; dy++) {
int nx = first_x + dx, ny = first_y + dy;
if(nx>=0 && nx<rows && ny>=0 && ny<cols
&& board[nx][ny].value == MINE) {
// 将雷移到其他位置
board[nx][ny].value = 0;
while(1) {
int x = rand() % rows;
int y = rand() % cols;
if(board[x][y].value != MINE
&& abs(x-first_x)>1 && abs(y-first_y)>1) {
board[x][y].value = MINE;
break;
}
}
}
}
}
// 重新计算数字
// ...原有计算代码...
}
5.2 性能优化技巧
- 减少重复计算:将周围雷数计算从O(n²)优化到O(n)
c复制// 布雷时直接更新周围数字
for(int dx=-1; dx<=1; dx++) {
for(int dy=-1; dy<=1; dy++) {
int nx = x + dx, ny = y + dy;
if(nx>=0 && nx<rows && ny>=0 && ny<cols
&& board[nx][ny].value != MINE) {
board[nx][ny].value++;
}
}
}
- 使用位运算优化状态存储:将value和is_show合并为一个字节
c复制typedef struct {
unsigned char data; // 低4位: value(0-8), 高4位: 状态标志
} CompactCell;
- 递归展开改为迭代实现:避免栈溢出风险
c复制void iterative_expand(int x, int y) {
// 使用栈结构实现展开
int stack[MAX_SIZE*MAX_SIZE][2];
int top = 0;
stack[top][0] = x;
stack[top][1] = y;
top++;
while(top > 0) {
top--;
int cx = stack[top][0];
int cy = stack[top][1];
// 处理当前格子...
// 将周围格子压栈
if(board[cx][cy].value == 0) {
for(int dx=-1; dx<=1; dx++) {
for(int dy=-1; dy<=1; dy++) {
// ...压栈逻辑...
}
}
}
}
}
5.3 跨平台兼容性处理
不同平台的差异主要在于:
- 清屏命令:Windows用"cls",Linux/Mac用"clear"
- 时间函数:Windows用GetTickCount(),Unix用time()
- 随机数质量:Windows的rand()质量较差
解决方案:
c复制#ifdef _WIN32
#include <windows.h>
#define CLEAR_SCREEN "cls"
#define GET_TICK_COUNT GetTickCount()
#else
#include <sys/time.h>
#define CLEAR_SCREEN "clear"
#define GET_TICK_COUNT (time(NULL)*1000)
#endif
6. 扩展功能实现
6.1 保存和加载游戏
c复制void save_game(const char* filename) {
FILE* fp = fopen(filename, "wb");
if(!fp) return;
fwrite(&rows, sizeof(int), 1, fp);
fwrite(&cols, sizeof(int), 1, fp);
fwrite(&mine_count, sizeof(int), 1, fp);
fwrite(&remain_count, sizeof(int), 1, fp);
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
fwrite(&board[i][j], sizeof(Cell), 1, fp);
}
}
fclose(fp);
}
void load_game(const char* filename) {
FILE* fp = fopen(filename, "rb");
if(!fp) return;
fread(&rows, sizeof(int), 1, fp);
fread(&cols, sizeof(int), 1, fp);
fread(&mine_count, sizeof(int), 1, fp);
fread(&remain_count, sizeof(int), 1, fp);
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
fread(&board[i][j], sizeof(Cell), 1, fp);
}
}
fclose(fp);
}
6.2 排行榜功能
c复制typedef struct {
char name[20];
int time;
int difficulty;
} Record;
Record records[10]; // 存储前10名记录
void update_records(int time, int difficulty) {
// 查找插入位置
int pos = -1;
for(int i=0; i<10; i++) {
if(records[i].difficulty == difficulty &&
(records[i].time == 0 || time < records[i].time)) {
pos = i;
break;
}
}
if(pos != -1) {
// 移动后面的记录
for(int i=8; i>=pos; i--) {
records[i+1] = records[i];
}
// 插入新记录
printf("请输入您的名字:");
scanf("%19s", records[pos].name);
records[pos].time = time;
records[pos].difficulty = difficulty;
// 保存到文件
save_records();
}
}
void save_records() {
FILE* fp = fopen("records.dat", "wb");
if(fp) {
fwrite(records, sizeof(Record), 10, fp);
fclose(fp);
}
}
7. 测试与调试技巧
7.1 单元测试设计
c复制void test_mine_generation() {
init_game(1); // 初级难度
int count = 0;
for(int i=0; i<9; i++) {
for(int j=0; j<9; j++) {
if(board[i][j].value == MINE) count++;
}
}
assert(count == 10);
}
void test_number_calculation() {
// 创建测试棋盘
memset(board, 0, sizeof(board));
board[1][1].value = MINE;
rows = cols = 3;
// 计算数字
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
if(board[i][j].value == MINE) continue;
// ...计算逻辑...
}
}
assert(board[0][0].value == 1);
assert(board[0][1].value == 1);
assert(board[0][2].value == 1);
assert(board[1][0].value == 1);
assert(board[1][2].value == 1);
assert(board[2][0].value == 1);
assert(board[2][1].value == 1);
assert(board[2][2].value == 1);
}
7.2 调试打印函数
c复制void debug_print_board() {
printf("Debug View:\n");
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
if(board[i][j].value == MINE) {
printf(" *");
} else {
printf(" %d", board[i][j].value);
}
}
printf("\n");
}
}
在开发过程中,这个函数可以帮助快速验证雷区生成和数字计算是否正确。
