文章目录
扫雷游戏作为一款我们小时候电脑上为数不多的几个游戏之一,其功能简单,界面简洁。本篇将介绍如何用C语言实现扫雷游戏。本片以介绍9×9的棋盘,内含10颗雷的游戏难度为大家介绍,大家也可以在代码中更改棋盘的尺寸和雷的个数。
游戏介绍
扫雷,顾名思义就是要找到埋在地里的雷,游戏中9×9的棋盘代表了所有土地,每一个小格代表土地的一部分,其中可能埋着雷,也可能没有雷。玩家选择一个坐标来扫雷,若被选中的坐标区域有雷,则游戏失败;若没有雷,则会显示其周围八个坐标的有雷坐标的个数,若其周围没有雷,则会对周围坐标显示其周围的雷数。若将全部没有埋雷的坐标都扫完,则游戏胜利。
实现各功能代码
游戏的控制代码
如果我们要写一个游戏代码,首先要写一个控制代码,用来实现游戏的进入或退出。
那么我们希望程序运行后会出现一个菜单,提示我们:输入1进入游戏,输入2退出游戏,其他输入则提示输入错误,并重新输入。
实现该功能的代码如下:
main()
{
another:
printf("--------------------------\n");
printf("---------1 PLAY----------\n");
printf("---------2 EXIT----------\n");
printf("--------------------------\n");
printf("请选择:");
int input = 0;
scanf("%d", &input);
if (input == 1) {
game();
}
else if (input == 2) {
printf("游戏退出\n");
}
else {
printf("输入错误,请重新输入。\n");
goto another;
}
return 0;
}
实现游戏功能的代码
通过上述代码不难看出,若输入1,则会执行game()
函数,执行完后程序结束。那么game()
函数该具备那些功能呢?
初始化两个棋盘
大家如果流览过其他的扫雷游戏的代码,会发现都使用了两个二维数组来表示棋盘,那么为什么要用两个数组呢?一个棋盘用一个数组不久可以表示了吗?原因是这样,如果用一个数组来表示棋盘,将不利于分辨该坐标是否被排查过,同时也不利于棋盘的输出。那么我们就用到了两个棋盘,第一个棋盘用来布置雷和计算周围雷的个数,这个棋盘不会输出给玩家看;第二个棋盘初始化全都为*
来表示未知,排查过的坐标则显示该坐标周围的雷的个数。这样做的好处很显然,第一个棋盘(即数组)是不会改变的,这样对我们统计某个坐标周围雷的个数时很方便的,如果只使用一个棋盘,则会非常混乱。
我们将第一个数组初始化为0
,并随机产生10个坐标,将数组的值改为1
,这样当我们计算某个坐标周围的雷的个数时,只需遍历他周围的8个坐标,并将数组的值相加,相加的结果即为雷的个数。
到这里有人就会发现问题了,这种计算雷的个数的方法,对于处在棋盘中间的坐标是可行的,但是对于在棋盘边上的坐标是不可行的,因为这种计算方法会导致数组越界,即计算(9,9)这个位置的周围雷的个数时,会将他周围一圈的坐标的数组值相加,这样就会出现(10,8)(10,9)(10,10)(9,10)(8,10)这几个坐标,很显然我们的数组就会越界,那么为了避免这种情况,我们可以选择将数组定义为[ROW+2][COL+2]这种大小(ROW表示棋盘的行数,COL表示棋盘的列数),即将棋盘向外扩展了一圈,我们将拓展出来的这一圈坐标对应的数组值定义为0,随机生成雷的时候,限制雷不能生成在最外圈,这样就可以完美契合我们的思路了,这一步是整个扫雷游戏代码的最精华的思想。
我们可以先创建一个头文件,然后在头文件中设置棋盘的尺寸和雷的个数。代码如下:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h> //为了后续实现随机数
#define ROW 9 //行数和列数,这两个代表棋盘的尺寸
#define COL 9
#define ROWS ROW+2 //这两个代表数组的实际尺寸
#define COLS COL+2
#define mine_number 10 //设置雷的个数
首先我们要初始化两个棋盘,可以创建一个code.c
文件,这个文件中用于存放游戏部分的函数代码。
数组初始化函数:
//将数组初始化为字符'set'
void Init(char arr[ROWS][COLS], int rows, int cols, char set)
//注意传递的数组的行数和列数,这里数组的实际大小为[ROWS][COLS],且我们想要全部初始化,
//所以后面两个参数我们定义的是 rows 和 cols ,即提醒我们传递 ROWS 和 COLS,而不是传递 ROW 和 COL
#define COLS
{
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
arr[i][j] = set;
}
}
}
布置雷
棋盘初始化好了之后,我们就可以随机放置地雷了。
布置雷函数:
void Set_mine(char arr[ROWS][COLS])
{
srand((unsigned int)time(NULL));
int num = mine_number;
while (num) {
int x = rand() % 9 + 1; //生成1~9之间的随机数,不能生成0和10,因为最外圈不布置雷
int y = rand() % 9 + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
num--;
}
}
}
棋盘的输出
布置好雷之后,我们可以将棋盘输出给玩家看了,但我们输出的一定是第二个棋盘,那个被我们初始化为*
的棋盘,这里如果一不小心将第一个棋盘输出了,那就相当于是把答案告诉玩家了,不过在我们调试程序的过程中可以将第一个棋盘打印出来,用来辅助我们调试。为了方便输出,我们可以写一个输出数组的函数。
数组输出函数:
void Print(char arr[ROWS][COLS], int row, int col)
//这里数组的实际大小虽然为[ROWS][COLS],但我们只需要打印棋盘大小的数组即可,
//所以我们后续的实参需要给的是 ROW 和 COL ,这里定义形参为 row 和 col 起提示作用
{
for (int r = 0; r <= rows; r++) //输出横坐标轴
printf("%d ", r);
printf("\n");
int i = 0;
for (i = 1; i <= rows; i++) {
printf("%d ", i); //输出纵坐标轴
int j = 0;
for (j = 1; j <= cols; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
计算周围雷的个数
玩家输入了想要扫雷的坐标后,程序要计算该坐标周围雷的个数,并将该值替换第二个数组的*
,然后输出第二个数组给玩家显示扫雷后的结果。
同时在头文件中定义一个全局数组win[1]
并初始化为0,然后每执行一次计算函数,win[0]
的值就+1,这样win[0]
的值就可以代表已扫过的坐标个数。(指针还没学,暂时用数组代替)
计算函数的实现很简单,只是需要注意我们最开始定义的是字符数组,所以在进行加减计算时,记得'0'
的计算。
计算坐标周围雷的个数,并将该值替换在第二个数组中,函数代码如下:
void Count(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
int num = 0;
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++) {
if (i == x && j == y)
continue;
else {
num += arr1[i][j] - '0';
}
}
}
arr2[x][y] = num + '0';
win[0]++;
}
扫雷函数
扫雷函数首先要是一个循环,在雷未被扫完时,一直进行这个循环,在雷被扫完后,跳出循环,并提示玩家游戏胜利。判断游戏胜利的条件可以为win[0]==ROW*COL-mine_number
,即扫过的坐标个数=棋盘的总坐标个数-埋了雷的坐标个数;也可以遍历第二个数组,看该数组中有多少个*
,如果*
的个数等于雷的个数,则游戏胜利,这种方式不需要用到win[0]
,所以在上一步中也无需定义win[1]
。
循环体中,首先需要玩家输入要判断的坐标,然后判断该坐标是否合法,再判断该坐标是否有雷,再判断该坐标是否已排查过,最后进行扩展函数,输出扩展后的第二个数组。(扩展函数见下)
扫雷函数:
void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col) {
while (win[0] < ROW * COL - mine_number) //替换部分
//while(Num(arr2, ROW, COL) > mine_number)
{
printf("请输入扫雷的坐标:");
int x = 0;
int y = 0;
scanf("%d%d", &x, &y);
if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { //判断坐标是否合法
printf("\n");
if (arr1[x][y] == '1') {
printf("游戏失败。\n");
Print(arr1, ROW, COL); //游戏失败后,显示埋雷情况
break;
}
else {
if (arr2[x][y] != '*') {
printf("这个位置已经排查过了,请重新选择。\n");
}
else if(arr1[x][y]=='0') {
Extend(arr1, arr2, x, y); //周围展开函数
Print(arr2, ROW, COL);
}
}
}
else {
printf("非法输入,请重新选择。\n");
}
if (win[0] == ROW * COL - mine_number) { //替换部分
printf("恭喜你!游戏获胜!\n"); //替换部分
break; //替换部分
} //替换部分
/*if (Num(arr2, ROW, COL) == mine_number) {
printf("恭喜你!游戏获胜!\n");
break;
}*/
}
}
上述代码使用的是win[0]==ROW*COL-mine_number
这个判断条件,如果想使用另一个判断条件,就替换上述代码中的注释部分,并在这个函数前面定义一个数第二个数组中*
的个数的函数即可。(下面的代码一定要在上面的代码之前)
数*
函数:
int Num(char arr[ROWS][COLS], int row , int col )
{
int number = 0;
for (int i = 1; i <= ROW; i++)
{
for (int j = 1; j <= COL; j++)
{
if (arr[i][j] == '*')
number++;
}
}
return number;
}
扩展函数(递归部分)
扩展函数是整个程序最大的难点,什么条件下执行递归,如何停止递归。
在Clear_mine
函数中,如果arr1[x][y]=='0'
成立,就执行Extend(arr1, arr2, x, y)
,所以我们第一步就是先计算(x,y)坐标周围的雷的个数,所以Extend
函数的第一步就是执行一次Count
函数。
扫雷游戏在什么情况下会展开周围的坐标呢?我们可以玩两把扫雷游戏找找规律,展开对主坐标和展开坐标都有要求。首先主坐标不能雷,且周围雷的个数为0,代码表示为if (arr1[x][y] == '0' && arr2[x][y] == '0')
,然后遍历周围八个坐标,如果其不是雷,则以该坐标作为主坐标,执行Extend
那么我们的代码就可以表示为:
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
Count(arr1, arr2, x, y);
if (arr1[x][y] == '0' && arr2[x][y] == '0')
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (arr1[i][j] == '0')
Extend(arr1, arr2, i, j);
}
}
}
}
如果用这个代码去调试,就会发现程序进入了死循环,这是为什么呢?
原来是已经扫过的坐标又被作为周围坐标,然后符合做主坐标的条件,从而进入了死循环。即以A坐标作为主坐标展开,展开遇到B坐标,B坐标符合做主坐标的条件,所以以B作为主坐标展开,展开遇到A坐标,A坐标符合…从而进入死循环,那么为了避免这种情况,我们可以设置某个坐标只有未被展开过才能以其为主坐标执行Extend
代码表示为:
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
Count(arr1, arr2, x, y);
if (arr1[x][y] == '0' && arr2[x][y] == '0')
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (arr1[i][j] == '0' && arr2[i][j] == '*')
Extend(arr1, arr2, i, j);
}
}
}
}
这样代码就不会死循环了,运行调试感觉也像模像样的。
但我们多试几次,就会发现代码是有问题的。
可以看到,左边部分的代码展开是没有问题的,但是为什么右面也有部分代码被展开了呢?
经过数个小时的调试,终于发现了问题所在,原来我们之前设置的最外面一圈数也参与进了递归的判断。
也就是说,除了输出的9×9棋盘,最外面还有一圈0,这一圈0符合本身没有雷,所以只要他的周围也没有雷,就会以他作为主坐标展开,就会有可能逐个以最外圈的坐标作为主坐标展开,从而展开到很远的地方,上述情形就是如此。
所以在判断条件中还应该限制坐标在(1,1)~(9,9)之间。
所以最终我们的Extend
函数应该为:
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
Count(arr1, arr2, x, y);
if (arr1[x][y] == '0' && arr2[x][y] == '0')
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (arr1[i][j] == '0' && i >= 1 && j >= 1 && arr2[i][j] == '*' && i<=ROW && j<=COL)
Extend(arr1, arr2, i, j);
}
}
}
}
至此我们的扫雷游戏各个部分就已经都准备好了。
参考代码
code.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "code.h"
#include<time.h>
extern mine;
extern show;
extern int win[1] = { 0 };
void Init(char arr[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++) {
arr[i][j] = set;
}
}
}
void Print(char arr[ROWS][COLS], int row, int col)
{
for (int r = 0; r <= row; r++)
printf("%d ", r);
printf("\n");
int i = 0;
for (i = 1; i <= row; i++) {
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++) {
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
void Set_mine(char arr[ROWS][COLS])
{
srand((unsigned int)time(NULL));
int num = mine_number;
while (num) {
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
num--;
}
}
}
void Count(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
int num = 0;
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++) {
if (i == x && j == y)
continue;
else {
num += arr1[i][j] - '0';
}
}
}
arr2[x][y] = num + '0';
win[0]++;
}
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y)
{
Count(arr1, arr2, x, y);
if (arr1[x][y] == '0' && arr2[x][y] == '0')
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (arr1[i][j] == '0' && i >= 1 && j >= 1 && arr2[i][j] == '*' && i<=ROW && j<=COL)
Extend(arr1, arr2, i, j);
}
}
}
}
void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col) {
while (win[0] < ROW * COL - mine_number)
//while(Num(arr2, ROW, COL) > mine_number)
{
printf("请输入扫雷的坐标:");
int x = 0;
int y = 0;
scanf("%d%d", &x, &y);
if (x >= 1 && x <= ROW && y >= 1 && y <= COL) {
printf("\n");
if (arr1[x][y] == '1') {
printf("游戏失败。\n");
Print(arr1, ROW, COL);
break;
}
else {
if (arr2[x][y] != '*') {
printf("这个位置已经排查过了,请重新选择。\n");
}
else if(arr1[x][y]=='0') {
Extend(arr1, arr2, x, y);
Print(arr2, ROW, COL);
}
}
}
else {
printf("非法输入,请重新选择。\n");
}
if (win[0] == ROW * COL - mine_number) {
printf("恭喜你!游戏获胜!\n");
break;
}
/*if (Num(arr2, ROW, COL) == mine_number) {
printf("恭喜你!游戏获胜!\n");
break;
}*/
}
}
code.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define mine_number 10
//初始化
void Init(char arr[ROWS][COLS], int rows, int cols, char set);
//打印数组
void Print(char arr[ROWS][COLS], int rows, int cols);
//埋雷
void Set_mine(char arr[ROWS][COLS]);
//扫雷
void Clear_mine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col);
//展开周围
void Extend(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int x, int y);
Mine_game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "code.h"
void game()
{
//创建两个二维数组并初始化
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
Init(mine,ROWS,COLS, '0');
Init(show, ROWS, COLS, '*');
//打印
//Print(mine, ROW, COL);
//Print(show, ROW, COL);
printf("该棋盘中共有%d个雷,请将其他区域选择出来。\n", mine_number);
//随机加入地雷
Set_mine(mine);
//Print(mine, ROW, COL);//调试时可以取消注释,打印mine数组
Print(show, ROW, COL);
//扫雷
Clear_mine(mine,show,ROW,COL);
}
main()
{
another:
printf("--------------------------\n");
printf("---------1 PLAY----------\n");
printf("---------2 EXIT----------\n");
printf("--------------------------\n");
printf("请选择:");
int input = 0;
scanf("%d", &input);
if (input == 1) {
game();
}
else if (input == 2) {
printf("游戏退出\n");
}
else {
printf("输入错误,请重新输入。\n");
goto another;
}
return 0;
}
运行演示
控制代码
扫雷游戏
为了方便演示,这里设置雷的个数为3。
结语
扫雷游戏是C语言的一道很老的练手题了,最巧妙的是用两个棋盘来分别表示埋雷和扫雷。但哪怕是在知道这种方法的情况下,想完整的写出扫雷游戏对初学者来说仍很困难,在我写的过程中,耗费时间最长的就是Extend
函数的递归,在何种情况下进入递归,文章中写的过程就是我试错的过程,写出来后确实看着不难,但想写出来确实不简单。
除此之外,扫雷游戏还可以再完善,比如:第一步一定不会踩到雷,标记雷的功能,记录走的步数,记录游戏时间等等,这些就属于细枝末节了,想加的话可以自己写一写。
最后,祝大家都能写出来!