游戏思路概要
扫雷游戏是一款基于逻辑推理的益智游戏。玩家需要使用推理和猜测的能力,在未知区域的地雷位置周围进行标记,并避免触发地雷。
-
游戏初始化:
- 创建一个游戏区域,被分成网格状的方块。
- 在方块中随机布置地雷。
-
点击方块:
- 玩家根据自己的判断和推理,选择一个未翻开的方块进行点击。
- 如果点击的方块是地雷,游戏结束,玩家失败。
- 如果点击的方块周围有地雷,显示该方块周围地雷的数量。
- 如果点击的方块周围没有地雷,递归地翻开相邻的空白方块。
-
游戏结束条件:
- 翻开所有不带地雷的方块,游戏获胜。
- 点击到地雷,游戏失败。
代码思路
- 建立一个头文件“game.h”用来封装所需要使用的头文件以及函数的声明。
- 建立一个源文件“game.c”用来对头文件中需要使用的函数进行定义。
- 建立另一个源文件“PlayGame.c”实现扫雷游戏。
图片展示:
函数介绍:
srand() 和 rand() 是C语言中用于生成随机数的标准库函数,它们声明在 stdlib.h 头文件中。
1、srand()
srand() 函数用于设置随机数生成器的种子。使用 srand() 函数可以改变随机数生成器的起始点,从而影响后续调用 rand() 生成的随机数序列。
函数原型如下:
void srand(unsigned int seed);
参数 seed 是一个无符号整数值,用作随机数生成器的种子。常用的种子值有 time(NULL),它使用当前时间作为种子,因此每次程序运行都会得到不同的随机数序列。
2、time()
time() 是C语言中的一个标准库函数,声明在 time.h 头文件中。它用于获取当前的系统时间并返回一个 time_t 类型的值。
函数原型如下:
time_t time(time_t* timer);
参数 timer 是一个可选的指向 time_t 对象的指针,用于接收时间值。如果传入 NULL,则 time() 函数会仅返回时间值,而不会将其存储到指定的位置。
返回值是一个表示当前时间的 time_t 类型的值,通常是一个整数,表示自某个特定时间(通常是系统特定的起始时间点)到调用 time() 时经过的秒数。
使用 time() 函数可以实现获取当前时间的功能。
3、rand()
rand() 函数用于生成一个随机数。
函数原型如下:
int rand(void);
rand() 函数返回一个在范围 [0, RAND_MAX] 内的伪随机整数。RAND_MAX 是一个常量,表示随机数的最大可能值,通常是一个较大的整数。
若要生成一个指定范围内的随机数,可以使用取余运算符 % 对 rand() 的返回值进行处理,例如:rand() % n 表示生成一个范围在 [0, n-1] 的随机整数。
由于rand()函数生成的随机值具有局限性,且只能生成一次随机数,所以我们在主函数中调用srand()函数,以及使用time(NULL)时刻改变其参数,使其程序调用rand()函数都可以生成不同的伪随机数,极大的确保了电脑下棋的随机性。
我们列举一个简单的生成随机数的例子:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
int i;
srand((unsighed int)time(NULL)); // 设置种子为当前时间
for (i = 0; i < 5; i++) {
printf("%d\n", rand()); // 生成随机数并打印
}
return 0;
}
**** 由于time(NULL)的返回值为time_t类型,通常是一个整数。由于游戏中所需的随机数类型为正整数,所以我们将time(NULL)的返回值用(unsigned int)强制转换为“无符号整型”,以确保电脑下棋的可行性。
注意:
在这个程序中,我们需要使用到两个数组mine[rows][cols]和show[rows][cols]。本次我们实现9x9的扫雷棋盘。见下图:
所以我们在程序中进行如下定义。
#define row 9
#define col 9
#define rows row+2
#define cols col+2
既然是实现9x9的棋盘,我们为什么要建立11x11大小的数组呢?我们知道,“如果点击的方块周围有地雷,显示该方块周围地雷的数量”。这就要求我们要对输入坐标周围的8个位置进行遍历,判断一共有几个地雷。但当需要判断的坐标位于边界时,我们下面这段判断地雷个数的函数会越界。
void get_mine_count(char mine[rows][cols],char show[rows][cols], int a, int b)
{
int i, j, sum = 0;
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
for (i = a - 1; i <= a + 1; i++)
{
for (j = b - 1; j <= b + 1; j++)
{
sum += mine[i][j] - '0';
}
}
}
}
所以我们建立一个11x11大小的棋盘并且将棋盘最外圈不放地雷,并且遍历的范围只在棋盘内部9x9大小的棋盘上。这样既能保证数组不会越界,也能准确的判断出坐标周围地雷的个数。
那么我们为什么要使用两个数组呢?
在扫雷游戏的实现中,可使用两个数组来表示游戏区域,一个数组用于存储地雷的位置信息,另一个数组用于存储每个方块的状态和周围地雷的数量。
以下是使用两个数组的原因:
-
地雷数组【mine[rows][cols]】:
- 地雷数组用于记录地雷的位置。
- 在游戏开始时,通过随机生成的方式将地雷的位置分布在游戏区域中。
- 该数组的元素用来表示方块是否为地雷,例如 1 表示地雷,0 表示非地雷。
-
状态数组【show[rows][cols]】:此数组用于将棋盘状态信息打印于屏幕上
- 状态数组用于记录每个方块的状态,如方块是否翻开、是否为地雷、周围地雷的数量等。
- 这个数组的元素可以是枚举类型,表示不同的状态和信息。
通过使用两个数组,程序能够有效地将地雷的位置和方块的状态进行分离存储。这样,在玩家进行点击或标记操作时,程序可以根据状态数组进行判断和更新,而无需直接读取和操作地雷数组。这样的设计可以简化游戏逻辑实现,并提高运行效率。
棋盘展示:
show[rows][cols]实现:
mine[rows][cols]数组实现: 1 表示地雷,0 表示非地雷
整体流程
1、头文件:game.h
#pragma once
#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 count 10
void meau();//菜单
void game();
void init(char board[rows][cols],char s);//初始化扫雷棋盘
void display(char board[rows][cols]);//打印棋盘
void setmine(char mine[rows][cols]);//设置“地雷”
void findmine(char mine[rows][cols], char show[rows][cols]);//寻找地雷
void get_mine_count(char mine[rows][cols], int a, int b);//获取环绕(a,b)坐标周围8个坐标内“地雷”的个数。
int is_win(char show[rows][cols]);//判断棋盘上剩余地雷数量
2、源文件:game.c
#include"game.h"//game.h 此头文件为个人创建,引用时需加双引号
//菜单,为玩家提供是否进入游戏的选择
void meau()
{
printf("****************************\n");
printf("****** 1. play *******\n");
printf("****** 0. exit *******\n");
printf("****************************\n");
}
//判断棋盘上剩余地雷数量,并返回地雷的数量
int is_win(char show[rows][cols])
{
int i, j, key = 0;
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] == '*')
{
key++;
}
}
}
return key;
}
//初始化地雷棋盘
void init(char board[rows][cols],char s)
{
int i, j;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = s;
}
}
}
//打印棋盘
void display(char board[rows][cols])
{
int i, j;
for (i = 0; i <= row; i++)
{
if (i == 0)
printf(" ");
else
printf("%-2d",i);
}
printf("\n");
for (i = 0; i <= col; i++)
{
if (i == 0)
printf(" ");
else
printf("__");
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%-3d|",i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//在mine数组中设置count个地雷
void setmine(char mine[rows][cols])
{
int a, b, c = count;
while (c>0)
{
a = rand() % row + 1;
b = rand() % col + 1;
if (mine[a][b] == '0')
{
mine[a][b] = '1';
c--;
}
}
}
//获取环绕(a,b)坐标周围8个坐标内“地雷”的个数。
void get_mine_count(char mine[rows][cols],char show[rows][cols], int a, int b)
{
int i, j, sum = 0;
//限制变量不越界
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
for (i = a - 1; i <= a + 1; i++)
{
for (j = b - 1; j <= b + 1; j++)
{
sum += mine[i][j] - '0';
}
}
}
show[a][b] = '0' + sum;
//当环绕(a,b)坐标周围8个坐标内“地雷”的个数为0时,(a,b)对应的位置为‘0’.
//将满足上述条件的坐标位置置空
if (show[a][b] == '0')
{
show[a][b] = ' ';
}
/*
当(a,b)坐标为'0'时,进行递归,判断其周围坐标是否满足【环绕(a,b)坐标周围8个坐标内“地雷”的个数为0】。
*/
if (sum == 0 && a >= 1 && a <= row && b >= 1 && b <= col)
{
for (i = a - 1; i <= a + 1; i++)
{
for (j = b - 1; j <= b + 1; j++)
{
//if()如果不添加此限制条件,将进入死递归。因为已经置空的坐标无需再进行判断
if(show[i][j]=='*')
get_mine_count(mine, show, i, j);
}
}
}
}
//寻找地雷
void findmine(char mine[rows][cols], char show[rows][cols])
{
int a, b, sum ;
int i, j;
while (1)
{
printf("请输入坐标,eg: nun1 num2 >:");
scanf("%d %d", &a, &b);
//限制变量不越界
if (a >= 1 && a <= 9 && b >= 1 && b <= 9)
{
if (mine[a][b] != '1')
{
get_mine_count(mine,show, a, b);
display(show);
}
else
{
printf("踩到雷了!!!\n");
display(show);
break;
}
}
else
{
printf("输入坐标错误,请重新输入>:");
}
//如果is_win()函数的返回值与设置的地雷数量相等,则扫雷成功。
if (is_win(show) == count)
{
printf("恭喜获胜!!!\n");
display(show);
printf("\n");
break;
}
}
}
3、源文件:playgame.c
#include"game.h"
void game()
{
char mine[rows][cols];
char show[rows][cols];
init(mine, '0');
init(show, '*');
display(show);
setmine(mine);
display(mine);
findmine(mine, show);
}
int main()
{
srand((unsigned int)time(NULL));
int input;
do
{
meau();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏!");
break;
default:
printf("输入错误,请重新输入>:");
break;
}
}while (input);
return 0;
}
总结:
由于博主自身水平原因,此处的 模块化设计 以及 递归 的应用不是特别好。本节主要为大家实现简易扫雷游戏。小伙伴们如果有更好的递归或判断方法欢迎在评论区交流意见。