1.扫雷游戏介绍
如图所示,在扫雷游戏中,玩家需要通过排查雷来找出不是雷的位置.
其中单元格内显示的数字是该单元格周围8个位置的雷的数量(即以该单元格为中心的一个九宫格再去掉该单元格).
如果该数字为0,则展开一片区域直到遇到一圈不为零的数字
2.运行环境的配置
在这里为什么要分为3个文件呢?
这是因为在一个具体的程序设计中,我们遵循着分而治之,变则疏之的思想,将一个复杂的游戏功能和具体调用的实现具体分为三个文件去实现。
每个文件只需要做好自己该做的任务即可,体现了高内聚的设计思想
通过头文件的包含来为每个文件建立起联系,每个文件的修改不会引起其他文件的改动(即:不会牵一发而动全身),体现了低耦合的设计思想
3.各种函数功能的实现
3.1创建游戏菜单界面
test.c
void menu() { printf("**************************\n"); printf("****** 1.play ******\n"); printf("****** 0.exit ******\n"); printf("**************************\n"); }
玩家选择1进行游戏,选择0退出游戏
int main() { int input = 0; srand((unsigned int)time(NULL));//布置雷的时候需要用到这个函数来生成随机数生成器 do { printf("请选择:>"); menu(); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("退出游戏\n"); break; default: printf("输入有误,请重新输入\n"); break; } } while (input); return 0; }
3.2 初始化棋盘
在这里,我们考虑要用’1’表示雷,'0’表示非雷.
这样设置的好处是我们在后面进行计算九宫格内其他8个位置的雷的个数时可以用其他8个位置的数据相加再减去8个’0’.
注意:
> 字符型数字’0’+数字型数字1或2…==字符型数字’1’或’2’…**
所以这样做就会导致玩家在排查一个单元格后出现的数字会和雷本身产生歧义(因为雷表示为’1’,排查雷出现的数字可以为1)
所以在这里我们构建两个数组,一个为show数组,负责表示玩家排查雷的信息(即玩家看到的棋盘),另一个为mine数组,负责表示存放雷信息的数组,又因为我们要计算九宫格内的雷的个数,所以我们采用11*11的棋盘,而不是玩家看到的9*9的棋盘去存放数据.
我们在这里定义:
1.mine数组和show数组均定义为char类型的数组
2.show数组中未排查单元格定义为’*',已排查单元格为数字或空格
3.在game.h中采用宏定义常量的方式去定义:
#define ROW 9 //玩家看到的棋盘大小 #define COL 9 #define ROWS ROW+2 //实际棋盘大小 #define COLS COL+2 #define EASY_COUNT 10 //雷的个数,方便玩家调整难度系数(雷的个数和棋盘的大小)
注意:InitBoard这个函数用来初始化棋盘的函数,而棋盘本身大小就是11*11,所以我们在第二个和第三个形参中传入ROWS和COLS
game.h
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
game.c
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) { int i = 0; for (i = 0; i < rows; i++) { int j = 0; for (j = 0; j < cols; j++) { //board[i][j]='*'; //board[i][j]='0'; //在这里,我们为了程序的通用性而设置了set这个参数,封装为一个参数,简化代码 board[i][j] = set; } } }
3.3 打印棋盘
game.h
> 注意:打印棋盘是给玩家看的,所以要打印9*9的棋盘,即11*11的棋盘"去掉"第一行,最后一行,第一列跟最后一列后形成的棋盘.所以传入ROW和COL
void DisplayBoard(char board[ROWS][COLS], int row, int col);
game.c
void DisplayBoard(char board[ROWS][COLS], int row, int col) { //在这里我们添加行号和列号来使这个棋盘结构更加清晰 int i = 0; for (i = 0; i <= row; i++) { printf("%d ", i);//列号 } printf("\n"); for (i = 1; i <= row; i++)//**去掉第一行和最后一行** { printf("%d ", i);//行号 int j = 0; for (j = 1; j <= col; j++)//**去掉第一列和最后一列** { printf("%c ", board[i][j]); } printf("\n"); } printf("\n");//在这里加个换行符,防止mine和show棋盘连续打印时结构过于紧凑 }
3.4 布置雷
game.h
注意:因为雷只放在9*9的棋盘中,所以传入ROW和COL
void MineSet(char board[ROWS][COLS], int row, int col);
game.c
在test.c的main函数中使用srand和time函数生成随机数生成器,因为这个随机数生成器只需要调用一次即可,而main函数在整个程序中只进行一次,所以将它放在main函数中即可
srand((unsigned int)time(NULL));
void MineSet(char board[ROWS][COLS], int row, int col) { //要用随机数函数生成随机坐标布置到棋盘当中 int x = 0; int y = 0; int count = EASY_COUNT; while (count) { x = rand() % row + 1;//产生1~row的随机数 y = rand() % col + 1;//产生1~col的随机数 if (board[x][y] == '0') { board[x][y] = '1'; count--;//布置成功后count-- } } }
3.5 排查雷
注意:这里要传入两个数组跟ROW和COL,因为我们是要根据mine棋盘中存放的关于雷的信息来改变show棋盘的值,
至于传入ROW和COL的原因是:
> 1.每次排查后都要打印show数组,需要传入ROW和COL
2.我们要根据排查次数win和整个9*9棋盘中非雷单元格的个数(ROW*COL-EASY_COUNT)的相对大小来判断玩家是否胜利
3.我们要通过ROW和COL来判断玩家输入的坐标是否非法(即:越界)来保证用户正确输入在该9*9棋盘中的坐标4.大家可以在下面的代码中好好体会这三点原因
game.h
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0;//记录排查过且不是雷的单元格的个数 while (win < row * col - EASY_COUNT) { printf("请输入您认为是雷的x坐标和y坐标,中间用空格隔开:\n"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否非法 { if (mine[x][y] == '1') { printf("很遗憾,您被炸死了!\n"); DisplayBoard(mine, row, col);//打印mine棋盘,告诉玩家,他/她踩到雷了 break; } else { expand(mine, show, x, y, &win);//展开函数(通过递归) system("cls");//清屏 DisplayBoard(show, row, col); } } else { printf("请输入的坐标非法,请重新输入!\n"); } } if (win == row * col - EASY_COUNT) { system("cls"); printf("恭喜您,排雷成功!\n"); DisplayBoard(mine, row, col);//打印mine棋盘,告诉玩家,让他享受胜利的喜悦 } }
3.6 递归展开函数
3.5中我们用到了expand这个函数,下面我们来看一下这个函数的具体实现
注意: 1.这里我们用到了int* win这个指针,因为在函数传参时
2.参数如果为值传递,则实际上是在函数栈区为形参开辟了一块空间对实参进行临时的值拷贝,改变形参并不会改变实参的值,
3.而参数如果为地址传递,则可以修改实参的值,所以我们在3.5的FindMine函数中传入win的地址:
expand(mine, show, x, y, &win);//展开
在expand函数中用int*win来接收
//递归展开 void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL)//每次检查x,y坐标的合理性,防止递归时发生错误 { int count = Show_mine_num(mine, x, y); if (count == 0)//四周没雷,进入递归展开 { show[x][y] = ' '; int i = 0; for (i = x-1; i <= x + 1; i++) { int j = 0; for (j = y - 1; j <= y + 1; j++) { //注意:在这里只对'*'位置进行展开,防止死递归 if (show[i][j] == '*') { expand(mine, show, i, j, win); } } } } else { show[x][y] = count+'0';//四周至少有一个雷,将数字转为字符数字只需加上'0'即可 } (*win)++; } }
在这里我们说明一下(*win)++
这行代码的作用:
根据expand这个函数的代码我们可以发现:每次调用expand这个函数时分为两种情况
1.该坐标周围有雷,即
count!=0
,那么会执行一次(*win)++
.同时去掉一个’*‘标记2.该坐标周围没有雷,即
count==0
,那么会进入递归状态,而每递归一次都会执行一次*(win)++
,同时减少一个’*'标记3.由递归的思想和定义来看,递归一定会有结束的时刻,当递归的最后一层调用时势必会使
count!=0
,即执行else语句,当递归的最后一层结束时,会返回到上一层的*(win)++
位置,所以说每调用一次expand函数,win的值就会加1
3.7 计算九宫格雷个数的函数
game.c
注意
字符型数字’0’+数字型数字1或2…==字符型数字’1’或’2’…
int Show_mine_num(char mine[ROWS][COLS], int x, int y) { return (mine[x - 1][y] + mine[x + 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] + mine[x][y + 1] + mine[x + 1][y + 1] - 8 * '0'); }
4 整体程序的实现
4.1 test.c
#include "game.h" void menu() { printf("**************************\n"); printf("****** 1.play ******\n"); printf("****** 0.exit ******\n"); printf("**************************\n"); } void game() { //1.打印棋盘,这里我们采用构造两个数组,玩家看到的棋盘是9*9, //但是我们在这里先构造11*11的两个棋盘, //一个存放雷的信息,('0':非雷,'1':雷) //一个存放玩家看到的棋盘信息(即排查出的雷的信息)('*':未排查点,'数字':显示周围8个坐标中有几个雷) //存放雷的棋盘 char mine[ROWS][COLS] = { 0 }; //存放玩家看到的棋盘(即排查雷的棋盘) char show[ROWS][COLS] = { 0 }; //初始化棋盘(11*11) InitBoard(mine, ROWS, COLS,'0');// InitBoard(show, ROWS, COLS,'*');// //至于这里为什么将两者都设置为字符型数组? //首先:因为我们要让show数组打印出'*',所以show数组的类型为char型 //又因为我们想让两个数组均使用相同的函数,所以将mine数组也设置为char类型 DisplayBoard(mine, ROW, COL);//注意:打印是要打印出9*9的棋盘给玩家看!!!!!! DisplayBoard(show, ROW, COL); //布置雷(9*9) MineSet(mine, ROW, COL); //DisplayBoard(mine, ROW, COL);//注意,这一行是测试用的代码,玩家进行游戏时,这行必须要注释掉 //排查雷(9*9) FindMine(mine, show, ROW, COL); } int main() { int input = 0; srand((unsigned int)time(NULL)); do { printf("请选择:>"); menu(); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("退出游戏\n"); break; default: printf("输入有误,请重新输入\n"); break; } } while (input); return 0; }
4.2 game.h
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> //rand函数和srand函数的头文件 #include <time.h> //time函数的头文件 #pragma once #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 #define EASY_COUNT 10 //这里采用宏定义常量方式,便于以后进行难度修改(即修改雷的个数和棋盘的大小) void InitBoard(char board[ROWS][COLS], int rows, int cols, char set); void DisplayBoard(char board[ROWS][COLS], int row, int col); void MineSet(char board[ROWS][COLS], int row, int col); void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
4.3 game.c
#include "game.h" void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) { int i = 0; for (i = 0; i < rows; i++) { int j = 0; for (j = 0; j < cols; j++) { //board[i][j]='*'; //board[i][j]='0'; //在这里,我们为了程序的通用性而设置了set这个参数,封装为一个函数,简化代码 board[i][j] = set; } } } void DisplayBoard(char board[ROWS][COLS], int row, int col) { //在这里我们添加行号和列号来使这个棋盘结构更加清晰 int i = 0; for (i = 0; i <= row; i++) { printf("%d ", i);//列号 } printf("\n"); for (i = 1; i <= row; i++) { printf("%d ", i);//行号 int j = 0; for (j = 1; j <= col; j++) { printf("%c ", board[i][j]); } printf("\n"); } printf("\n");//在这里加个换行符,防止mine和show棋盘连续打印时结构过于紧凑 } void MineSet(char board[ROWS][COLS], int row, int col) { //要用随机数函数生成随机坐标布置到棋盘当中 int x = 0; int y = 0; int count = EASY_COUNT; while (count) { x = rand() % row + 1;//产生1~row的随机数 y = rand() % col + 1;//产生1~col的随机数 if (board[x][y] == '0') { board[x][y] = '1'; count--;//布置成功后count-- } } } int Show_mine_num(char mine[ROWS][COLS], int x, int y) { return (mine[x - 1][y] + mine[x + 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] + mine[x][y + 1] + mine[x + 1][y + 1] - 8 * '0'); } //递归展开 void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win) { if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { int count = Show_mine_num(mine, x, y); if (count == 0)//四周没雷,进入递归展开 { show[x][y] = ' '; int i = 0; for (i = x-1; i <= x + 1; i++) { int j = 0; for (j = y - 1; j <= y + 1; j++) { //注意:在这里只对'*'位置进行展开,防止死递归 if (show[i][j] == '*') { expand(mine, show, i, j, win); } } } } else { show[x][y] = count+'0';//四周至少有一个雷,将数字转为字符数字 } (*win)++; } } void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0; while (win < row * col - EASY_COUNT) { printf("请输入您认为是雷的x坐标和y坐标,中间用空格隔开:\n"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否非法 { if (mine[x][y] == '1') { printf("很遗憾,您被炸死了!\n"); DisplayBoard(mine, row, col);//打印mine棋盘,告诉玩家,他/她踩到雷了 break; } else { expand(mine, show, x, y, &win);//展开 system("cls");//清屏 DisplayBoard(show, row, col); } } else { printf("请输入的坐标非法,请重新输入!\n"); } } if (win == row * col - EASY_COUNT) { system("cls"); printf("恭喜您,排雷成功!\n"); DisplayBoard(mine, row, col);//打印mine棋盘,告诉玩家,让他享受胜利的喜悦 } }
以上就是C语言实现扫雷小游戏的全部代码,感谢大家看到最后,希望能给大家带来帮助和收获