#扫雷游戏是Windows系统下的经典桌面游戏,也是程序设计中的经典案例。本文将详细介绍如何使用C语言实现一个控制台版的扫雷游戏,并分析其中的核心算法和实现原理。
一、游戏基本原理与设计思路
扫雷游戏的基本规则是:在一个9×9的棋盘上随机布置10个地雷,玩家通过输入坐标来排查地雷。如果选择的坐标有雷,游戏结束;如果选择的坐标没有雷,则显示周围雷的数量。当玩家成功排查出所有非雷区域时,游戏胜利。
实现这个游戏的关键在于解决三个问题:
- 如何表示游戏的真实雷区和玩家可见区域
- 如何处理棋盘边缘的越界问题
- 如何计算每个坐标周围雷的数量
双数组设计是解决第一个问题的核心方案。我们使用两个二维数组:mine数组存储真实雷区信息(用字符’1’表示雷,‘0’表示非雷);show数组存储玩家可见信息(用字符’*'表示未翻开的格子,数字字符表示周围雷的数量)。这样设计可以避免歧义,因为数字字符和雷字符可以明确区分。
对于第二个问题,处理棋盘边缘的越界访问,我们采用了"缓冲带"技术。通过将数组大小定义为11×11(即在9×9的基础上各加一圈缓冲),我们可以在计算周围雷数时无需额外的边界判断,大大简化了代码逻辑。缓冲带区域(第0行、第0列、第10行、第10列)在初始化时也被设置为默认值,但在游戏过程中不会被使用。
第三个问题的解决方法是:对于每个坐标,检查其周围8个格子(包括对角线相邻的格子)是否有雷,统计雷的数量并转换为数字字符显示。这种计算方式简单直观,但在处理边缘坐标时需要额外的边界检查,而缓冲带技术正好解决了这个问题。
二、核心函数实现详解
1. 初始化函数InitBoard
voidInitBoard(charboard[ROWS][COLS],introw,intcol,charset){for(inti=0;i<row;i++){for(intj=0;j<col;j++){board[i][j]=set;}}}初始化函数的作用是将整个棋盘数组初始化为指定字符。对于mine数组,我们使用字符’0’初始化,表示所有位置初始都没有地雷;对于show数组,我们使用字符’*'初始化,表示所有位置初始都是未翻开的状态。
初始化过程遍历整个数组(包括缓冲带),将每个元素设置为指定值。这种设计虽然初始化了整个11×11的数组,但大大简化了后续的边界处理,因为缓冲带区域不会被使用,因此不需要特别处理。
2. 埋雷函数SetMine
voidSetMine(charmine[ROWS][COLS],introw,intcol){intcount=EASY_COUNT;srand(time(0));while(count){intx=rand()%row+1;inty=rand()%col+1;if(mine[x][y]=='0'){mine[x][y]='1';count--;}}}埋雷函数负责在有效区域内随机布置指定数量的地雷。这里有几个关键点:
- 随机数种子初始化:使用
srand(time(0))确保每次运行游戏时地雷位置都是随机的 - 坐标生成:
x = rand() % row + 1和y = rand() % col + 1确保生成的坐标在有效区域(1row和1col)内 - 重复检测:通过
if (mine[x][y] == '0')确保不会在同一个位置重复布置地雷
需要注意的是,这里的row和col参数应该传递的是有效区域的大小(即9),而不是缓冲带后的大小(即11)。否则生成的坐标可能会超出有效区域,导致越界访问。
3. 棋盘显示函数DisplayBoard
voidDisplayBoard(charboard[ROWS][COLS],introw,intcol){for(inti=0;i<=col;i++)printf("%d ",i);printf("\n");for(inti=1;i<=row;i++){printf("%d ",i);for(intj=1;j<=col;j++){printf("%c ",board[i][j]);}printf("\n");}}显示函数负责在控制台打印当前的棋盘状态,包括行号和列号。这里有几个需要注意的细节:
- 列号从0开始打印:
for (int i = 0; i <= col; i++) - 行号从1开始打印:
for (int i = 1; i <= row; i++) - 仅显示有效区域:外层循环的范围是
i=1~row和j=1~col,避免显示缓冲带内容 - 格式化输出:使用
%c确保每个字符占据固定宽度,使棋盘显示整齐美观
这种设计使得玩家可以方便地根据坐标输入进行操作,同时保持棋盘显示的整洁美观。
三、排雷逻辑与胜负判断机制
1. 排雷函数FindMine
voidFindMine(charmine[ROWS][COLS],charshow[ROWS][COLS],introw,intcol){intwin=0;intx,y;while(win<col*row-EASY_COUNT){printf("请输入排雷的位置:>");scanf("%d %d",&x,&y);if((x>=1&&x<=row)&&(y>=1&&y<=col)){if(mine[x][y]=='1'){DisplayBoard(mine,ROW,COL);printf("很遗憾,排雷失败,你被炸“死”了...\n");break;}intn=GetMineCount(mine,x,y);show[x][y]=n+'0';system("cls");DisplayBoard(show,ROW,COL);win++;}elseprintf("无效输入,请重新输入...\n");}if(win>=col*row-EASY_COUNT){printf("恭喜你,排雷成功...\n");DisplayBoard(mine,ROW,COL);}}排雷函数是游戏的核心逻辑,负责处理玩家的输入并更新游戏状态。这里有几个关键点:
- 胜利条件判断:
while (win < col * row - EASY_COUNT) - 坐标有效性检测:
if ((x >= 1 && x <= row) && (y >= 1 && y <= col)) - 地雷检测:
if (mine[x][y] == '1') - 周围雷数计算:
int n = GetMineCount(mine, x, y) - 更新显示:
show[x][y] = n + '0'
2. 周围雷数计算函数GetMineCount
intGetMineCount(charmine[ROWS][COLS],intx,inty){returnmine[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');}周围雷数计算函数负责统计指定坐标周围8个格子中的地雷数量。这里有几个需要注意的细节:
- 直接计算周围8个格子:通过简单的加法操作,统计周围8个格子中的’1’字符数量
- 转换为数字:
n + '0'将整数转换为对应的数字字符 - 减去基准值:
- (8 * '0')是为了将字符’0’转换为数值0,因为字符’0’的ASCII码值是48
这种方法虽然简单,但存在一个问题:当棋盘边缘的格子被检查时,周围的某些格子可能超出有效区域,但由于我们使用了缓冲带技术,这些越界访问实际上不会导致程序崩溃。不过,这种方法并没有真正处理越界问题,只是利用了缓冲带的存在。
四、游戏运行效果与改进建议
1. 游戏运行效果
当玩家运行这个扫雷游戏时,会看到以下界面:
********扫雷游戏********** ********* 1 play********** ********* 0 exit********** ********扫雷游戏********** 请输入你的选择:>选择开始游戏后,会看到一个9×9的棋盘,初始状态全部显示为’*':
0 1 2 3 4 5 6 7 8 9 1 * * * * * * * * * * 2 * * * * * * * * * * 3 * * * * * * * * * * 4 * * * * * * * * * * 5 * * * * * * * * * * 6 * * * * * * * * * * 7 * * * * * * * * * * 8 * * * * * * * * * * 9 * * * * * * * * * *玩家输入坐标(如"2 3"),如果该位置没有地雷,则显示周围地雷的数量:
0 1 2 3 4 5 6 7 8 9 1 * * * * * * * * * * 2 * * 1 * * * * * * * 3 * * * * * * * * * * 4 * * * * * * * * * * 5 * * * * * * * * * * 6 * * * * * * * * * * 7 * * * * * * * * * * 8 * * * * * * * * * * 9 * * * * * * * * * *如果玩家踩中地雷,游戏会显示真实雷区并提示失败:
0 1 2 3 4 5 6 7 8 9 1 * * * * * * * * * * 2 * * * * * * * * * * 3 * 1 * * * * * * * * 4 * * * * * * * * * * 5 * * * * * * * * * * 6 * * * * * * * 1 * * 7 * * * * * * * * * * 8 * * * * * * * * * * 9 * * * * * * * * * * 12. 改进建议
虽然这个扫雷游戏实现了基本功能,但仍存在一些可以改进的地方:
自动展开空白区域功能缺失
当前游戏没有实现点击空白格(周围雷数为0)时自动展开周围区域的功能,这是标准扫雷游戏的重要特性。可以通过添加递归函数Expand来实现:
voidExpand(charmine[ROWS][COLS],charshow[ROWS][COLS],intx,inty){if((x<1||x>ROW)||(y<1||y>COL))return;// 越界检查if(show[x][y]!='*')return;// 已翻开或标记的格子不再处理intcount=GetMineCount(mine,x,y);show[x][y]=count+'0';// 显示周围雷数if(count==0)// 如果周围没有雷,递归展开周围8个格子{Expand(mine,show,x-1,y-1);// 左上Expand(mine,show,x-1,y);// 上Expand(mine,show,x-1,y+1);// 右上Expand(mine,show,x,y-1);// 左Expand(mine,show,x,y+1);// 右Expand(mine,show,x+1,y-1);// 左下Expand(mine,show,x+1,y);// 下Expand(mine,show,x+1,y+1);// 右下}}标记功能缺失
标准扫雷游戏支持右键标记疑似地雷的位置,但当前游戏没有实现这一功能。可以通过以下方式扩展:
// 修改FindMine函数voidFindMine(charmine[ROWS][COLS],charshow[ROWS][COLS],introw,intcol){intwin=0;intmine_count=EASY_COUNT;// 剩余地雷数量intx,y;intaction;// 1表示左键,2表示右键while(win+mine_count<row*col)// 胜利条件{printf("请输入排雷的位置和操作(1=左键,2=右键):>");scanf("%d %d %d",&x,&y,&action);if((x<1||x>row)||(y<1||y>col)){printf("无效输入,请重新输入...\n");continue;}if(action==1)// 左键:翻开格子{if(mine[x][y]=='1'){DisplayBoard(mine,ROW,COL);printf("很遗憾,排雷失败,你被炸“死”了...\n");break;}Expand(mine,show,x,y);// 使用Expand函数自动展开win++;}elseif(action==2)// 右键:标记地雷{if(show[x][y]=='*')show[x][y]='F';// 标记为地雷elseif(show[x][y]=='F')show[x][y]='*';// 取消标记mine_count=GetRemainingMines(mine,show);// 更新剩余地雷数量}else{printf("无效操作,请重新输入...\n");continue;}system("cls");DisplayBoard(show,ROW,COL);if(mine_count==0)// 所有地雷都被正确标记{printf("恭喜你,排雷成功...\n");break;}}// 最终判断胜负if(win+mine_count==row*col)printf("恭喜你,排雷成功...\n");elseprintf("游戏结束...\n");}胜负判断条件错误
当前游戏的胜负判断条件存在错误:win < col * row - EASY_COUNT。这里的问题在于:col和row参数应该传递的是有效区域的大小(即9),而不是缓冲带后的大小(即11)。正确的条件应该是:win < ROW * COL - EASY_COUNT,其中ROW和COL是宏定义的9。
棋盘显示范围问题
当前的显示函数DisplayBoard的循环范围没有严格限制在有效区域,可能会显示缓冲带的内容。应该修改为:
voidDisplayBoard(charboard[ROWS][COLS],introw,intcol){printf(" ");for(inti=1;i<=col;i++)// 列号从1开始printf("%d ",i);printf("\n");for(inti=1;i<=row;i++){printf("%d ",i);for(intj=1;j<=col;j++)// 仅显示有效区域{printf("%c ",board[i][j]);}printf("\n");}}界面美化建议
可以通过ANSI转义码在控制台中实现彩色输出,使游戏界面更加美观。例如:
// 在DisplayBoard函数中使用颜色voidDisplayBoard彩色版(charboard[ROWS][COLS],introw,intcol){for(inti=1;i<=col;i++)// 列号从1开始printf("%d ",i);printf("\n");for(inti=1;i<=row;i++){for(intj=1;j<=col;j++)// 仅显示有效区域{if(board[i][j]=='F')// 红色标记printf("\033[31mF\033[0m ");elseif(board[i][j]=='*')// 灰色未翻开printf("\033[37m*\033[0m ");elseif(board[i][j]=='0')// 空白区域printf(" ");else// 数字字符printf("\033[34m%d\033[0m ",board[i][j]-'0');}printf("\n");}}在Windows 10+系统中,需要先启用ANSI支持:
#include<windows.h>voidenable_ansi(){HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);DWORD mode;GetConsoleMode(hOut,&mode);mode|=ENABLE_VIRTUAL_TERMINAL_PROCESSING;SetConsoleMode(hOut,mode);}动态内存分配优化
当前游戏使用固定大小的数组,可以通过动态内存分配优化内存使用,特别是对于不同难度等级的棋盘:
// 修改为动态内存分配voidStartGame(){char(*mine)[COLS]=malloc(ROWS*sizeof(char[COLS]));char(*show)[COLS]=malloc(ROWS*sizeof(char[COLS]));// 初始化棋盘InitBoard(mine,ROWS,COLS,'0');InitBoard(show,ROWS,COLS,'*');// 埋雷SetMine(mine,ROW,COL);// 显示棋盘DisplayBoard(show,ROW,COL);// 扫雷FindMine(mine,show,ROW,COL);// 释放内存free(mine);free(show);}结构体封装优化
可以通过结构体封装棋盘状态,提高代码的可读性和维护性:
typedefstruct{inthas_mine;// 是否有地雷intis_opened;// 是否已翻开intis_flagged;// 是否已标记intmine_count;// 周围地雷数量}Cell;typedefstruct{Cell board[ROWS][COLS];introw;intcol;intmine_count;}Board;计时功能添加
可以添加计时功能,记录玩家的游戏时间:
#include<time.h>voidStartGame(){// ...clock_tstart_time=clock();// 记录开始时间// 扫雷FindMine(mine,show,ROW,COL);// 计算游戏时间doubleelapsed_time=(double)(clock()-start_time)/CLOCKS_PER_SEC;printf("游戏用时: %.2f秒\n",elapsed_time);// ...}五、完整代码与执行效果
以下是修改后的完整代码,包含自动展开和标记功能:
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<stdlib.h>#include<time.h>// 用于棋盘的显示#defineROW9#defineCOL9// 用于真实的处理(添加缓冲带)#defineROWSROW+2#defineCOLSCOL+2// 初级难度地雷数量#defineEASY_COUNT10voidStartGame();voidInitBoard(charboard[ROWS][COLS],introw,intcol,charset);voidSetMine(charmine[ROWS][COLS],introw,intcol);voidDisplayBoard(charboard[ROWS][COLS],introw,intcol);voidFindMine(charmine[ROWS][COLS],charshow[ROWS][COLS],introw,intcol);intGetMineCount(charmine[ROWS][COLS],intx,inty);voidExpand(charmine[ROWS][COLS],charshow[ROWS][COLS],intx,inty);intmain(intargc,char*argv[]){intselect=1;while(select){printf("********扫雷游戏**********\n");printf("*********1 play**********\n");printf("*********0 exit**********\n");printf("********扫雷游戏**********\n");printf("请输入你的选择:>");scanf("%d",&select);if(select==0)break;if(select!=1){printf("输入有误,请重新输入...\n");continue;}StartGame();}printf("退出游戏,欢迎下次使用...\n");return0;}voidStartGame(){charmine[ROWS][COLS];charshow[ROWS][COLS];// 初始化棋盘InitBoard(mine,ROWS,COLS,'0');// 字符'0'代表无雷,'1'代表雷InitBoard(show,ROWS,COLS,'*');// 埋雷SetMine(mine,ROW,COL);// 显示棋盘DisplayBoard(show,ROW,COL);// 扫雷FindMine(mine,show,ROW,COL);}voidInitBoard(charboard[ROWS][COLS],introw,intcol,charset){for(inti=0;i<row;i++){for(intj=0;j<col;j++){board[i][j]=set;}}}voidSetMine(charmine[ROWS][COLS],introw,intcol){intcount=EASY_COUNT;srand((unsigned)time(0));while(count){intx=rand()%row+1;inty=rand()%col+1;if(mine[x][y]=='0'){mine[x][y]='1';count--;}}}voidDisplayBoard(charboard[ROWS][COLS],introw,intcol){printf(" ");for(inti=1;i<=col;i++)// 列号从1开始printf("%d ",i);printf("\n");for(inti=1;i<=row;i++){printf("%d ",i);for(intj=1;j<=col;j++)// 仅显示有效区域{if(board[i][j]=='F')// 红色标记printf("\033[31mF\033[0m ");elseif(board[i][j]=='*')// 灰色未翻开printf("\033[37m*\033[0m ");elseif(board[i][j]=='0')// 空白区域printf(" ");else// 数字字符printf("\033[34m%d\033[0m ",board[i][j]-'0');}printf("\n");}}voidFindMine(charmine[ROWS][COLS],charshow[ROWS][COLS],introw,intcol){intwin=0;intmine_count=EASY_COUNT;// 剩余地雷数量intx,y;intaction;// 1表示左键,2表示右键while(win+mine_count<row*col)// 胜利条件{printf("请输入排雷的位置和操作(1=左键,2=右键):>");scanf("%d %d %d",&x,&y,&action);if((x<1||x>row)||(y<1||y>col)){printf("无效输入,请重新输入...\n");continue;}if(action==1)// 左键:翻开格子{if(mine[x][y]=='1'){DisplayBoard(mine,ROW,COL);printf("很遗憾,排雷失败,你被炸“死”了...\n");break;}Expand(mine,show,x,y);// 使用Expand函数自动展开win++;}elseif(action==2)// 右键:标记地雷{if(show[x][y]=='*')show[x][y]='F';// 标记为地雷elseif(show[x][y]=='F')show[x][y]='*';// 取消标记mine_count=GetRemainingMines(mine,show);// 更新剩余地雷数量}else{printf("无效操作,请重新输入...\n");continue;}system("cls");DisplayBoard(show,ROW,COL);if(mine_count==0)// 所有地雷都被正确标记{printf("恭喜你,排雷成功...\n");break;}}// 最终判断胜负if(win+mine_count==row*col)printf("恭喜你,排雷成功...\n");elseprintf("游戏结束...\n");}intGetMineCount(charmine[ROWS][COLS],intx,inty){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'));}voidExpand(charmine[ROWS][COLS],charshow[ROWS][COLS],intx,inty){if((x<1||x>ROW)||(y<1||y>COL))return;// 越界检查if(show[x][y]!='*')return;// 已翻开或标记的格子不再处理intcount=GetMineCount(mine,x,y);show[x][y]=count+'0';// 显示周围雷数if(count==0)// 如果周围没有雷,递归展开周围8个格子{Expand(mine,show,x-1,y-1);// 左上Expand(mine,show,x-1,y);// 上Expand(mine,show,x-1,y+1);// 右上Expand(mine,show,x,y-1);// 左Expand(mine,show,x,y+1);// 右Expand(mine,show,x+1,y-1);// 左下Expand(mine,show,x+1,y);// 下Expand(mine,show,x+1,y+1);// 右下}}intGetRemainingMines(charmine[ROWS][COLS],charshow[ROWS][COLS]){intcount=0;for(inti=1;i<=ROW;i++){for(intj=1;j<=COL;j++){if(mine[i][j]=='1'&&show[i][j]!='F')count++;}}returncount;}六、总结与学习收获
通过实现这个扫雷游戏,我们可以学习到以下几个重要的C语言编程知识点:
1. 二维数组的使用与初始化
我们使用了两个二维数组mine和show来分别表示真实雷区和玩家可见区域,通过InitBoard函数实现了数组的初始化。这种设计模式在处理网格状数据时非常有用。
2. 随机数生成与种子初始化
通过srand(time(0))和rand()函数实现了地雷的随机布置。这是程序中实现随机性的关键部分。
3. 递归算法的应用
Expand函数使用递归算法实现了点击空白格时自动展开周围区域的功能。递归是一种强大的算法设计方法,可以简化许多复杂问题的解决过程。
4. 用户界面设计
通过DisplayBoard函数实现了棋盘的显示,并通过ANSI转义码实现了彩色输出。这展示了如何在控制台环境中创建友好的用户界面。
5. 游戏状态管理
通过FindMine函数管理游戏状态,包括处理玩家输入、更新显示、判断胜负等。这展示了如何设计和实现一个完整的游戏循环。
6. 边界处理技术
通过将数组大小定义为11×11(即在9×9的基础上各加一圈缓冲),我们可以在计算周围雷数时无需额外的边界判断,大大简化了代码逻辑。
7. 动态内存分配
通过malloc和free函数实现了动态内存分配,这在处理不同难度等级的棋盘时非常有用。
8. 结构体封装
通过Board和Cell结构体封装了棋盘状态,提高了代码的可读性和维护性。
通过学习和实现这个扫雷游戏,我们可以深入理解C语言的数组操作、函数调用、递归算法等核心概念,同时也能掌握游戏开发的基本思路和技巧。这是一个很好的C语言学习项目,可以帮助我们巩固和提高编程能力。
希望这篇博客能够帮助你理解C语言扫雷游戏的实现原理,并激发你进一步学习和改进这个项目的兴趣。