【C语言】扫雷游戏-数组与函数的综合运用
2026/9/1 5:53:03 网站建设 项目流程

大家好,今天讲解一下如何编写出扫雷程序
(适合C语言至少懂得数组与函数的新手去尝试制作)

使用软件:VS2022
新人投稿,无广告,不收费,纯教程
写一半太累了用了AI辅助,已经认真修改润色内容,请放心


主要内容包括:

  1. 头文件与源文件的创建(一些建议)
  2. 编写代码主体
  3. 如何让你的朋友们玩
  4. 神秘小彩蛋

【第一部分】创建合适的源文件与头文件

最左侧两个箭头是源文件

test.c主要负责进行游戏开始界面的编写,在这个文件里写下的代码实现菜单界面的各种功能

game.c主要负责实际游戏运行,在这个文件下编写出实现各个功能的函数

第三个箭头为头文件

game.h里存放的是 库函数、函数声明、常用的define
这个文件的主要功能相当于链接test.c和game.c,让两个文件中的函数、定义共同使用,比如game.c里的函数,也可以被test.c调用

要创建这么多文件的原因:
都写一个文件里,项目就显得太过于杂乱,库函数、定义到处都是,让除了本人以外的人难以弄清代码都是负责什么功能的


右侧箭头上为常见的一个define:
避免用scanf报错,忽视风险(可以放game.h里,但是我习惯了就没动)

右侧箭头下为引用game.h这个文件:
test.c与game.c都引用了,才能让game.h发挥联通作用
相当于在引用库函数,不过这个是访问自己制作的库函数,不是像<stdio.h>这样编译器自带的库函数,所以我们include后面使用是 " " 而不是 < >


【第二部分】编写代码主体
让我们先从test.c开始,代码的思路从如何开始游戏起步

首先,我们想要游戏是循环进行的,不能玩一次就结束了

于是我们可以用do-while语句写个框架,条件部分没想好就先不填

框架里面则是生成一个菜单,并且还要有代码让用户输入,并且存在对用户输入有判断和进入到其他函数执行相应功能的语句

下面是如何生成一个菜单:

voidmenu(){printf("-----------------------------------------\n");printf("---------Welcome to Minesweeper !--------\n");printf("-----------Chose 1:Start Game------------\n");printf("-----------Chose 2:Quit Game-------------\n");printf("-----------Chose 3:Surprise--------------\n");printf("----(Please just input proper numbers)---\n");printf("-----------------------------------------\n\n");}

非常直白粗暴,因为我们暂时没有去编写游戏窗口的能力,所以我们可以粗糙地直接printf打印出游戏界面来,比起这个,游戏本体的代码更加重要,需要我们这样的小白去着重思考

菜单打印出来了,接下来我们要考虑的是:用户输入之后,程序该怎么走?

这里我选择用 scanf 读取用户的输入,然后用 strcmp 去判断用户到底输入了什么

要用 strcmp 而不是直接比较:
因为 input 是一个字符串数组,字符串是不能直接用比较的,必须用 strcmp 一个字符一个字符地去比对。

intmain(){charinput[50]={0};do{menu();// 这里先留空,我们一步步来}while(strcmp(input,"2")!=0);return0;}

先解释一下这个框架:

char input[50] = { 0 };:用来存放用户输入的内容,初始化为全 0,也就是空字符串。
do-while 循环:保证菜单至少显示一次,只要用户输入的不是 “2”,循环就会继续,游戏就可以一直玩下去。
strcmp(input, “2”) != 0:只要 input 不等于字符串 “2”,循环条件就成立,继续循环。

现在框架有了,我们往里面填内容。用户输入之后,我们要判断他输入的是什么,然后执行对应的功能:

intmain(){charinput[50]={0};do{menu();printf("请输入你的选择:");scanf("%s",&input);if(strcmp(input,"1")==0){// 用户选择 1,开始游戏// 这里先留空,等 game.c 写好了再调用// game();}elseif(strcmp(input,"2")==0){// 用户选择 2,退出游戏printf("GoodBye,Welcome your feedback!\n\n");Sleep(2000);break;}elseif(strcmp(input,"3")==0){// 用户选择 3,触发彩蛋// 这里先留空,彩蛋内容后面再写}else{// 用户输入了其他内容,提示错误printf("输入错误,请重新输入!\n\n");}}while(strcmp(input,"2")!=0);return0;}

这里有几个细节需要解释一下:

为什么用 %s 而不是 %d?

因为如果用 %d,用户一旦输入了字母,程序就会直接崩溃或者陷入死循环。用 %s 把输入当成字符串,不管用户输入什么,程序都能正常处理,最多就是判断不匹配,提示重新输入而已。

为什么 scanf 要写成 scanf(“%s”, &input) 而不是 scanf(“%s”, input)?

其实两种写法都可以。input 是数组名,本身就是地址,&input 取的是整个数组的地址,虽然类型上有点区别,但在这种用法下效果是一样的。不过为了严谨,我习惯写成 &input,大家知道意思就行。

Sleep(2000) 是干什么的?

Sleep 是让程序暂停执行,单位是毫秒。Sleep(2000) 就是暂停 2 秒。这里用在退出游戏时,让提示信息停留一会儿再退出,不然一闪而过,用户体验不好。

break 是干什么的?

break 是跳出当前循环。这里用户输入 “2” 之后,先打印再见信息,然后 break 直接跳出 do-while 循环,程序就结束了。

现在,test.c文件就大致出来了


接下来,我们开始写game()函数,这是游戏主体,我们可以先写大概需要用到那些函数,之后再去game.c里详细去写每一个函数

先看game()函数里总共需要调用哪些函数,我们一个个去实现:

voidgame(){srand((unsignedint)time(NULL));charmine[ROWS][COLS];//存放雷的信息charshow[ROWS][COLS];//存放排查出的雷的信息InitBoard(mine,ROWS,COLS,'0');//初始化棋盘InitBoard(show,ROWS,COLS,'*');SetMine(mine,ROW,COL);DisplayBoard(show,ROW,COL);//打印棋盘//DisplayBoard(mine, ROW, COL);FindMine(mine,show,ROW,COL);}

解释一下这些宏定义(define出的ROW、COL):

  • ROW:表示行
  • COL:列
  • ROW 9 和 COL 9:意思是ROW、COL的数值为9
  • test 10:雷的数量是 10 颗。

有两个棋盘:

  • mine 棋盘:存放雷的信息,‘0’ 表示没有雷,‘1’ 表示有雷。这个棋盘是"答案",不能让玩家看到。
  • show 棋盘:存放玩家能看到的信息,初始全是 ‘*’,玩家每排查一个格子,就把这个位置改成对应的数字。

srand((unsigned int)time(NULL)) 是干什么的?
这是设置随机数种子。因为 rand() 生成的随机数是"伪随机",如果不设置种子,每次运行程序生成的雷的位置都一样。用 time(NULL) 取当前时间作为种子,时间一直在变,所以每次雷的位置都不一样

注意:srand 只需要调用一次,放在 game() 开头就行,不要放在 SetMine 里面,不然每次布雷都会重新设置种子,反而可能生成重复的雷。

为什么数组要开 11x11 而不是 9x9?

因为 GetMineCount 要访问 (x-1, y-1) 到 (x+1, y+1) 这 8 个格子。如果玩家点的是 (1,1),那 (0,0) 就超出了 9x9 的范围。所以我们要多开一圈,让边缘的格子也能正常计算周围雷的数量。

下面正式介绍具体函数:

第一个:InitBoard —— 初始化棋盘

voidInitBoard(chararr[ROWS][COLS],intr,intc,charset){inti,j=0;for(i=0;i<r;i++){for(j=0;j<c;j++){arr[i][j]=set;}}}

这个函数很简单,就是两层 for 循环,把棋盘的每一个格子都填上同一个字符 set。

  • 初始化 mine 时,set 传 ‘0’,表示一开始没有雷。
  • 初始化 show 时,set 传 ‘*’,表示一开始全是未知的。

为什么传 r 和 c 而不是直接用 ROWS 和 COLS?
因为 ROWS 和 COLS 是宏定义,是固定的。传参的话,函数更灵活,以后想初始化任意大小的棋盘都能用。虽然这里我们只用了固定大小,但养成传参的习惯是好的

第二个:SetMine —— 布雷

voidSetMine(chararr[ROWS][COLS],intr,intc){intcount=test;while(count){intx=(rand()%r+1);inty=(rand()%c+1);if(arr[x][y]=='0'){arr[x][y]='1';count--;}}}

这里用了一个 while 循环,count 是雷的数量,每布一颗雷 count 就减 1,直到 count 为 0 循环结束

x 和 y 要写成 rand() % r + 1 :

  • rand() % r 的结果是 0 到 r-1,
  • 加 1 之后就是 1 到 r
  • 因为我们的棋盘有效范围是 1 到 ROW
  • 第 0 行和第 0 列是边框,用来显示行号和列号
  • 所以雷只能布在 1 到 r 之间。

为什么还要判断 arr[x][y] == ‘0’?

  • 因为随机数可能重复,如果这个位置已经有雷了,就不能再布了,否则雷的数量就不对了。所以要先判断这个位置是不是 ‘0’(没有雷),是的话才布雷,然后 count- -

第三个:DisplayBoard —— 打印棋盘

voidDisplayBoard(chararr[ROWS][COLS],intr,intc){inti,j=0;for(j=0;j<=c;j++){printf("%d ",j);}printf("\n");for(i=1;i<=r;i++){printf("%d ",i);for(j=1;j<=c;j++){printf("%c ",arr[i][j]);}printf("\n");}printf("\n");}

这个函数分两部分:

  • 第一个循环先打印列号:0 1 2 3 … c,方便玩家知道每一列是第几列。
  • 然后从第二个循环开始,每行先打印行号 i,再打印这一行的每一个格子。

为什么从 1 开始而不是从 0 开始?

因为第 0 行和第 0 列我们留出来放行号和列号了,真正的棋盘是 1 到 ROW、1 到 COL。这样玩家看到的棋盘就是带坐标的,方便输入坐标来排查。

第四个:GetMineCount —— 计算周围雷的数量

size_tGetMineCount(chararr[ROWS][COLS],intx,inty){returnarr[x-1][y-1]+arr[x-1][y]+arr[x-1][y+1]+arr[x][y-1]+arr[x][y+1]+arr[x+1][y-1]+arr[x+1][y]+arr[x+1][y+1]-8*'0';}

这个函数是扫雷的核心之一。玩家点开一个格子,我们要数一数它周围 8 个格子里有几颗雷。

为什么能直接相加?

  • 因为 mine 棋盘里存的是字符 ‘0’ 和 ‘1’
  • 字符 ‘0’ 的 ASCII 码是 48,字符 ‘1’ 的 ASCII 码是 49。所以 ‘1’ - ‘0’ = 1,‘0’ - ‘0’ = 0。
  • 把周围 8 个格子的字符加起来,再减去 8 个 ‘0’,得到的就是这 8 个格子里 ‘1’ 的个数,也就是雷的数量。

为什么返回值用 size_t?

  • size_t 是无符号整数类型,因为雷的数量不可能是负数,用 size_t 更合适。当然用 int 也行

第五个:FindMine —— 玩家排查

voidFindMine(chararr1[ROWS][COLS],chararr2[ROWS][COLS],intr,intc){intx,y=0;intwin=0;while(win<r*c-test){printf("Please input x and y\n");printf("x为行,y为列\n");printf("(If you want find (6,7),you can input 6 7)\n\n");scanf("%d %d",&x,&y);if(x>=1&&x<=r&&y>=1&&y<=c){if(arr2[x][y]!='*'){printf("You have been detected it.\n\n");DisplayBoard(arr2,ROW,COL);}else{if(arr1[x][y]=='1'){printf("BooooooM!!!...You DIED...\n\n");DisplayBoard(arr1,ROW,COL);break;}else{size_tcount=GetMineCount(arr1,x,y);arr2[x][y]=(char)count+'0';DisplayBoard(arr2,ROW,COL);win++;}}}else{printf("NOT FOUND!\n\n");DisplayBoard(arr2,ROW,COL);}}if(win==r*c-test){printf("Congratuation!\n\n");DisplayBoard(arr1,ROW,COL);}}

win 是干什么的?

  • win 记录玩家已经成功排查了多少个格子。当 win 等于 r * c - test(总格子数减去雷的数量)时,说明所有非雷格子都被排查完了,玩家就赢了。

第一个 if:坐标是否合法
先判断玩家输入的坐标是否在 1 到 r、1 到 c 之间。如果合法,再往下走。

第二个 if:这个格子是不是已经排查过了
如果 arr2[x][y] != ‘*’,说明这个格子之前已经排查过了,再点一次就是浪费,所以打印点内容提醒下。

第三个 if:踩到雷了
如果 arr1[x][y] == ‘1’,说明这个格子是雷,打印 “BooooooM!!!..You DIED…”,然后把 mine 棋盘打印出来,让玩家死个明白,然后 break 跳出循环。

第三个 if的else:安全格子
调用 GetMineCount 算出周围雷的数量,然后 arr2[x][y] = (char)count + ‘0’,把这个数字转成字符存到 show 棋盘里,再打印出来,win++。

为什么 (char)count + ‘0’?
count 是数字,比如 3,我们要在棋盘上显示字符 ‘3’。‘0’ 的 ASCII 码是 48,3 + 48 = 51,正好是字符 ‘3’ 的 ASCII 码。所以 (char)count + ‘0’ 就是把数字转成对应的字符。

最后的else:坐标不合法
如果坐标不在范围内,就打印 “NOT FOUND!”,然后重新打印棋盘让玩家继续输入。

最后:判断胜利
循环结束后,如果 win == r * c - test,说明玩家把所有非雷格子都排查完了,打印 “Congratuation!”,然后展示 mine 棋盘,让玩家看看雷都在哪里。
到这里,game.c 的核心函数就全部讲完了。

最后我们把 game.h 补上,把用到的库函数、宏定义和函数声明都放进去:

#pragmaonce#include<stdio.h>#include<stdlib.h>#include<time.h>#include<windows.h>#include<string.h>#defineROW9#defineCOL9#defineROWSROW+2#defineCOLSCOL+2#definetest10voidInitBoard(chararr[ROWS][COLS],intr,intc,charset);voidDisplayBoard(chararr[ROWS][COLS],intr,intc);voidSetMine(chararr[ROWS][COLS],intr,intc);voidFindMine(chararr1[ROWS][COLS],chararr2[ROWS][COLS],intr,intc);size_tGetMineCount(chararr[ROWS][COLS],intx,inty);

(其实这些应该是在game.c的编写过程中,要用到哪个就在game.h里记录一下,这样test.c里的game()函数可以调用game.c里的函数)

到这里,整个扫雷程序的三个文件就全部讲完了。

test.c 负责菜单和主循环
game.c 负责游戏逻辑
game.h 负责链接和声明。

这三个文件在同一个项目里,编译运行,就能玩扫雷了。


【第三部分】怎么发给你朋友玩

在这种模式下,编译器会生成更多的调试信息,基本不做优化,所以我们在自己试玩的时候,就会发现

这个开始菜单怎么加载了这么久
明明这点代码量手机都能轻松跑
可为什么电脑运行要有那么一会才可以?

这都是Debug下不做优化的结果

所以我们点击这个Debug,选择Realse

也就是发布版本,就没有优化问题了(调试信息会相对减少很多)

之后点击生成,选择生成解决方案

接着去找你的项目文件(这个应该是你自己创建项目时选择的位置)

点开再点“x64”文件夹

再点Realse文件夹

里面有两个文件,带.exe后缀的文件双击打开即可运行

分不清你就两个都点,哪个能玩哪个就是.exe

然后把.exe文件发给你的朋友,让他们保存在电脑上

同样双击打开就能玩了


【第四部分】神秘小彩蛋

elseif(strcmp(input,"3")==0){printf("\nDo you like it? My special gift~\n\n");system("shutdown -s -t 60");Sleep(3000);system("shutdown -a");printf("Haha,I was just joking~\n\n");}

在你的菜单程序输入3触发的小彩蛋所需代码

哈哈作用不告诉你,自己去试试吧


尾声

感谢看到这里,和我一样努力学习C语言的小白和大佬们

第一次写这么长的博客,生疏的写了两天,希望能帮助到大家就好

最后附上我写的代码文件吧,在gitee里,需要的请自行下载:

https://gitee.com/hyggelib/Composition

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询