一.前言
呕吼,第二篇博客。过了大概半个多月的时间,我的c语言的学习之路也走过了一小段。本文带来扫雷游戏的实现过程,也作为这段时间所学知识的一个小总结。扫雷游戏大家应该都不陌生,谁敢说小时候一次没被炸死过呢ಡωಡ ಡωಡ。
言归正传,首先我们看下扫雷游戏的分析和设计。
二.扫雷游戏分析和设计
1.界面设计![](https://img-blog.csdnimg.cn/direct/cdac964382c642c3844e6890284fd84a.png)
上图即为我们所熟悉的扫雷游戏界面,由于具体规则大家应该都比较熟悉,这里不具体细说。本文暂不考虑图形界面的实现(主要是还不会),仅考虑通过控制台实现扫雷游戏,如下图所示:
2.数据结构分析
扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要⼀定的数据结构来存储这些 信息。 因为我们需要在9*9的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个9*9的数组来存放信息,如下图所示:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | |||||||||
1 | |||||||||
2 | |||||||||
3 | |||||||||
4 | |||||||||
5 | |||||||||
6 | |||||||||
7 | |||||||||
8 |
如果这个位置有雷我们就存1, 如果没有雷就存0,如下图所示:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
7 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
我们如果要排查(2,4)这个坐标时,访问周围一圈8个位置,统计周围雷的个数为0。但如果我们要排查(5,8)这个坐标时,右边的三个坐标就会发生越界。为了避免这种情况,我们不妨在设计的时候,将数组扩大一圈,雷还是布置在中间9×9的坐标上,而最外面一圈不去布置。所以我们将数组设计为11×11的数组较为合理。如下图所示:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
0 | |||||||||||
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
3 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | ||
4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | ||
6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
8 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ||
9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ||
10 |
棋盘问题解决了,我们接下来考虑如何存储一个坐标周围雷的信息,我们都知道扫雷游戏里点击一个不是雷的位置,他会显示周围雷的数量,我们如果将雷的个数信息也存放在布置雷的数组里,雷的信息和周围雷的个数信息可能会相互干扰产生打印上的困难。我们这里考虑用两个数组分开存储,即设置mine数组存放雷的信息,将排查的信息放在show数组。代码如下:
#define ROW 9 //这里为了方便以后更改棋盘难度 用define定义
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
char mine[ROWS][COLS] = {0}; //存放布置雷的信息
char show[ROWS][COLS] = {0}; //存放排出雷的个数信息
3.文件结构设计
我们设计三个文件:
test.c //⽂件中写游戏的测试逻辑
game.c //⽂件中写游戏中函数的实现
game.h //⽂件中写游戏需要的数据类型和函数声明
三.扫雷游戏代码实现
首先我们为游戏设置一个开始提示,我们用一个函数实现。
void menu() {
printf("---------------------------\n");
printf("------1.play 开始游戏-------\n");
printf("------0.exit 结束游戏-------\n");
printf("---------------------------\n");
}
当输入1时,我们正式开始游戏,当输入0的时候结束游戏,当输入其他数值时应提示输入错误,请重新输入。
void test() {
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case 1:
printf("运行游戏\n"); //为了测试目前代码是否正确,暂时用printf语句替代运行函数
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("输入错误,请重新输入:");
break;
}
} while (input);
}
代码测试结果如下
目前我们代码运行逻辑没有问题,开始着手游戏的实现。首先我们先对定义的两个数组进行初始化,我们将mine数组初始化为‘0’,show数组初始化为‘*’。用嵌套for循环来实现对数组的初始化。
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ret) {
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
board[i][j] = ret;
}
}
}
我们再来看棋盘的打印,注意我们只是为了防止数组越界将数组设置为11×11,但其周围一圈并不存放任何信息,我们最后要呈现的依旧是9×9的棋盘。
//打印棋盘
DisplayBoard(show, ROW, COL);
DisplayBoard(mine, ROW, COL);
void DisplayBoard(char board[ROWS][COLS], int row, int col) {
printf("--------扫雷--------\n");
int i = 0;
for (i = 0; i <= col; i++) {
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++) {
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++) {
printf("%c ", board[i][j]);
}
printf("\n");
}
}
现在棋盘的初始化和打印已经完成,我们开始着手雷的布置,用‘1’代表雷。并且在扫雷游戏中雷每次的出现位置应该是随机的,我们这里用rand()和srand()函数来实现随机。rand()函数能生成0~32767之间的随机数,而我们所要的应该是生成1~9之间的随机数,我们用rand () % ROW + 1即可实现1~9之间的随机数。srand()旨在每次重新开始游戏的时候变换rand()函数随机数种子。代码如下:
#define EASY_COUNT 10
void SetMine(char mine[ROWS][COLS], int row, int col) {
int count = EASY_COUNT; //EASY_COUNT是雷的个数,为了便于修改,仍用define进行定义
int x = 0;
int y = 0;
while (count) {
x = rand() % row + 1;
y = rand() % col + 1;
if (mine[x][y] != '1') {
mine[x][y] = '1';
count--;
}
}
}
void test() {
srand((unsigned int)time(NULL));
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);
}
雷的布置已经完成,接下里我们考虑如何进行排雷。如我们前面所讲,当一个位置不是雷的时候,统计其周围一圈雷的个数,并将其存放到show数组中。分两部来看,首先我们来看如何得到该位置周围雷的个数。假设此时排查位置坐标为(x,y),那么其周围8个位置的坐标范围即为x - 1 ~ x + 1,y - 1 ~ y + 1,由于此时(x,y)不是雷,其数值为'0',故不必单独考虑。值得注意的是,数组中存储的元素数据类型是char类型,想要得到int类型数据需减去‘0’计算差值。
int GetMineCount(char mine[ROWS][COLS], int x, int y) {
int i = 0;
int count = 0;
for (i = -1; i <= 1; i++) {
int j = 0;
for (j = -1; j <= 1; j++) {
count += (mine[x + i][y + j] - '0');
}
}
return count;
}
我们再来看如何具体进行排雷,首先输入要排查的坐标,先判断坐标是否合法,合法的话判断该位置是否是雷。如果是雷,game over,最后打印布雷数组;如果不是雷,将周围雷的数量赋给该坐标。设置win变量作为循环结束的标志,如果不是雷,win++直至将所有不是雷的坐标都排查过循环结束,排雷成功。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
char input = '0';
int win = 0;
while (win < row * col - EASY_COUNT) {
printf("请输入排查坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
//输入的位置是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
//不是雷
else {
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
printf("您输入的坐标有误x(1~9) y(1~9),请重新输入:>");
}
if (win == row * col - EASY_COUNT) {
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
我们此时将雷的数量设置为80,看一下成功的情况。
这样一次一次的排在棋盘尺寸比较大,雷的数量比较少的时候可能比较费时间,我们可以减小难度,当排查坐标周围一圈都没有雷的时候,从自身向周围不断平铺直至出现周围有雷的坐标听停止,即递归实现。实现如下:
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y) {
if (x >= 1 && x <= ROW && y >= 1 && y <= COL) {
int count = GetMineCount(mine, x, y);
if (count == 0) {
show[x][y] = ' ';
int i = 0;
for (i = x - 1; i <= x + 1; i++) {
int j = 0;
for (j = y - 1; j <= y + 1; j++) {
if (show[i][j] == '*') {
Unfold(mine, show, i, j);
}
}
}
}
else
show[x][y] = count + '0';
}
}
//此时要对排雷函数也进行一些改动,不再以排查所以不是雷的地方作为结束条件,而是进行标雷,当所有雷的位置都被标出排雷成功
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
char input = '0';
int win = 0;
while (win < EASY_COUNT) {
printf("请输入排查坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
//输入的位置是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
//不是雷
else {
Unfold(mine, show, x, y);
DisplayBoard(show, ROW, COL);
//标雷
getchar(); //此处getchar用来吸收上面输入的回车
printf("是否标雷,请输入Y or N:>");
scanf("%c", &input);
while (input == 'Y') {
printf("请输入标雷的坐标:>");
scanf("%d %d", &x, &y);
show[x][y] = '#'; //如果判断是雷,标记‘#’
if (mine[x][y] == '1') //如果标雷位置与雷的位置一致,win++
win++;
break;
}
DisplayBoard(show, ROW, COL);
}
}
else
printf("您输入的坐标有误x(1~9) y(1~9),请重新输入:>");
}
if (win == EASY_COUNT) {
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
我们此时将雷的数量设置为1, 来检验一下成功的情况。
至此,排雷游戏结束,各位如果想变换难度可以将ROW、COL、EASY_COUNT的值自行改动。完结撒花ಡωಡಡωಡಡωಡಡωಡ ,全部代码如下:
game.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.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 ret);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col);
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ret) {
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
board[i][j] = ret;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col) {
printf("--------扫雷--------\n");
int i = 0;
for (i = 0; i <= col; i++) {
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++) {
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++) {
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col) {
int count = EASY_COUNT;
int x = 0;
int y = 0;
while (count) {
x = rand() % row + 1;
y = rand() % col + 1;
if (mine[x][y] != '1') {
mine[x][y] = '1';
count--;
}
}
}
//得到周围雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y) {
int i = 0;
int count = 0;
for (i = -1; i <= 1; i++) {
int j = 0;
for (j = -1; j <= 1; j++) {
count += (mine[x + i][y + j] - '0');
}
}
return count;
}
//实现平铺
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y) {
if (x >= 1 && x <= ROW && y >= 1 && y <= COL) {
int count = GetMineCount(mine, x, y);
if (count == 0) {
show[x][y] = ' ';
int i = 0;
for (i = x - 1; i <= x + 1; i++) {
int j = 0;
for (j = y - 1; j <= y + 1; j++) {
if (show[i][j] == '*') {
Unfold(mine, show, i, j);
}
}
}
}
else
show[x][y] = count + '0';
}
}
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
char input = '0';
int win = 0;
while (win < row * col - EASY_COUNT) {
printf("请输入排查坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
//输入的位置是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
//不是雷
else {
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
printf("您输入的坐标有误x(1~9) y(1~9),请重新输入:>");
}
if (win == row * col - EASY_COUNT) {
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
test.c
#include"game.h"
void menu() {
printf("---------------------------\n");
printf("------1.play 开始游戏-------\n");
printf("------0.exit 结束游戏-------\n");
printf("---------------------------\n");
}
void game() {
char mine[ROWS][COLS] = { 0 }; //存放雷的信息
char show[ROWS][COLS] = { 0 }; //存放排出雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
//排雷
FindMine(mine, show, ROW, COL);
}
void test() {
srand((unsigned int)time(NULL));
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);
}
int main() {
test();
return 0;
}
四.结语
这是本人的第二篇博客,距离上篇时间跨度已有大半个月,其实中间应该有一篇循环语句的学习(假期去苏州玩了一圈,小偷懒),希望以后能继续坚持写下去。如果本文对各位有所帮助,我将倍感荣幸。
吾尽吾心,终亦不悔。天道酬勤,何事难为!