【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;
}

后语

好了,今天的扫雷游戏就分享到这里了,如果喜欢的话可以为我点个赞,我是林先生,专注于提高文章的文字水平,再见~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林先生-1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值