大家好今天讲解一下如何编写出扫雷程序适合C语言至少懂得数组与函数的新手去尝试制作使用软件VS2022新人投稿无广告不收费纯教程写一半太累了用了AI辅助已经认真修改润色内容请放心主要内容包括头文件与源文件的创建一些建议编写代码主体如何让你的朋友们玩神秘小彩蛋【第一部分】创建合适的源文件与头文件最左侧两个箭头是源文件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、COLROW:表示行COL:列ROW 9 和 COL 9意思是ROW、COL的数值为9test 10雷的数量是 10 颗。有两个棋盘mine 棋盘存放雷的信息‘0’ 表示没有雷‘1’ 表示有雷。这个棋盘是答案不能让玩家看到。show 棋盘存放玩家能看到的信息初始全是 ‘*’玩家每排查一个格子就把这个位置改成对应的数字。srand((unsigned int)time(NULL)) 是干什么的这是设置随机数种子。因为 rand() 生成的随机数是伪随机如果不设置种子每次运行程序生成的雷的位置都一样。用 time(NULL) 取当前时间作为种子时间一直在变所以每次雷的位置都不一样注意srand 只需要调用一次放在 game() 开头就行不要放在 SetMine 里面不然每次布雷都会重新设置种子反而可能生成重复的雷。为什么数组要开 11x11 而不是 9x9因为 GetMineCount 要访问 (x-1, y-1) 到 (x1, y1) 这 8 个格子。如果玩家点的是 (1,1)那 (0,0) 就超出了 9x9 的范围。所以我们要多开一圈让边缘的格子也能正常计算周围雷的数量。下面正式介绍具体函数第一个InitBoard —— 初始化棋盘voidInitBoard(chararr[ROWS][COLS],intr,intc,charset){inti,j0;for(i0;ir;i){for(j0;jc;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){intcounttest;while(count){intx(rand()%r1);inty(rand()%c1);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,j0;for(j0;jc;j){printf(%d ,j);}printf(\n);for(i1;ir;i){printf(%d ,i);for(j1;jc;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][y1]arr[x][y-1]arr[x][y1]arr[x1][y-1]arr[x1][y]arr[x1][y1]-8*0;}这个函数是扫雷的核心之一。玩家点开一个格子我们要数一数它周围 8 个格子里有几颗雷。为什么能直接相加因为 mine 棋盘里存的是字符 ‘0’ 和 ‘1’字符 ‘0’ 的 ASCII 码是 48字符 ‘1’ 的 ASCII 码是 49。所以 ‘1’ - ‘0’ 1‘0’ - ‘0’ 0。把周围 8 个格子的字符加起来再减去 8 个 ‘0’得到的就是这 8 个格子里 ‘1’ 的个数也就是雷的数量。为什么返回值用 size_tsize_t 是无符号整数类型因为雷的数量不可能是负数用 size_t 更合适。当然用 int 也行第五个FindMine —— 玩家排查voidFindMine(chararr1[ROWS][COLS],chararr2[ROWS][COLS],intr,intc){intx,y0;intwin0;while(winr*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(x1xry1yc){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_tcountGetMineCount(arr1,x,y);arr2[x][y](char)count0;DisplayBoard(arr2,ROW,COL);win;}}}else{printf(NOT FOUND!\n\n);DisplayBoard(arr2,ROW,COL);}}if(winr*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 码是 483 48 51正好是字符 ‘3’ 的 ASCII 码。所以 (char)count ‘0’ 就是把数字转成对应的字符。最后的else坐标不合法如果坐标不在范围内就打印 “NOT FOUND!”然后重新打印棋盘让玩家继续输入。最后判断胜利循环结束后如果 win r * c - test说明玩家把所有非雷格子都排查完了打印 “Congratuation!”然后展示 mine 棋盘让玩家看看雷都在哪里。到这里game.c 的核心函数就全部讲完了。最后我们把 game.h 补上把用到的库函数、宏定义和函数声明都放进去#pragmaonce#includestdio.h#includestdlib.h#includetime.h#includewindows.h#includestring.h#defineROW9#defineCOL9#defineROWSROW2#defineCOLSCOL2#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