【C语言】写一个和电脑上差不多的扫雷游戏(超详细教程!)
前言
扫雷游戏是一个非常好的益智小游戏,我相信很多人都玩过,它的游戏规则也很简单,就是需要玩家在最短时间内排查出雷盘上所有非雷的格子,若格子不是雷就显示这个格子周围一圈有多少个雷,若点击到是雷的格子就被炸死。
今天我给大用C语言实现一个和电脑上逻辑差不多的扫雷游戏。
电脑上(网页上)的扫雷
既然我们要实现一个与电脑上逻辑差不多的扫雷游戏,那我们必先知道电脑上的扫雷游戏逻辑是怎样的:
我们可以看到:电脑上的扫雷游戏最大的特点是当点击的格子周围一颗雷也没有时,就会展开一大片,知道展开到周围有至少一颗雷的格子。
当被炸死时,就会显示出所有的雷而且未点击的格子也不会显示出数字。
当游戏胜利时,也会显示出所有雷的位置。
模块化编程
在讲具体步骤之前先要给大家来讲一下模块化编程:
模块化编程就是把一个大问题分层若干个小问题来解决,好比一个公司分成了若干个部门,每一部门都有其分配的任务。这样子问题处理起来就不会容易乱,而且逻辑清晰。
我们这里主要分成三个文件:
game.h—>用来存放游戏相关的各函数的声明
game.c—>用来存放游戏相关的各函数的实现
test.c—>用来测试游戏(也就是真正开始玩游戏)
具体实现步骤
1、创建菜单
2、创建雷盘并初始化
3、布置雷到存放雷的雷盘
4、打印用于显示的雷盘
5、通过玩家输入坐标排查雷
6、统计坐标周围有过少个雷
7、判断排雷是否成功
8、用于游戏结束(输/赢)的打印
创建菜单
我们可以把菜单当成我们的主界面,在游戏结束或者胜利是选择返回:
// 菜单
void menu() {
printf("<<<<<<<<欢迎来到扫雷游戏>>>>>>>>\n");
printf("<<<<<<<<选择1---开始游戏>>>>>>>>\n");
printf("<<<<<<<<选择0---退出游戏>>>>>>>>\n");
}
创建棋盘并初始化
创建棋盘和初始化都是和游戏相关的内容,所以我们要在game.c文件中定义一个game函数,用来实现游戏的具体逻辑:
// 游戏
void game() {
// 创建一个雷盘,用于存放布置好的雷
char mineBoard[ROWS][COLS];
// 创建一个雷盘,用于显示排查出的雷的信息
char showBoard[ROWS][COLS];
// 对雷盘进行初始化
Init(mineBoard, ROWS, COLS, '0');
Init(showBoard, ROWS, COLS, '0'); // 用于显示的雷盘初始化为0也能标志该坐标未被访问过
}
创建雷盘里的ROW、COL和ROWS、COLS都是在game.h文件里使用宏定义的常量:
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
这里为什么会有ROWS = ROW + 2可以先不管,后面讲到排查雷的函数的时候会解释
初始化雷盘
// 初始化雷盘
void Init(char board[ROWS][COLS], int rows, int cols, char set) {
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
board[i][j] = set;
}
}
}
布置类到存放雷的雷盘
我们需要电脑随机在雷盘上布置雷,而且我们只把雷布置到雷盘中间ROW行COL列的位置,就是空出四周的行和列不使用(后面会解释,别急):
// 设置雷到雷盘
void SetMine(char board[ROWS][COLS], int row, int col) {
srand((unsigned int)time(NULL)); // 产生随机数生成器,并保证生成的数够随机
int count = COUNT;
while (count) {
int x = (rand() % 9) + 1; // 产生一个随机数
int y = (rand() % 9) + 1; // 取余再加1的原因是为了使产生的坐标范围在1 - ROW和1-COL
if (board[x][y] == '0') {
board[x][y] = '*'; // 将雷显示成'*'
count--;
}
}
}
这里面有一个COUNT也是在game.h里使用宏定义的常量,这样能使我们的代码扩展性很好,若以后要修改雷的数量也很方便。
这里面使用到的rand函数和time函数都要引相应的头文件,rand函数对应的头文件时stdlib.htime函数对应的头文件是time.h。我们把头文件的包含统一放到game.h里面:
#include <stdlib.h>
#include <time.h>
打印用于显示的雷盘
// 打印雷盘
void print(char board[ROWS][COLS], int row, int col) {
int i, j;
printf(" |------------------|\n");
printf(" | (⊙▽⊙) |\n");
printf(" |------------------|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf("%2d", i);
for (j = 1; j <= col; j++) {
if (board[i][j] != '0') {
printf(" %c", board[i][j]);
}
else {
printf("□"); // 若坐标未被访问过就打印一个"□",保持神秘……
}
}
printf("\n");
}
}
雷盘打印的效果如下👇 :
排查雷
通过玩家输入坐标进行排查若坐标不是雷,显示该坐标周围有几个雷,若是雷就被炸死,若坐标不是雷且坐标周围也没有雷,则展开一片(用函数的递归实现):
// 排查雷
// board1是用于显示的棋盘,board2是用于存放雷的棋盘
void FindMine(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int x, y;
int win = 0;
while (1) {
printf("请输入要排查的坐标>:");
scanf("%d %d", &x, &y);
if ((x < 1 || x > row) || (y < 1 || y > col)) {
printf("坐标输入有误,请重新输入……\n");
continue;
}
if (board1[x][y] != '0' && board2[x][y] != '*') {
printf("此坐标已被排查,请重新输入……\n");
continue;
}
if (board2[x][y] == '*') {
game_over(board1, board2, row, col); // 被炸死了
break;
}
count_mine(board1, board2, x, y);
win = isWin(board1, board2, row, col);
if (win == 1) {
game_wins(board1, board2, row, col); // 赢了
break;
}
print(board1, row, col);
}
}
统计坐标周围多少个雷(递归实现)
到这里就可以解释为什么要空出四周的一行或一列了:
如上图,我们在统计坐标周围有多少个雷的时候,其实就是在遍历访问该坐标周围的坐标,可想而知若是我们不前后左右个空出一行或一列的话,那么在排查到边缘坐标时候就会出现数组越界异常。
代码实现如下:
// 统计坐标周围雷的个数
// board1是用于显示的雷盘,board2是用于存放雷的雷盘
void count_mine(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y) {
int count = 0;
int i, j;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i == 0 && j == 0) {
continue;
}
if (board2[x + i][y + j] == '*') {
count++;
}
}
}
if (count == 0) {
board1[x][y] = ' '; // 0个雷让它显示为空格,这样看起来清爽一点
}
else {
board1[x][y] = count + '0';
}
// 递归开始
if ((x >= 1 && x <= COL) && (y >= 1 && y <= ROW) && count == 0) { // 一定要在布置雷的区域才递归,不然又会出现数组越界异常
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i == 0 && j == 0) {
continue;
}
if (board1[x + i][y + j] == '0') { // 一定要是未访问过的坐标才递归,否则必定出现死递归,导致栈溢出
count_mine(board1, board2, x + i, y + j);
}
}
}
}
}
判断是否排雷成功
判断输其实很简单,点到雷就输了,在排雷函数里判断即可。判断赢就是当你把所有非雷的坐标全都排完就赢了。其思路是统计用于显示的雷盘中已被访问过的坐标的个数,当已被访问过的坐标数等于坐标总数(实际放雷区域的坐标总数) 减去雷的个数时,就赢了。
代码实现如下:
// 判断游戏是否胜出,赢返回1,未赢返回0
int isWin(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int count = 0;
int i, j;
for (i = 1; i <= row; i++) {
for (j = 1; j <= col; j++) {
if (board2[i][j] == '*') {
continue;
}
if (board1[i][j] != '0') {
count++;
}
}
}
if (count == row * col - COUNT) {
return 1;
}
return 0;
}
用于游戏结束(输/赢)的雷盘打印
这两个函数其实逻辑是一样的,只是显示的不一样。
赢:
// 用于游戏胜利的雷盘打印
// board1是用于显示雷盘,board2是存放类的雷盘
void game_wins(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int i, j;
printf(" |★★★★★★★(*^O^*)★★★★★★★|\n");
printf(" |★★★恭喜你,排雷成功!太棒了!★★★|\n");
printf(" |★★★★★★★★★★★★★★★★★★|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
printf(" ");
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf(" %2d", i);
for (j = 1; j <= col; j++) {
if (board1[i][j] != '0') {
printf("%c ", board1[i][j]);
}
else if (board1[i][j] == '0' && board2[i][j] == '0') {
printf("□"); // 若还存在未被访问的坐标,继续显示为"□"
}
if (board2[i][j] == '*') {
printf("☆"); // 将原来是雷的地方标成☆,是胜利的标志
}
}
printf("\n");
}
}
排雷成功结果显示:
😥因为小水平有限,只能勉强玩一下,5个雷的情况,不过我觉得问题不大,开发游戏的人未必需要是游戏高手嘛~~
被炸死:
// 用于被炸死的雷盘打印
// board1是用于显示雷盘,board2是存放类的雷盘
void game_over(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int i, j;
printf(" |**********(>_<)*********|\n");
printf(" |***很遗憾,你被炸死了!***|\n");
printf(" |************************|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
printf(" ");
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf(" %2d", i);
for (j = 1; j <= col; j++) {
if (board1[i][j] != '0') {
printf("%c ", board1[i][j]);
}
else if (board1[i][j] == '0' && board2[i][j] == '0') {
printf("□");
}
if (board2[i][j] == '*') {
printf("%c ", board2[i][j]);
}
}
printf("\n");
}
}
被炸死结果显示:
game函数和main函数
游戏实际运行的逻辑其实实在game函数和main函数里控制的
game函数:
// 游戏
void game() {
// 创建一个棋盘,用于存放布置好的雷
char mineBoard[ROWS][COLS];
// 创建一个棋盘,用于显示排查出的雷的信息
char showBoard[ROWS][COLS];
// 对棋盘进行初始化
Init(mineBoard, ROWS, COLS, '0');
Init(showBoard, ROWS, COLS, '0');
// 设置雷到棋盘
SetMine(mineBoard, ROW, COL);
// 打印用于显示的棋盘
print(showBoard, ROW, COL);
// 排查雷
FindMine(showBoard, mineBoard, ROW, COL); // 后面的工作都由排雷函数来完成
}
main函数:
int main() {
int input;
int i = 0;
do {
if (i == 0) {
menu();
printf("请选择:");
scanf("%d", &input);
}
else {
char result;
printf("是否再来一局?y/n:");
while (1) {
getchar();
scanf("%c", &result);
if (result == 'y') {
input = 1;
printf("新游戏开始!\n");
break;
}
else if (result == 'n') {
input = 0;
break;
}
else {
printf("输入有误,请重新输入……\n");
}
}
}
switch (input) {
case 1:
printf("游戏开始!\n");
game(); // game函数,游戏的底层逻辑
break;
case 0:
printf("已退出游戏……\n");
break;
default :
printf("输入有误,请重新输入\n");
}
i++;
} while (input);
return 0;
}
模块化代码展示
game.h:
#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 Init(char board[ROWS][COLS], int rows, int cols, char set);
// 设置雷到棋盘
void SetMine(char board[ROWS][COLS], int row, int col);
// 打印棋盘
void print(char board[ROWS][COLS], int row, int col);
// 排查雷
// board1是用于显示的棋盘,board2是用于存放雷的棋盘
void FindMine(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col);
// 统计坐标周围雷的个数
// board1是用于显示的棋盘 ,board2是用于存放雷的棋盘
void count_mine(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y);
// 判断游戏是否胜出
int isWin(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col);
void game();
game.c:
#include "game2.h"
// 初始化棋盘
void Init(char board[ROWS][COLS], int rows, int cols, char set) {
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
board[i][j] = set;
}
}
}
// 设置雷到雷盘
void SetMine(char board[ROWS][COLS], int row, int col) {
srand((unsigned int)time(NULL));
int count = COUNT;
while (count) {
int x = (rand() % 9) + 1; // 产生一个随机数
int y = (rand() % 9) + 1; // 取余再加1的原因是为了使产生的坐标范围在1 - ROW和1-COL
if (board[x][y] == '0') {
board[x][y] = '*'; // 将雷显示成'*'
count--;
}
}
}
// 打印雷盘
void print(char board[ROWS][COLS], int row, int col) {
int i, j;
printf(" |------------------|\n");
printf(" | (⊙▽⊙) |\n");
printf(" |------------------|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf("%2d", i);
for (j = 1; j <= col; j++) {
if (board[i][j] != '0') {
printf(" %c", board[i][j]);
}
else {
printf("□"); // 若坐标未被访问过就打印一个"□",保持神秘……
}
}
printf("\n");
}
}
// 用于被炸死的雷盘打印
// board1是用于显示雷盘,board2是存放类的雷盘
void game_over(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int i, j;
printf(" |**********(>_<)*********|\n");
printf(" |***很遗憾,你被炸死了!***|\n");
printf(" |************************|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
printf(" ");
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf(" %2d", i);
for (j = 1; j <= col; j++) {
if (board1[i][j] != '0') {
printf("%c ", board1[i][j]);
}
else if (board1[i][j] == '0' && board2[i][j] == '0') {
printf("□");
}
if (board2[i][j] == '*') {
printf("%c ", board2[i][j]);
}
}
printf("\n");
}
}
// 用于游戏胜利的雷盘打印
// board1是用于显示雷盘,board2是存放类的雷盘
void game_wins(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int i, j;
printf(" |★★★★★★★(*^O^*)★★★★★★★|\n");
printf(" |★★★恭喜你,排雷成功!太棒了!★★★|\n");
printf(" |★★★★★★★★★★★★★★★★★★|\n");
for (i = 1; i <= row; i++) {
if (i == 1) {
printf(" ");
for (j = 0; j <= col; j++) {
printf("%2d", j);
}
printf("\n");
}
printf(" %2d", i);
for (j = 1; j <= col; j++) {
if (board1[i][j] != '0') {
printf("%c ", board1[i][j]);
}
else if (board1[i][j] == '0' && board2[i][j] == '0') {
printf("□"); // 若还存在未被访问的坐标,继续显示为"□"
}
if (board2[i][j] == '*') {
printf("☆"); // 将原来是雷的地方标成☆,是胜利的标志
}
}
printf("\n");
}
}
// 排查雷
// board1是用于显示的棋盘,board2是用于存放雷的棋盘
void FindMine(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int x, y;
int win = 0;
while (1) {
printf("请输入要排查的坐标>:");
scanf("%d %d", &x, &y);
if ((x < 1 || x > row) || (y < 1 || y > col)) {
printf("坐标输入有误,请重新输入……\n");
continue;
}
if (board1[x][y] != '0' && board2[x][y] != '*') {
printf("此坐标已被排查,请重新输入……\n");
continue;
}
if (board2[x][y] == '*') {
game_over(board1, board2, row, col);
break;
}
count_mine(board1, board2, x, y);
win = isWin(board1, board2, row, col);
if (win == 1) {
game_wins(board1, board2, row, col);
break;
}
print(board1, row, col);
}
}
// 统计坐标周围雷的个数
// board1是用于显示的棋盘,board2是用于存放雷的棋盘
void count_mine(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y) {
int count = 0;
int i, j;
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i == 0 && j == 0) {
continue;
}
if (board2[x + i][y + j] == '*') {
count++;
}
}
}
if (count == 0) {
board1[x][y] = ' '; // 0个雷让它显示为空格,这样看起来清爽一点
}
else {
board1[x][y] = count + '0';
}
// 递归开始
if ((x >= 1 && x <= COL) && (y >= 1 && y <= ROW) && count == 0) { // 一定要在布置雷的区域才递归,不然又会出现数组越界异常
for (i = -1; i <= 1; i++) {
for (j = -1; j <= 1; j++) {
if (i == 0 && j == 0) {
continue;
}
if (board1[x + i][y + j] == '0') { // 一定要是未访问过的坐标才递归,否则必定出现死递归,导致栈溢出
count_mine(board1, board2, x + i, y + j);
}
}
}
}
}
// 判断游戏是否胜出,赢返回1,未赢返回0
int isWin(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col) {
int count = 0;
int i, j;
for (i = 1; i <= row; i++) {
for (j = 1; j <= col; j++) {
if (board2[i][j] == '*') {
continue;
}
if (board1[i][j] != '0') {
count++;
}
}
}
if (count == row * col - COUNT) {
return 1;
}
return 0;
}
// 游戏
void game() {
// 创建一个雷盘,用于存放布置好的雷
char mineBoard[ROWS][COLS];
// 创建一个雷盘,用于显示排查出的雷的信息
char showBoard[ROWS][COLS];
// 对雷盘进行初始化
Init(mineBoard, ROWS, COLS, '0');
Init(showBoard, ROWS, COLS, '0');
// 设置雷到雷盘
SetMine(mineBoard, ROW, COL);
// 打印用于显示的雷盘
print(showBoard, ROW, COL);
// 排查雷
FindMine(showBoard, mineBoard, ROW, COL); // 后面的工作都由排雷函数来完成
}
test.c
#include "game2.h"
// 菜单
void menu() {
printf("<<<<<<<<欢迎来到扫雷游戏>>>>>>>>\n");
printf("<<<<<<<<选择1---开始游戏>>>>>>>>\n");
printf("<<<<<<<<选择0---退出游戏>>>>>>>>\n");
}
int main() {
int input;
int i = 0;
do {
if (i == 0) {
menu();
printf("请选择:");
scanf("%d", &input);
}
else {
char result;
printf("是否再来一局?y/n:");
while (1) {
getchar();
scanf("%c", &result);
if (result == 'y') {
input = 1;
printf("新游戏开始!\n");
break;
}
else if (result == 'n') {
input = 0;
break;
}
else {
printf("输入有误,请重新输入……\n");
}
}
}
switch (input) {
case 1:
printf("游戏开始!\n");
game(); // game函数,游戏的底层逻辑
break;
case 0:
printf("已退出游戏……\n");
break;
default :
printf("输入有误,请重新输入\n");
}
i++;
} while (input);
return 0;
}
后语
好了,今天的扫雷游戏就分享到这里了,如果喜欢的话可以为我点个赞,我是林先生,专注于提高文章的文字水平,再见~