全文目录
引言
在了解了C语言的知识之后,就可以尝试实现一些有意思的小程序。
在这篇文章中将详细介绍在编写扫雷时的思路。包括布雷、标雷、扫雷以及展开的一些实现,希望对大家有所帮助:
扫雷思路简述
分析模板
在实现扫雷之前,我们可以在网络上找到扫雷游戏,来分析一下它的功能:
首先,对于扫雷的难度等级有基础、中级、高级以及自定义。这里展示的是基础的难度,棋盘大小为9*9,共有10个雷待扫。
有标雷与扫雷两种模式,当点击鼠标右键时是标雷;点击鼠标左键时是扫雷。
当标雷时,会出现一个小红旗;
扫雷时,会点开该位置:若该位置有雷时,就会被炸到,游戏结束;若没有雷时,会显示该位置周围8个位置雷的数量。若周围的8个位置没有雷,就会向外展开,直到遇到周围有雷的位置停止,并显示雷数量。
当点到有雷的位置就会被炸到,游戏结束;当扫到只剩余10个方块且时,就说明已经排雷成功,游戏结束:
实现思路
我们需要将这些功能大概实现出来。由于还没有学到图形化界面与鼠标点击,所以暂时只考虑通过标准输入与标准输出来玩这个游戏:
布雷前,首先我们需要一个9*9的二维数组用来存储雷的信息,但是我们不能直接将这个雷的信息展示给玩家,所以还需要一个9*9的二维数组用来展示给玩家。
但又出现一个问题,就是当我们需要计算一个位置周围雷的大小时,需要遍历这个位置周围的8个位置。此时如果这个位置在边缘的位置时,遍历时就会出现数组越界的问题。所以我们需要的应该是两个11*11的二维数组用来存储雷的信息与展示给玩家的信息;
布雷时,我们可以使用随机数函数与时间戳来实现;
然后就是扫雷了,我们可以用switch语句来实现标雷与扫雷的区分。
并且封装一些函数来实现游戏中的一些操作。
在这次的实现中,为了更加清晰,将分文件实现。即声明在game.h头文件中;函数实现在game.c源文件中;主函数部分在test.c源文件中实现:
逐步实现
主体部分
main函数
首先,我们需要打印一个菜单,用于展示两种操作play与exit,以便用户选择:
void menu()
{
printf(" +----- 扫 雷 -----+\n");
printf(" |---- 1.play ----|\n");
printf(" |---- 0.exit ----|\n");
printf(" +------------------+\n");
}
再结合scanf与switch语句来实现游戏或退出:
当选择play时,进入game函数;选择exit时退出。
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入。。。\n");
break;
}
同时,我们需要实现当选择错误后或者再玩一遍后还能继续选择。所以我们使用do_while循环:
int main()
{
int input = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入。。。\n");
break;
}
} while (input);
return 0;
}
以上就是原函数的大概框架。
game函数
接下来需要在这个源文件中实现game函数的框架:
当进入game函数后,表示玩家选择开始游戏。
首先,我们需要创建两个11*11的二维数组:show数组表示展示给玩家的信息;mine数组表示布好后雷的位置。
为了在更改难度时方便一次更改所有数组的大小,我们可以将数组的大小定义为常量在game.h头文件中。当需要使用这个常量时,只需要引用这个自己的头文件即可:#include <game.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
接下来就需要初始化两个棋盘,我们将这步封装为一个函数init。这个函数可以实现将二维字符数组的每个元素初始化为指定的字符;
然后需要布雷。我们将布雷的操作也封装为一个函数set;
在布雷之后,我们需要将show数组打印出来,同时也需要将mine数组打印以方便调试代码。所以我们需要一个print函数,可以打印出一个指定的二维字符数组;
然后就是扫雷的部分了。这部分也可以封装为一个函数search。在这个函数中,将实现玩家寻找雷的一系列操作:
void game()
{
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
//初始化棋盘
init(show, ROWS, COLS, '*');
init(mine, ROWS, COLS, '0');
//设置雷
set(mine, ROW, COL);
//打印棋盘
print(show, ROW, COL);
//print(mine, ROW, COL);//调试用
//扫雷
search(show, mine, ROW, COL);
}
首先初始化棋盘,将show棋盘全部初始化为字符’*‘;将mine棋盘全部初始化为字符’0’(注意不是数字0);
接下来在mine棋盘中布雷;
打印棋盘,给玩家展示初始化的show棋盘;
最后开始扫雷。
函数部分
初始化棋盘 init
我们可以通过两层for循环来实现将二维字符数组的元素改为指定字符:
void init(char board[ROWS][COLS], int rows, int cols, char c)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = c;
}
}
}
这个函数有4个参数:
第一个表示指定二维字符数组;
第二个与第三个表示数组的实际大小;
第四个表示指定的字符。
返回值为void。
设置雷 set
我们可以通过srand与rand函数结合时间戳来生成一些任意的坐标,实现在棋盘中随机布雷:
void set(char board[ROWS][COLS], int row, int col)
{
int n = 0;
int x = 0;
int y = 0;
while (n < WIN)
{
x = (rand() % row) + 1;
y = (rand() % col) + 1;
if (board[y][x] == '0')
{
board[y][x] = '1';
n++;
}
}
}
rand函数可以实现随机生成一个0到0x7fff的数字。
调用rand函数前需要调用srand(seek)以给rand函数设置一个值,盗用srand函数时,后面的seek相同时,产生的随机值也是相同的,所以我们可以通过时间戳作为srand的参数以获取随机值: srand((unsigned int)time(NULL));
这句代码需要写在main函数中。
#include"game.h"
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入。。。\n");
break;
}
} while (input);
return 0;
}
由于生成的是非常大的数,所以我们需要将生成的数%row与col得到的是最终的结果(0到8的数)。
将雷的位置设置为字符’1’(mine棋盘中)。
打印棋盘 print
通过两个for循环实现打印指定棋盘:
void print(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf(" ------ 扫 雷 ------\n");
printf(" -------------------\n");
for (j = 0; j <= col; j++)
{
printf(" %d", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
这段代码打印的效果就是:
排雷 search
接下来就是排雷的部分。
在这部分中,我们需要实现两种模式,即扫雷与标雷;
在标雷模式下,指定的坐标将被标记为’?';在扫雷模式下,指定的坐标为雷则游戏结束,不为雷则该位置为其周围8个位置的雷数,需要时展开。
并且能够判断游戏是否结束(胜利或失败)。
我们可以通过switch语句来实现扫雷或标雷的选择:
void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int judge = 0;
int input = 0;
while (1)
{
printf(" ---- 1. find ----\n");
printf(" ---- 2. mark ----\n");
printf(" 请选择标雷或排雷:");
scanf("%d", &input);
switch (input)
{
case 1:
while (1)
{
printf("请输入坐标:");
scanf("%d%d", &x, &y);
if ((x<1 || x>col) || (y<1 || y>row))
{
printf("输入非法,请重新输入。。。\n");
}
else if (show[y][x]!='*')
{
if (show[y][x] == '?')
{
show[y][x] = '*';
print(show, ROW, COL);
break;
}
else
{
printf("该坐标已排过,请重新输入:\n");
}
}
else
{
if (mine[y][x] == '1')
{
printf("恭喜你,炸到了好樊鸭。。。\n");
show[y][x] = 'X';
print(show, ROW, COL);
judge = 1;
break;
}
else
{
DisplayNum(show, mine, ROW, COL, x, y);
print(show, ROW, COL);
int ret=IsWin(show, ROW, COL);
if (ret == 1)
{
printf("很遗憾,没有炸到好樊鸭。。。\n");
print(show, ROW, COL);
judge = 1;
}
break;
}
}
}
break;
case 2:
while (1)
{
printf("请输入坐标:");
scanf("%d%d", &x, &y);
if ((x<1 || x>col) || (y<1 || y>row))
{
printf("输入非法,请重新输入。。。\n");
}
else if (show[y][x] != '*')
{
if (show[y][x] == '?')
{
show[y][x] = '*';
print(show, ROW, COL);
break;
}
else
{
printf("该坐标已排过,请重新输入:\n");
}
}
else
{
show[y][x] = '?';
print(show, ROW, COL);
break;
}
}
break;
default:
printf("输入非法,请重新输入。。。\n");
break;
}
if (judge == 1)
{
break;
}
}
}
输入一个坐标,首先判断这个坐标的合法性。不能越界,也不能已经排过。
当扫雷模式时,若输入坐标在的mine棋盘的对应位置为’1’时,游戏失败;对应位置不为’1’时,进入我们封装的一个DisplayNum函数来实现显示数字或展开。之后再进入我们封装的IsWin函数来判断是否胜利。
当标雷模式是,将输入的坐标转换为’?'(show棋盘中)。
需要注意的是,在排到或标记到被标记为’?‘的位置时,’?'标记消除,不产生任何影响。
展示数字或展开函数DisplayNum
这个DisplayNum函数有6个参数,除了需要将两个二位字符数组以及棋盘大小传给DisplayNum函数外,还需要将前面输入的坐标传给DisplayNum。
我们可以通过两遍for循环来计算该坐标周围8个位置的雷的数量,由于已经确定输入坐标在mine棋盘中的字符为’0’所以直接遍历9个位置是可以的。
当这个数字不等于0是时将这个数字放到show棋盘中的相应位置。(需要注意的是,求出的sum是数字而非字符,所以在放到show棋盘中时,需要加’0’以转换为字符)
当这个数字等于0时,即该坐标周围的8个坐标都非雷,此时就需要进行展开:
展开时,我们可以采用函数递归的方式:
将输入位置坐标周围的8个坐标作为输入坐标再传递给DisplayNum,再进行判断,计算。。。
当某次坐标计算结果不为0时,该方向停止递归,并将计算的结果放在show数组的相应位置。
void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
int x2 = 0;
int y2 = 0;
int sum = 0;
//计算周围八个格子的雷数量
for (y2 = y - 1; y2 < y + 2; y2++)
{
for (x2 = x - 1; x2 < x + 2; x2++)
{
if (mine[y2][x2] == '1')
{
sum++;
}
}
}
if (sum != 0)
{
show[y][x] = sum + '0';
//print(show, ROW, COL);//调试用
}
//展开
else
{
show[y][x] = ' ';
for (y2 = y - 1; y2 < y + 2; y2++)
{
if (y2 < 1 || y2 > row)//如果y2超出限制,y2++后再判断
{
continue;
}
for (x2 = x - 1; x2 < x + 2; x2++)
{
if (x2 < 1 || x2 > col)//如果x2超出限制,x2++后再判断
{
continue;
}
else
{
if ((show[y2][x2] != '*') || ((x2 == x) && (y2 == y)))//遍历到已经遍历过的或与中心坐标相同就跳过
{
continue;
}
else
{
DisplayNum(show, mine, ROW, COL, x2, y2);
//print(show, ROW, COL);//调试用
}
}
}
}
}
}
判断胜利函数IsWin
每次排雷之后,都需要进行是否胜利的判断。
判断的原理为,当被标记的位置的数量加为扫的位置的数量等于最大雷数时('?‘与’*'的和),就表示游戏胜利。
直接遍历这个show棋盘即可。
int IsWin(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int x = 0;
int y = 0;
for (y = 1; y <= row; y++)
{
for (x = 1; x <=col; x++)
{
if ((show[y][x] == '*') || (show[y][x] == '?'))
{
count++;
}
}
}
if (count == WIN)
{
return 1;
}
else
{
return 0;
}
}
当然,我们的所有函数的声明都在game.h头文件中。
参考源码
//game.h
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define WIN 10
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
//初始化
void init(char board[ROWS][COLS], int rows, int cols, char c);
//设置雷
void set(char board[ROWS][COLS], int row, int col);
//打印棋盘
void print(char board[ROWS][COLS], int row, int col);
//排雷
void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col);
//计算或展开
void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y);
//判断是否胜利
int IsWin(char show[ROWS][COLS], int row, int col);
//game.c
#include"game.h"
void init(char board[ROWS][COLS], int rows, int cols, char c)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = c;
}
}
}
void set(char board[ROWS][COLS], int row, int col)
{
int n = 0;
int x = 0;
int y = 0;
while (n < WIN)
{
x = (rand() % row) + 1;
y = (rand() % col) + 1;
if (board[y][x] == '0')
{
board[y][x] = '1';
n++;
}
}
}
void print(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf(" ------ 扫 雷 ------\n");
printf(" -------------------\n");
for (j = 0; j <= col; j++)
{
printf(" %d", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int judge = 0;
int input = 0;
while (1)
{
printf(" ---- 1. find ----\n");
printf(" ---- 2. mark ----\n");
printf(" 请选择标雷或排雷:");
scanf("%d", &input);
switch (input)
{
case 1:
while (1)
{
printf("请输入坐标:");
scanf("%d%d", &x, &y);
if ((x<1 || x>col) || (y<1 || y>row))
{
printf("输入非法,请重新输入。。。\n");
}
else if (show[y][x]!='*')
{
if (show[y][x] == '?')
{
show[y][x] = '*';
print(show, ROW, COL);
break;
}
else
{
printf("该坐标已排过,请重新输入:\n");
}
}
else
{
if (mine[y][x] == '1')
{
printf("恭喜你,炸到了好樊鸭。。。\n");
show[y][x] = 'X';
print(show, ROW, COL);
judge = 1;
break;
}
else
{
DisplayNum(show, mine, ROW, COL, x, y);
print(show, ROW, COL);
int ret=IsWin(show, ROW, COL);
if (ret == 1)
{
printf("很遗憾,没有炸到好樊鸭。。。\n");
print(show, ROW, COL);
judge = 1;
}
break;
}
}
}
break;
case 2:
while (1)
{
printf("请输入坐标:");
scanf("%d%d", &x, &y);
if ((x<1 || x>col) || (y<1 || y>row))
{
printf("输入非法,请重新输入。。。\n");
}
else if (show[y][x] != '*')
{
if (show[y][x] == '?')
{
show[y][x] = '*';
print(show, ROW, COL);
break;
}
else
{
printf("该坐标已排过,请重新输入:\n");
}
}
else
{
show[y][x] = '?';
print(show, ROW, COL);
break;
}
}
break;
default:
printf("输入非法,请重新输入。。。\n");
break;
}
if (judge == 1)
{
break;
}
}
}
void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
int x2 = 0;
int y2 = 0;
int sum = 0;
//计算周围八个格子的雷数量
for (y2 = y - 1; y2 < y + 2; y2++)
{
for (x2 = x - 1; x2 < x + 2; x2++)
{
if (mine[y2][x2] == '1')
{
sum++;
}
}
}
if (sum != 0)
{
show[y][x] = sum + '0';
//print(show, ROW, COL);//调试用
}
//展开
else
{
show[y][x] = ' ';
for (y2 = y - 1; y2 < y + 2; y2++)
{
if (y2 < 1 || y2 > row)//如果y2超出限制y2++,再判断
{
continue;
}
for (x2 = x - 1; x2 < x + 2; x2++)
{
if (x2 < 1 || x2 > col)//如果x2超出限制x2++,再判断
{
continue;
}
else
{
if ((show[y2][x2] != '*') || ((x2 == x) && (y2 == y)))//遍历到已经遍历过的或与中心坐标相同就跳过
{
continue;
}
else
{
DisplayNum(show, mine, ROW, COL, x2, y2);
//print(show, ROW, COL);//调试用
}
}
}
}
}
}
int IsWin(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int x = 0;
int y = 0;
for (y = 1; y <= row; y++)
{
for (x = 1; x <=col; x++)
{
if ((show[y][x] == '*') || (show[y][x] == '?'))
{
count++;
}
}
}
if (count == WIN)
{
return 1;
}
else
{
return 0;
}
}
//test.c
#include"game.h"
void menu()
{
printf(" +----- 扫 雷 -----+\n");
printf(" |---- 1.play ----|\n");
printf(" |---- 0.exit ----|\n");
printf(" +------------------+\n");
}
void game()
{
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
//初始化棋盘
init(show, ROWS, COLS, '*');
init(mine, ROWS, COLS, '0');
//设置雷
set(mine, ROW, COL);
//打印棋盘
print(show, ROW, COL);
//print(mine, ROW, COL);//调试用
//扫雷
search(show, mine, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入。。。\n");
break;
}
} while (input);
return 0;
}
运行结果
总结
到此,扫雷的所有内容都已经结束了
这个工程量比较大,可能有一些细节的部分是没有说清楚的,如果有任何问题欢迎在评论区提出
代码在我的测试中目前还没有发现问题,若大家在测试过程中发现了任何的bug请在评论区指出,谢谢大家(参考源码在文末有展示,稍后会再将源码发布,方便大家测试)
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦