一、前言
Hello,大家好,我是star,一名C语言小白,从上一篇博客开始,0基础学习C语言,在学习了数组和函数相关内容后尝试了一下用C语言实现扫雷小游戏。下面是我对该程序的简要分析和介绍,希望能够帮到初识C语言的宝子们。如果里面的内容存在错误或不足之处,恳请大家多多纠正,你们的批评是star前进的动力。希望能和大家一起进步!!
二、扫雷游戏的分析和设计
1. 需要实现的功能
- 游戏界面的打印
- 游戏棋盘的打印
这里可以按照个人习惯输出想要的界面风格
- 数据初始化和存放
我们要把布置雷和排查雷的信息储存起来,也就是用棋盘来容纳雷和非雷的单位以及排查出来周围雷个数的信息,首先想到就是创建两个二维数组,数组的长度就是棋盘的长和宽都为9
char mine[9][9]={0};//存放布置好的雷的信息;
char show[9][9]={0};//存放周围雷的个数的信息;
- 随机布置雷
随机布置雷,这就需要给雷生成随机的横纵坐标。
那如何生成随机数呢?现在就要给大家介绍生成随机数的函数rand函数,而rand函数是对⼀个叫“种子”的基准值进行运算生成的随机数。
所以其实rand函数生成的是伪随机数,那是因为rand函数生成随机数的默认种⼦是1。如果要生成不同的随机数,就要让种子是随机变化的。
在C语言中提供一个函数srand,用来初始化随机数生成器
void srand(unsigned int seed);
通过seed参数来设置rand函数的种子。在C语言中我们一般把时间作为种子,因为时间是一直变化的,所以我们引入另一个函数time来得到时间
time_t time(time_t *timer);
当timer是NULL也就是空指针时,会返回一个时间戳
需要注意的是在使用rand函数和time函数时要包含头文件:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
srand((unsigned int)time(NULL));
int x = rand() % row + 1;//通过rand函数产生在1-9范围内雷的随机横纵坐标;
int y = rand() % col + 1;
- 排查雷
(1)如果该位置不是雷,就显示周围有几个雷
(2)如果该位置是雷就“Game Over!”
炸死后输出了mine数组来查看雷的分布情况
(3)找出所有雷则“You Win”
2.数据结构的分析
- 布置雷和排查雷的数据存储和表现形式
mine数组存放布置雷的信息,如果该位置布置有雷就放1,没有布置雷就放0;
show数组存放排查雷的信息,当我们排查某一坐标时,得到相对应的反馈信息,如果继续存放在mine数组里,就会造成信息紊乱,所以便有了show数组来存放反馈信息。反馈信息有两种,也就是说show的功能有两个,第一是统计该坐标周围一圈还有几个雷,第二就是可以在show数组对应坐标标记雷的位置。
需要注意的一点是
9x9的数组在排查坐标统计周围坐标雷的个数时会产生越界问题,例如我们在排查(9,5)这个坐标时,9已经是行数的极限,在判断和统计其周围一圈雷的个数时会造成数组下标溢出,所以我们选择将这个棋盘扩大一圈,也就是说数组创建为11x11的大小,这样就解决了越界问题。
在游戏进行时,我们要隐藏mine数组,打印show数组以得到反馈信息。
为了保持神秘我们要给show数组初始化为字符‘*’,但由于mine数组里存放的是数字,两个数组数据类型不一样,为了保持统一,将mine数组中存放字符‘0’和字符‘1’。
3.模块化程序设计
- 实现相关功能的函数创建以及调用
- 统计周围一圈雷的个数
int Get_mine_count(char mine[ROWS][COLS], int x, int y)
{
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] - 7 * '0');//‘0’- ‘0’ = 0;‘1’-‘0’=1
}
因为*‘0’-‘0’=0;‘1’-‘0’=1*
将周围一圈八个坐标的字符加起来减去8*‘0’就得到雷的个数
- 递归展开空白坐标
如何实现展开空白坐标呢?
首先我们需要实现这个函数的主体部分内容:判断该坐标周围一圈有没有雷,如果没有雷,则该坐标赋值空格‘ ’;
如果当前坐标已经排查过(!=‘*’)或者(!=‘0’),则递归结束;
void Clear(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int*win)//将win指针变量作为函数形参,需要注意的是传址调用,&win;
{
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == ' ' || show[x][y] != '*')//递归结束条件:当前坐标已经排查过或者当前坐标周围存在雷;
{
return;
}
else if (Get_mine_count(mine, x, y) != '0')
{
show[x][y] = Get_mine_count(mine, x, y);
(*win)++;
return;
}
else
{
show[x][y] = ' ';
(*win)++;
Clear(mine, show,ROW,COL, x - 1, y, win);//在已经传址的函数内部递归,这里的win就是指针,指向win的地址;,所以直接传win;
Clear(mine, show,ROW,COL, x, y - 1,win);
Clear(mine, show, ROW, COL, x+1, y ,win);
Clear(mine, show, ROW, COL, x, y+1, win);
Clear(mine, show, ROW, COL, x+1, y+1, win);
Clear(mine, show, ROW, COL, x - 1, y-1, win);
Clear(mine, show, ROW, COL, x - 1, y+1, win);
Clear(mine, show, ROW, COL, x + 1, y-1, win);
}
return;
}
}
- 标记和取消标记
因为目前我的水平还做不到可视化该程序和鼠标控制,所以只能采用逐一输入的方式来标记当前坐标,规定雷的坐标标记为‘#’,取消标记同理如下。值得一提的是,因为加了标记功能,所以我们还要添加一个模式选择的界面。
void Sign_mine(char show[ROWS][COLS], int *Mine)
{
int x = 0;
int y = 0;
printf("请输入你要标记的雷的坐标: ");
scanf("%d %d", &x, &y);
while (1)
{
if (x<1 || y<1 || x>ROW || y>COL)
{
printf("输入错误!\n");
break;
}
else
{
if (show[x][y] == '*')//标记一个雷,雷的数量就减少一个;
{
show[x][y] = '#';
(*Mine)--;
break;
}
else
{
if (show[x][y] == '#')
{
printf("该位置已经被标记过,请重新输入!");
Sleep(2000);
system("cls");
break;
}
else
{
printf("该位置已经被排查过,请重新输入!");
Sleep(2000);
system("cls");
break;
}
}
}
}
}
- 多文件形式对函数的声明和定义
mine.h//声明游戏中需要的函数和参数的数据类型
mine.c//扫雷游戏的函数主体
fun.c//定义不同功能的函数
三、扫雷游戏的代码实现
整个扫雷游戏的代码如下:
mine.h
`#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
#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 par);
//打印棋盘;
void PrintBoard(char Board[ROWS][COLS], int row, int col,int Mine);
//布置雷;
void Getmine(char Board[ROWS][COLS], int row, int col);
//统计非雷坐标周围一圈雷的个数;
int Get_mine_count(char mine[ROWS][COLS], int x, int y);
//排雷;
void Findmine(char mine[ROWS][COLS], char show[ROW][COL], int row, int col);
//拓展
//展开非雷坐标;
void Clear(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int *win);
//标记雷的坐标;
void Sign_mine(char show[ROWS][COLS], int *Mine);
//取消标记;
void Dissign_mine(char show[ROWS][COLS], int *Mine);
fun.c
#define _CRT_SECURE_NO_WARNINGS
#include"mine.h"
//初始化数组;
void InitBoard(char Board[ROWS][COLS], int rows, int clos,char par)//把初始化的字符参数上传,达到可以初始化为不同字符的目的;
{
for (int i = 0; i <ROWS; i++)
{
for (int j = 0; j <COLS; j++)
{
Board[i][j] = par;
}
}
}
//打印数组;
void PrintBoard(char Board[ROWS][COLS], int row, int col, int Mine)//这里优化了游戏界面
{
int j = 0;
int i = 0;
printf("--------扫雷--------\n");
for (i = 0; i <= row; i++)
{
if (i == 0)
printf(" ");
else
printf("%d ", i);
}
printf("\n");
for (i = 0; i <= row; i++)
{
printf("--");
if (i == 0)
printf("|");
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d |", i);
for (j = 1; j <= col; j++)
printf("%c ",Board[i][j]);
printf("\n");
}
printf("剩余雷数:%d\n", Mine);//输出剩余雷数,便于游戏进行;
}
//布置雷;
void Getmine(char Board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;//通过rand函数产生在1-9范围内雷的随机横纵坐标;
int y = rand() % col + 1;
if (Board[x][y] == '0')//10个随机坐标但循环可能不止10次,所以需要判断当前随机坐标是否已经存在雷;
{
Board[x][y] = '1';//雷为’1‘;
count--;
}
}
}
//记录该非雷坐标周围雷的个数;
int Get_mine_count(char mine[ROWS][COLS], int x, int y)
{
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] - 7 * '0');//‘0’- 0 = 0;‘1’-‘0’=1
}
//递归展开非雷坐标;
void Clear(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int*win)//将win指针变量作为函数形参,需要注意的是传址调用,&win;
{
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == ' ' || show[x][y] != '*')//递归结束条件:当前坐标已经排查过或者当前坐标周围存在雷;
{
return;
}
else if (Get_mine_count(mine, x, y) != '0')
{
show[x][y] = Get_mine_count(mine, x, y);
(*win)++;
return;
}
else
{
show[x][y] = ' ';
(*win)++;
Clear(mine, show,ROW,COL, x - 1, y, win);//在已经传址的函数内部递归,这里的win就是指针,指向win的地址;,所以直接传win;
Clear(mine, show,ROW,COL, x, y - 1,win);
Clear(mine, show, ROW, COL, x+1, y ,win);
Clear(mine, show, ROW, COL, x, y+1, win);
Clear(mine, show, ROW, COL, x+1, y+1, win);
Clear(mine, show, ROW, COL, x - 1, y-1, win);
Clear(mine, show, ROW, COL, x - 1, y+1, win);
Clear(mine, show, ROW, COL, x + 1, y-1, win);
}
return;
}
}
// 标记雷
void Sign_mine(char show[ROWS][COLS], int *Mine)
{
int x = 0;
int y = 0;
printf("请输入你要标记的雷的坐标: ");
scanf("%d %d", &x, &y);
while (1)
{
if (x<1 || y<1 || x>ROW || y>COL)
{
printf("输入错误!\n");
break;
}
else
{
if (show[x][y] == '*')//标记一个雷,雷的数量就减少一个;
{
show[x][y] = '#';
(*Mine)--;
break;
}
else
{
if (show[x][y] == '#')
{
printf("该位置已经被标记过,请重新输入!");
Sleep(2000);
system("cls");
break;
}
else
{
printf("该位置已经被排查过,请重新输入!");
Sleep(2000);
system("cls");
break;
}
}
}
}
}
//取消标记;
void Dissign_mine(char show[ROWS][COLS], int *Mine)
{
int x = 0, y = 0;
printf("请输入要取消标记的坐标:>");
scanf("%d %d", &x, &y);
while (1)
{
if (x<1 || x>ROW || y<1 || y>COL)
{
printf("输入错误!\n");
break;
}
else
{
if (show[x][y] == '#')
{
show[x][y] = '*';
(*Mine)++;
break;
}
else
{
if (show[x][y] == '*')
{
printf("该坐标没有被标记,无需取消标记!");
Sleep(2000);
system("cls");
break;
}
else
printf("该坐标已经被排查,取消标记无效!");
Sleep(2000);
system("cls");
}
}
}
}
void menu1()
{
printf(" 选择模式 \n");
printf(" 1.Find mine \n");
printf(" 2.Sign mine \n");
printf(" 3.Dissign mine \n");
printf("\n");
}
//排雷;
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x, y;
int win = 0;
int Mine = EASY_COUNT;
while (win < row * col - EASY_COUNT)//判断条件为查找次数<棋盘个数-雷的数量;
{
int choice = 0;
menu1();
printf("请选择模式:");//新增了选择模式功能,可以选择查找雷或者标记/取消标记雷的坐标;
scanf("%d", &choice);
system("cls");
switch (choice)
{
case 1:
{
PrintBoard(show, ROW, COL, Mine);
printf("请输入坐标:");
scanf("%d %d", &x, &y);
system("cls");
if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
{
if (show[x][y] == '*')
{
Clear(mine, show, ROW, COL, x, y, &win);
PrintBoard(show, ROW, COL, Mine);
printf("已排查坐标个数:%d \n", win);
if (mine[x][y] == '1')
{
printf("抱歉,您被炸死了\n");
printf("\n");
PrintBoard(mine, ROW, COL, Mine);//炸死之后输出雷的棋盘;
return;
}
}
else
{
if (show[x][y] == '#')
{
printf("该坐标已经被标记!");
Sleep(2000);
system("cls");
}
else
{
printf("该坐标已经查找过!");
Sleep(2000);
system("cls");
}
}
}
else
{
printf("您输入的坐标有误\n");
PrintBoard(show, ROW, COL, Mine);//输入坐标错误之后,输出show棋盘,重新选择坐标输入;
printf("\n");
}
break;
}
case 2:
{
PrintBoard(show, ROW, COL, Mine);
Sign_mine(show, &Mine);
break;
}
case 3:
{
PrintBoard(show, ROW, COL, Mine);
Dissign_mine(show, &Mine);
break;
}
default:
{
printf("选择错误,请重新选择!");
Sleep(2000);
system("cls");
}
}
if (win==ROW*COL-EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
PrintBoard(mine, ROW, COL, &Mine);
printf("\n");
return;
}
}
}
mine.c
#define _CRT_SECURE_NO_WARNINGS
#include"mine.h"
void Menu()
{
printf(" 扫雷游戏 ");
printf("\n");
printf(" 1.play ");
printf("\n");
printf(" 2.exit ");
printf("\n");
}
void game()
{
char mine[ROWS][COLS];//存放布置好的雷的信息;
char show[ROWS][COLS];//存放周围雷的个数的信息;
InitBoard(mine, ROWS, COLS,'0');//初始化mine数组为‘0’;
InitBoard(show, ROWS, COLS, '*');//初始化show数组为‘*’;
Getmine(mine, ROW, COL);//得到布置的随机雷的棋盘;
Findmine(mine, show, ROW, COL);//扫雷主体部分包括三个模式;
}
int main()
{
srand((unsigned int)time(NULL));
int input;
while (1)
{
Menu();
printf("请选择:");
scanf("%d", &input);
system("cls");
switch (input)
{
case 1:
game();
break;
case 2:
printf("退出游戏");
printf("\n");
break;
default:
printf("输入错误,重新选择");
printf("\n");
}
}
return 0;
}