前言
扫雷是我们同年的小游戏,今天我们就来实现一下这个童年小游戏。
与上次三子棋一样,我们用三个文件完成这个程序。
目录
一. 设计思路
1.选项
每一个游戏都有属于自己的游戏选择界面
(1 / 0) 玩游戏/退出游戏 && 输入其他数字则输入错误重新输入
void menu()
{
printf("**************************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**************************\n");
}
#include "game.h"
int main()
{
int input = 0;
menu();
do
{
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
游戏模块
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);
}
主函数中的#include
并没有使用库函数的头文件,而是使用自己写出来的game.h文件
。
2. 扫雷游戏的设计
我们用两个数组[show 数组 (用户排雷观看用)与 mine数组(布置雷用)], show数组用来展示该坐标周围八个坐标有多少个雷。当我学习了一位大佬的设计,mine数组用来设置地雷,用字符'1'
来设置为雷,而字符'0'
为非雷,当然我们也可以用其他字符来设置雷,但是当我们看了后续代码就可以看出大佬这样设计的方便之处了。
show数组(棋盘)
mine数组(棋盘)
当我们设置这么一个 9 x 9
的二维数组,就会发现当选择的坐标不是边界的坐标时,只需要统计周围八个坐标是否是雷,而如果是选择边界的坐标时,若访问周围八个坐标时,则有可能会越界访问,并且如果我们将这些特殊情况单独列出来,对于我们目前来说还是有些许困难,所以我选择在这个9 x 9
的二维数组加大一圈。
如果我们在我们需要的大小下再加大一圈的话,就不需要担心这个问题了。若我们需要一个 9 x 9
的扫雷界面 ,我们则设计一个11 x 11
的数组。像下图,我们需要的是蓝色部分,但是如果我们选择的是边界,周围八个坐标都在11 x 11
的数组中,也不会出现数组越界的情况。
3. 棋盘的初始化
我们要将棋盘的内容进行初始化。当我们进入一个扫雷游戏的时候,刚开始我们什么都看不见,所以我们在给用户显示的棋盘上都初始化为*
,对于布置雷的棋盘先全部初始化为'0'
,之后再布置雷。
当我们看下面这段初始化代码,这里的board[i][j]
我们并不知道放什么值,假设初始化mine棋盘放'0'
,那么我们还要设计一个初始化show棋盘的函数,相反也是,无论我们传什么进去都要设计两个函数,下面对该函数进行优化。
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = ; // 0 ? *
}
}
}
当我们将函数多一个参数,我们想初始化什么就将该字符传进去。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
4. 布置雷的函数
注意事项:
(1)布置雷的时候,记得控制坐标的范围,防止越界。
(2)布置雷的时候,要判断该坐标是否为非雷的坐标。
(3)记得rand()函数
使用的必要条件。
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
}
5. 棋盘的打印
这里做一个用户方便看的棋盘可能要试验好几次。
记得在每一行,每一列记录一下行号和列号,方便用户输入坐标。
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 1;
int j = 1;
printf(" *************** 扫雷 ***************\n");
for (j = 0; j <= col; j++)
{
printf(" %d ", j); //记录行号
}
printf("\n\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i); 记录列号
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j != col)
{
printf("|");
}
}
printf("\n");
if (i != row)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j != col)
{
printf("|");
}
}
}
printf("\n");
}
}
6. 排雷函数
6.1 排雷
void FindMine(char mine[ROWS][COLS],
char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入需要排除的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (show[x][y] == '*') //判断该坐标是否已经被排除
{
if (mine[x][y] == '1') //判断是否为雷
{
printf("你输了,游戏结束\n");
DisplayBoard(mine, ROW, COL);
return;
}
else
{
//记录排除坐标的个数
win += Explode_spread(show , mine , row , col , x ,y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("该坐标已被占用\n");
}
}
else
{
printf("输入的坐标非法\n");
}
printf("是否需要标记地址,若需要则输入'Y',否则则输入’N'\n");
while ((ch = getchar()) != '\n'); //清理缓存区
scanf("%c", &ch);
switch (ch)
{
case 'Y':
SignMine(show, row, col);
break;
case 'N':
break;
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,游戏胜利\n");
}
}
6.2 记录周围雷个数
这里统计雷个数的方法很巧妙。
若用其他的定义雷,把周围的八个坐标全部判断会有点麻烦,但是这里用'1'
定义雷只需要将八个坐标的值相加再与8 * '0'
相减则能直接得到雷的个数。
int MineCount(char mine[ROWS][COLS] ,int x ,int y)
{
return (mine[x - 1][y - 1] +
mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1]) - 8 * '0';
}
6.3 非雷的连片展开
扫雷游戏中,当排查的坐标周围的八个坐标没有一个雷,show棋盘上该坐标显示为空格,然后排查的的范围将会扩大,将周围的八个坐标挨个向前面一样判断,直到遇到周围有雷的时候停止,并且将周围的雷的个数记录在show棋盘上。
注意事项:
(1)我们设计的这个函数为递归函数,必须有条件来限制,否则会造成死递归。这里我们看到两个重合的位置,若不加限制条件,这几个坐标相互向外判断,造成死循环。
(2)注意控制坐标的合法性,不能让数组越界访问。
int Explode_spread(char show[ROWS][COLS],
char mine[ROWS][COLS],int row,int col,int x,int y)
{
int i = 0, j = 0;
int win = 0;
if (x >= 1 && x <= row && y <= col && y >= 1)
{
//防止数组越界
if ((MineCount(mine, x, y) + '0') == '0')
{
//若该坐标周围八个坐标都没有雷,向外扩散
show[x][y] =' ';
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
{
//判断是否该坐标已经被判断过
Explode_spread(show, mine, row, col, i, j);
win++;
}
}
}
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';
win++;
}
}
return win;
}
6.4 标雷
void SignMine(char show[ROWS][COLS],int row, int col)
{
static sign = EASY_COUNT; //标记的次数
char option = 'Y';
int x = 0;
int y = 0;
char ch = 0;
if (sign) //现在标记次数
{
while (option == 'Y')
{
printf("还能标记%d个坐标\n", sign);
printf("请输入需要标记的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (show[x][y] == '*')
{
show[x][y] = '?';
sign--;
DisplayBoard(show, ROW, COL);
}
else
{
printf("该坐标已被占用\n");
}
}
else
{
printf("输入的坐标非法\n");
}
printf("是否需要继续标记地址,若需要则输入'Y',否则则输入’N'\n");
while ((ch = getchar()) != '\n'); //清理缓存区
scanf("%c", &option);
}
}
else
{
printf("标记次数过多\n");
return;
}
}
二. 整体程序的完成
1. game.h文件 :声明
#pragma once
#define ROW 9 //需要的行
#define COL 9 //需要的列
#define ROWS ROW + 2 //加大一圈的行
#define COLS COL + 2 //加大一圈的列
#define EASY_COUNT 10 //简单版本的地雷个数
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols, char set);
//打印棋盘
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);
2. game.c文件 :扫雷的实现模块
#include "game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 1;
int j = 1;
printf(" *************** 扫雷 ***************\n");
for (j = 0; j <= col; j++)
{
printf(" %d ", j); //记录行号
}
printf("\n\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i); //记录列号
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j != col)
{
printf("|");
}
}
printf("\n");
if (i != row)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j != col)
{
printf("|");
}
}
}
printf("\n");
}
}
//布置地雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
}
//记录一个坐标周围有多少个地雷
int MineCount(char mine[ROWS][COLS] ,int x ,int y)
{
return (mine[x - 1][y - 1] +
mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1]) - 8 * '0';
}
//若一个坐标周围八个坐标都没有雷,连片展开
int Explode_spread(char show[ROWS][COLS],
char mine[ROWS][COLS],int row,int col,int x,int y)
{
int i = 0, j = 0;
int win = 0;
if (x >= 1 && x <= row && y <= col && y >= 1)
{
//防止数组越界
if ((MineCount(mine, x, y) + '0') == '0')
{
//若该坐标周围八个坐标都没有雷,向外扩散
show[x][y] =' ';
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
{
//判断是否该坐标已经被判断过
Explode_spread(show, mine, row, col, i, j);
win++;
}
}
}
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';
win++;
}
}
return win;
}
//标记地雷
void SignMine(char show[ROWS][COLS],int row, int col)
{
static sign = EASY_COUNT;
char option = 'Y';
int x = 0;
int y = 0;
char ch = 0;
if (sign)
{
while (option == 'Y')
{
printf("还能标记%d个坐标\n", sign);
printf("请输入需要标记的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (show[x][y] == '*')
{
show[x][y] = '?';
sign--;
DisplayBoard(show, ROW, COL);
}
else
{
printf("该坐标已被占用\n");
}
}
else
{
printf("输入的坐标非法\n");
}
printf("是否需要继续标记地址,若需要则输入'Y',否则则输入’N'\n");
while ((ch = getchar()) != '\n'); //清理缓存区
scanf("%c", &option);
}
}
else
{
printf("标记次数过多\n");
return;
}
}
//排雷
void FindMine(char mine[ROWS][COLS],
char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入需要排除的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y <= col && y >= 1)
{
if (show[x][y] == '*') //判断该坐标是否已经被排除
{
if (mine[x][y] == '1') //判断是否为雷
{
printf("你输了,游戏结束\n");
DisplayBoard(mine, ROW, COL);
return;
}
else
{
//记录排除坐标的个数
win += Explode_spread(show , mine , row , col , x ,y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("该坐标已被占用\n");
}
}
else
{
printf("输入的坐标非法\n");
}
printf("是否需要标记地址,若需要则输入'Y',否则则输入’N'\n");
while ((ch = getchar()) != '\n'); //清理缓存区
scanf("%c", &ch);
switch (ch)
{
case 'Y':
SignMine(show, row, col);
break;
case 'N':
break;
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,游戏胜利\n");
}
}
3. test.c文件:测试扫雷的基本功能
#include "game.h"
void menu()
{
printf("**************************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**************************\n");
}
void game()
{
//定义两个11 x 11 的数组(棋盘)
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);
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
menu();
do
{
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
结尾
如果有什么建议和疑问,或是有什么错误,希望大家能够提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!