目录
● 前言
本文章记录了如何用C语言实现了经典的小游戏——扫雷,并且尽可能地还原了自动展开以及自动标雷。如有Bug,请高抬贵手于评论区指出,感谢!
✨1、游戏规则
扫雷的详细规则是随便点开一个方格,根据展开方格的数字去推断其相邻九宫格内未展开方格下面是否是地雷,最终任务就是点开所有没有地雷的方格,以找出所有的地雷。
✨2、创建文件
我们需要创建三个文件,以便更好放置代码和修改代码,分别为:
(1)game.h //头文件,存放需要包含的其他头文件(在function和main文件只需#include<game.h>,即可包含其他的头文件),以及宏定义,函数声明
(2)function.c //存放函数的文件,包含主要的功能函数
(3)main.c // 程序的整体框架,存放主函数main
✨3、game.h 文件
🎈3.1 包含头文件
#include<stdio.h> #include<stdlib.h> #include<time.h>
🎈3.2 define定义宏
#define ROW 50 #define COL 50
先定义宏:行50,列50
🎈3.3 声明函数
void InitBoard(char board[ROW][COL], int rows, int cols, char set); void DisplayBoard(char board[ROW][COL], int row, int col); void SetMine(char board[ROW][COL], int row, int col, int minecount); void FindMine(char mine[ROW][COL], char show[ROW][COL], int row, int col,int minecount); int GetMineCount(char mine[ROW][COL], int x, int y); void AutoOpen(char mine[ROW][COL], char[ROW][COL], int x, int y,int row,int col); void AutoMark(char show[ROW][COL], int x, int y, int row, int col);
这些函数后面都会提及到
✨4、main.h 文件
🎈4.1 包含头文件
#include "game.h"
🎈4.2 main主函数
int main() { int input = 0; srand((unsigned int)time(NULL)); //随机函数rand的初始化函数srand do { //打印菜单,用menu函数封装 menu(); //输入1开始游戏;输入0退出;输入其他,报错重新输入 printf("请选择 (0/1) : > "); scanf("%d", &input); switch (input) { case 1: printf("开始游戏\n"); system("cls"); //清屏指令 game(); //游戏主体game函数 break; case 0: printf("退出游戏\n"); break; default: system("cls"); printf("输入错误,请重新输入\n"); break; } } while (input); //输入非0进入循环:1.游戏结束再次弹出菜单;2.输入错误重新输入 return 0; }
🎈4.3 game 游戏主体函数
void game() { char mine[ROW][COL] = {0}; //存放地雷数据的数组 char show[ROW][COL] = {0}; //与玩家交互的数组 int size = 0; int row = 0; int col = 0; //row,col是棋盘行列大小 int rows = 0; int cols = 0; int minecount=0; //minecount是地雷数量,rows=row+2,cols同理;+2是为了防止数组越界 printf("********************************\n"); printf("**** 1.入门 2.进阶 3.专家 ****\n"); printf("********************************\n"); printf("请选择游戏难度 (1/2/3) :> "); while (1) { scanf("%d", &size); if (size == 1) { row = 9; col = 9; rows = 11; cols = 11; minecount = 10; system("cls"); break; } else if (size == 2) { row = 16; col = 16; rows = 18; cols = 18; minecount = 40; system("cls"); break; } else if (size == 3) { row = 30; col = 30; rows = 32; cols = 32; minecount = 180; system("cls"); break; } else printf("输入错误,请重新输入 :> "); } InitBoard(mine,rows,cols,'0'); //初始化棋盘 InitBoard(show, rows, cols, '*'); DisplayBoard(show,row,col); //打印已初始化的棋盘 SetMine(mine,row,col,minecount); //设置地雷 FindMine(mine,show,row,col,minecount); //玩家排查地雷 }
♦️ row和col是棋盘行列的大小,玩家可以选择棋盘大小,即不同的游戏难度。
♦️ 为了防止数组越界,需要定义rows和cols,比row和col都大2,即rows=row+2,cols同理。
▶为什么要防止数组越界呢?
☟ 且看下图 ☟
📌 因为我们需要对周围一圈的元素进行操作
📌 当玩家选择9x9棋盘边缘的格子时,会出现数组越界的问题
📌 所以必须 为行和列扩大两格,即在原数组加上一圈的元素
🎈4.4 menu 菜单打印函数
void menu()
{
printf(" Tips: 进入全屏效果更佳哦~\n");
printf("*******************************\n");
printf("******* 1. PLAY *******\n");
printf("******* 0. EXIT *******\n");
printf("*******************************\n");
}
✨5、function.h 文件
🎈5.1 InitBoard 初始化棋盘
void InitBoard(char board[ROW][COL],int rows,int cols,char set) { int i = 0; int j = 0; for (i=0;i<rows;i++) { for (j=0;j<cols;j++) { board[i][j] = set; } } }
♦️ 字符变量set对应 ‘0’ 和 ‘*’ ,分别将mine数组和show数组初始化。
🎈5.2 DisplayBoard 打印棋盘
void DisplayBoard(char board[ROW][COL], int row, int col) { int i = 0; int j = 0; //分割线 for (j = 1; j <= col-2; j++) { printf("--"); } printf("扫雷"); for (j = 1; j <=col-2; j++) { printf("--"); } printf("\n "); //列号 for (j=1;j<=col;j++) { printf("%2d ",j); } printf("\n "); //分割线 for (j = 1; j <= col; j++) { printf("———"); } printf("\n"); for (i=1;i<=row;i++) { printf("%2d | ",i); //行号 for (j=1;j<=col;j++) { printf("%c ",board[i][j]); //打印数组元素 } printf("\n"); } //分割线 for (j = 1; j <= col-2; j++) { printf("--"); } printf("扫雷"); for (j = 1; j <= col-2; j++) { printf("--"); } printf("\n"); }
打印棋盘,效果如下:
🎈5.3 SetMine 设置地雷
void SetMine(char board[ROW][COL], int row, int col, int minecount) { int x; int y; while (minecount) { x = rand() % row + 1; //行数随机值 y = rand() % col + 1; //列数随机值 if (board[x][y] != '1') { board[x][y] = '1'; //若非雷,则放置雷 minecount--; //雷数-1 } } }
随机布置雷的效果(1是雷):
🎈5.4 GetMineCount 统计周围雷的数量
该函数将在AutoOpen函数以及FineMine函数中被调用
int GetMineCount(char mine[ROW][COL],int x,int y) { int i; int j; char sum = 0; for (i=x-1;i<=x+1;i++) { for (j=y-1;j<=y+1;j++) { sum = sum + mine[i][j]; } } return (sum - 9 * '0'); }
♦️ 参考经典扫雷的模式:当玩家排查一个格子,如果不是雷,则需要统计周围一圈的雷的数量。
♦️ 用for循环将周围一圈的mine数组的字符ASCII值都加起来(包括中间的),再减去9个字符0,得到函数返回值。
♦️ 由于雷是字符 ‘1’,ASCII值比字符 ‘0’ 大1个单位,所以得到的返回值是多少,则代表有几个雷。
🎈5.5 AutoOpen 自动展开(递归)
//自动展开,用递归实现 void AutoOpen(char mine[ROW][COL],char show[ROW][COL],int x,int y,int row,int col) { int i; int j; int count; //周围没有雷的格子,设置为空格 show[x][y] = ' '; for (i=x-1;i<=x+1;i++) { for (j=y-1;j<=y+1;j++) { if ((i >= 1 && i <= row) && (j >= 1 && j <= col) && show[i][j]=='*'&&mine[i][j]=='0') { //win是统计已排查的个数 win++; count = GetMineCount(mine, i, j); show[i][j] = count + '0'; if (show[i][j]=='0') { AutoOpen(mine, show, i, j,row,col); } } } } }
观察经典扫雷可以发现,当玩家排查一个格子时,通常会展开一大片
这是触发了自动展开的机制,那么这个机制的出现要满足什么条件?
————————————————————————————————
(1)排查的格子不是雷
(2)格子周围一圈没有雷,即show数组中该元素为字符 ‘0’ 。
(3)这个格子没被排查过,即show数组中该元素为字符 ‘*’ 。
————————————————————————————————
( 前两点很容易想到,第三点容易被忽略,一旦被忽略,函数会进入死递归。)
🛑满足上述条件,则调用AutoOpen函数。没错,正是在AutoOpen里调用自己,也就是通常说的递归。
🛑补充说明:在函数开头,将要展开的坐标的元素替换为空格
🎈 5.6 AutoMark 自动标记地雷
void AutoMark(char show[ROW][COL],int x,int y,int row,int col) { int i; int j; char tagcount = '0'; char tag[ROW][COL] = { 0 }; int m; int n; for (i=1;i<=row;i++) { tagcount = '0'; for (j=1;j<=col;j++) { tagcount = '0'; if (show[i][j]!='*'&&show[i][j]!=' ') { for (m = i - 1; m <= i + 1; m++) { for (n = j - 1; n <= j + 1; n++) { if ((m>=1&&m<=row)&&(n>=1&&n<col) && (show[m][n]=='*'||show[m][n]=='!')) { tagcount++; } } } if (show[i][j]==tagcount) { for (m = i - 1; m <= i + 1; m++) { for (n = j - 1; n <= j + 1; n++) { if ((m >= 1 && m <= row) && (n >= 1 && n < col) && (show[m][n]=='*' || show[m][n] == '!')) { show[m][n]='!'; } } } } } } } }
![]()
通过观察经典扫雷,不难发现自动标雷的触发条件:
🧭 周围一圈未排查的格子的个数 等于 中间格子显示雷的个数
🎈5.7 FindMine 玩家排查雷(判断输赢)
void FindMine(char mine[ROW][COL],char show[ROW][COL],int row,int col,int minecount) { int x = 0; int y = 0; int i = 0; int j = 0; printf("请输入要排查的坐标 (!表示被标记的雷) :> "); while (win<row*col-minecount) { scanf("%d %d",&x,&y); system("cls"); if ((x>=1 && x<=row )&& (y>=1 && y<=col) && show[x][y]=='*') { //第一次排雷必定不是雷。如果是雷,重新放置一个雷 if (mine[x][y] == '1'&& win==0) { i = 1; int x2; int y2; while (i) { x2 = rand() % row + 1; y2 = rand() % col + 1; if (mine[x2][y2] != '1') { mine[x2][y2] = '1'; i--; } } mine[x][y] = '0'; } //如果是雷,提示被炸死,游戏结束 if (mine[x][y]=='1') { printf("很遗憾,你被炸死了,游戏结束\n( P 为地雷)\n"); for (i = 1; i <= row; i++) { for (j=1;j<=col;j++) { if (mine[i][j]=='1') { show[i][j] = 'P'; } } } DisplayBoard(show,row,col); break; } else { //win是统计已排查的个数,到达指定数量,游戏结束 win++; int count = GetMineCount(mine,x,y); show[x][y] =count+'0'; if (show[x][y]=='0') { AutoOpen(mine,show,x,y,row,col); } AutoMark(show,x,y,row,col); DisplayBoard(show, row, col); printf("请输入要排查的坐标 (!表示被标记的雷) :> "); } } else if ((x >= 1 && x <= row) && (y >= 1 && y <= col) && show[x][y] != '*') { DisplayBoard(show, row, col); printf("此坐标已排查,请重新输入 (!表示被标记的雷) :>"); } else { DisplayBoard(show, row, col); printf("坐标不合理,请重新输入 (!表示被标记的雷) :> "); } } //判断游戏胜利 if (win==row*col-minecount) { printf("\n恭喜你,成功排出全部雷!\n( P 为地雷)\n"); for (i = 1; i <= row; i++) { for (j = 1; j <= col; j++) { if (mine[i][j] == '1') { show[i][j] = 'P'; } } } DisplayBoard(show, row, col); } }
📀 这个函数包含:玩家输入坐标、判断玩家踩雷、判断玩家排雷成功 以及 玩家首次排雷保护(首次必定不是雷)
📀 玩家排查坐标且未踩雷,则调用自动展开函数;自动标雷在自动展开之后,在DisplayBoard之前被调用。
————————————————
感谢阅读,欢迎来讨论相关内容~
————————————————