扫雷小游戏
《扫雷》是一款大众类的益智小游戏。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
这里给大家提供一个在线的扫雷游戏,感兴趣的可以去体验一下。
http://www.minesweeper.cn/
今天我们使用C语言配合打印控制台去实现一个简单的扫雷小游戏。
一、游戏设计思路
1.玩家开始游戏时,给玩家一个开始游戏和结束游戏的提示菜单。用户输入1开始游戏,输入0结束游戏退出程序。
2.当玩家选择1时开始游戏,屏幕上显示一个9*9的棋盘,因为后续要实现标记地雷的功能,所以提示玩家“请输入要排查的坐标(如:2,2排查坐标 2#2标记坐标):”,玩家输入“数字,数字”是排查地雷,输入“数字#数字”是标记地雷。
3.当玩家输入一个正确的坐标时,如果这个位置没有雷,就把周围所有没有雷的位置显示出来。
4.当玩家排查到雷时,提示游戏结束,当棋盘上只有雷没有显示出来的时候,提示游戏胜利。
二、具体实现
2.1显示游戏菜单
创建一个test.c的文件,在main函数里调用test()函数,test()是整个程序功能实现的开始。首先打印一个菜单目录,提示用户输入1或者0,如果输入不合法,提示用户重新输入,这里我们可以使用do…while()循环去实现。代码示例:
#include "game.h"
void menu() {
printf("*********************\n");
printf("*******1. play*******\n");
printf("*******0. exit*******\n");
printf("*********************\n");
}
void test() {
int input = 0;
do {
menu();//打印目录
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case 1://用户输入1开始游戏
game();
break;
case 0://用户输入0结束游戏
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
}
int main() {
test();
return 0;
}
menu函数功能就是实现打印菜单的功能。在test函数中,如果用户输入1,则执行game()函数,开始游戏,输入0结束游戏。输入其他打印“选择错误,请重新选择!”。
2.2 game()函数具体实现
首先我们创建一个game.h的头文件,里面我们存放一些静态常量,声明的函数等。再创建一个game.c的源文件,里面实现具体的函数功能。
先看一下game.h头文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//声明函数
void InitBoard(char arr[ROWS][COLS], int rows, int cols,char set);
//打印棋盘
void DisplayBoard(char arr[ROWS][COLS],int row,int col);
//布置雷
void SetMine(char arr[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
设计一个9*9的棋盘,里面有10个雷,我们可以用宏定义ROW,COL,EASY_COUNT 去提前定义。ROWS和COLS常量是方便我们去定义数组,实现具体功能而存在的。
然后我们去实现game函数:
void game() {
char mine[ROWS][COLS];//数组初始化为‘0’
char show[ROWS][COLS];//数组初始化为‘*’
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//布置雷
//在9*9的棋盘上随机布置10个雷
SetMine(mine,ROW,COL);
DisplayBoard(show, ROW, COL);//打印棋盘
//排查雷
FindMine(mine, show, ROW, COL);
}
我们将这三个函数实现在game.c源文件中,在这里我们去调用。介绍一下这三个函数:
1.首先是调用InitBoard()函数初始化了两个数组,mine和show,mine初始化为‘0’,show初始化为’*'。
mine数组用来设计雷的位置,没有雷显示‘0’,有雷的位置显示‘1’。
show数组是用来显示给玩家看的,玩家看到的是全是*的棋盘,排查后才会显示。
void InitBoard(char arr[ROWS][COLS],int rows, int cols,char set) {//对于给定的数组,将里面的元素全部初始化为传递的set值
int i,j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
2.初始化完成之后,我们需要在mine棋盘上布置雷,调用SetMine()函数:
void SetMine(char arr[ROWS][COLS], int row, int col)
{
srand((unsigned int)time(NULL));
int count = EASY_COUNT;//EASY_COUNT=10
while (count)
{
//在数组中随即设置10个雷
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
以当前时间作为参数的随机种子,确保每次产生的雷的位置都不一样。
3.我们需要在屏幕上显示当前棋盘给玩家看,也就是显示show棋盘,调用DisplayBoard()函数:
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
int i = 0;
printf("------扫雷游戏-------\n");
//打印列号
for (i = 0; i <= row; i++)
{
printf("%2d ", i);
}
printf("\n");
printf("———————————————\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%2d|", i);
for (j = 1; j <= col; j++)
{
printf("%2c ", arr[i][j]);
}
printf("\n");
}
printf("———————————————\n");
}
细心的读者可能已经发现,我们调用SetMine和DisplayBoard函数的时候,后面传递的参数是ROW和COL,这是为什么呢?
因为我们定义数组时使用的是ROWS、COLS,我们前面定义了ROWS和COLS的值是11,但当我们布置雷的时候,是希望他布置在下表为1-9之间的,因为我们显示在玩家面前的棋盘就是1-9,超出棋盘范围就没意义了,DisplayBoard也是同理,显示的是1-9。至于为什么要多余设置ROW和ROWS,我们下面会讲到。
2.2 FindMine()函数具体实现
FindMine()函数用于玩家去排查雷,玩家输入一个坐标,如果不是雷就继续,是雷就结束游戏,直到所有雷排出。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
char z = ',';
//定义一个对应show数组的视野全开的数组,以便后面进行0雷扩展
char all[ROWS][COLS];
int sz = sizeof(mine) / sizeof(mine[0]);
for (int i = 1; i < ROWS; i++) {
for (int j = 1; j < COLS; j++) {
int count = GetMineCount(mine, i, j);
all[i][j] = count + '0';
}
}
while (1) {
int win = 0;
printf("请输入要排查的坐标(如:2,2排查坐标 2#2标记坐标):");
scanf("%d %c %d", &x,&z,&y);
//判断坐标的有效性
if (x >= 1 && x <= row && y >= 1 && y <= col && z == ',') {
//判断是不是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else {//不是雷
int count = GetMineCount(mine,x,y);
//递归扩展
ZeroExpend(show,all,x,y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
}
}else if (x >= 1 && x <= row && y >= 1 && y <= col && z == '#') {
show[x][y] = '#';
DisplayBoard(show, ROW, COL);
}else {
printf("坐标非法,请重新输入\n");
}
//胜利条件
//如果show中已经显示的数为总个数-雷的个数,证明此时show中只剩下雷,那么游戏胜利
for (int i = 1; i <= ROW; i++) {
for (int j = 1; j <= COL; j++) {
if (show[i][j] != '*'&& show[i][j] != '#') {
win++;
}
}
}
if (win == ROW*COL-EASY_COUNT)
{
printf("恭喜你游戏胜利!\n");
DisplayBoard(show, ROW, COL);
break;
}
}
}
首先我们可以创建一个对应于show的视野全开数组,可以看成是开了透视的外挂。该数组用于后面我们实现坐标是0向四周扩展的功能。
然后进入while循环,输入三个值xyz,然后进行判断:
如果是"数字,数字"的形式,就进入排查雷的过程,否则再去判断是否是"数字#数字"的形式,如果是就进行标记雷的过程。如果都不是,那么就提示输入的坐标有误重新输入。
然后我们再来看一下具体排查雷是怎么写的:
我们拿到用户输入的xy值,进入mine数组中判断这个位置是不是雷,如果是雷游戏结束,如果不是雷,计算这个坐标位置的周围有几个雷。我们调用GetMineCount()方法去计算,返回周围雷的个数。
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int count = 0;
int sum = 0;
for (int i = x - 1; i <= x + 1; i++) {//获取该坐标周围的8个坐标的值,累加起来
for (int j = y - 1; j <= y + 1; j++) {
sum += mine[i][j];
}
}
count = sum - 9 * '0';//ASCII码值相减,9个坐标减去9*'0'得到确切个数
return count;
}
注意:
如果我们将数组的个数设置为ROW,如果这里的坐标刚好是边缘坐标,当我们进行遍历的时候会出现数组下标越界的问题,我们再在遍历过程中去判断是否是边界元素又会使代码变得繁琐复杂。
所以我们在定义的时候,直接将数组扩大一圈,也就是给ROW和COL加上2定义为ROWS和COLS,使原本的9×9变成11×11,并且在初始化的时候全部赋值为0,但是在布置雷的时候也不会布置在最外一层,这样在进行统计周围雷个数的时候,既不会出现数组下标越界,也不会使代码变得复杂冗余。这解释了前面我们提到过的SetMine和DisplayBoard函数的传值问题,也解释了为什么在定义mine和show数组的时候,使用ROWS和COLS,而不是ROW和COL了。
统计完这个坐标的个数后,我们接着调用ZeroExpend()函数进行是0扩展:
void ZeroExpend(char show[ROWS][COLS], char all[ROWS][COLS], int x, int y)
{
//使用函数递归,如果这个位置是‘0’,并且在show中没有被显示过(*号),就把这个位置点开
int i, j;
if (all[x][y] == '0' && show[x][y] == '*') {
show[x][y] = all[x][y];
//向四周扩展
for (i = x - 1; i <= x + 1 && x >= 0 && x <= ROW; i++) {
for (j = y - 1; j <= y + 1 && y >= 0 && y <= COL; j++) {
ZeroExpend(show, all, i, j);
}
}
}
else {//直到这个值不是‘0’
show[x][y] = all[x][y];
}
}
函数传递了两个数组show和all,我们把这两个数组对比查看,如果玩家输入的坐标位置在all中是’0’,并且在show中是’*',表示该位置是0并且没有被显示出来,我们将这个位置显示出来,并且继续向四周扩展显示,我们采用递归的方法去实现。
直到这个位置已经被显示出来了,或者说这个位置不为‘0’,我们停止递归。此时我们就完成了显示坐标和向四周扩展的功能。
2.3 结束游戏
我们玩这个游戏的过程中发现,只有我们排查到雷的时候游戏才会结束,否则将一直持续下去。所以我们需要一个游戏胜利的条件。
当棋盘上的非雷坐标全部显示出来了的时候,我们结束游戏,显示游戏胜利。
//胜利条件
//如果show中已经显示的数为总个数-雷的个数,证明此时show中只剩下雷,那么游戏胜利
while (1) {
int win = 0;
printf("请输入要排查的坐标(如:2,2排查坐标 2#2标记坐标):");
...//省略
//胜利条件
//如果show中已经显示的数为总个数-雷的个数,证明此时show中只剩下雷,那么游戏胜利
for (int i = 1; i <= ROW; i++) {
for (int j = 1; j <= COL; j++) {
if (show[i][j] != '*'&& show[i][j] != '#') {
win++;
}
}
}
if (win == ROW*COL-EASY_COUNT)
{
printf("恭喜你游戏胜利!\n");
DisplayBoard(show, ROW, COL);
break;
}
}
在while循环的开始定义一个win,每次玩家选择完坐标结束后,去遍历show中已经显示的个数,当win的个数为棋盘总数-雷个数时,证明场上只剩下雷,游戏胜利结束游戏。
总结
扫雷游戏中还有模式的选择,自定义棋盘大小功能,用到了可变数组的方面,这就涉及了指针内容。关于指针当面,我们后续再进行分享。
好了,以上就是关于C语言自己制作扫雷小游戏的全部过程,有什么问题或者文章哪里写的有不对的地方,欢迎评论区或私信告诉我。如果这篇文章对你有帮助的话请点赞收藏关注一下,感谢大家的支持~🌹🌹