C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析


文章目录

  • 前言
  • 一、问题描述
  • 二、基本框架构思
  • 三、具体实现
    • 1.扫雷接口实现
    • 2.地图初始化
    • 3.设置雷
    • 4.显示界面
    • 5.开始扫雷
    • 6.计算周围雷的数量
    • 7.排查雷
    • 8.空白展开
    • 9.标记雷
    • 10.取消标记
  • 四、结果演示
  • 五、完整代码
  • 总结

前言 扫雷,是一个十分经典的小游戏,相信大家小时候都玩过,在实现过程中你将会有很大的成就感,现在就让我们一起来实现它吧,。

一、问题描述 实现除界面外扫雷游戏的所有功能。包括
实现一个简单的界面
实现排查雷的功能
实现标记雷的功能
实现显示周围雷的数量
实现第一次排查不会遇雷
实现如果周围没有雷则展开一片
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

二、基本框架构思 在开始编写代码之前,我们必须去玩一玩扫雷,熟悉一下它的各种规则和机制,有助于我们形成更加清晰的代码逻辑。同时在写完一个功能后,进行测试并与真实的扫雷功能做对比。为了编写代码,我玩了十余次。
在试玩过后,我们首先想到的便是界面如何展示,雷如何设置。对于二维平面,我们最常用的就是数组,在数组里面存放不同的数据,来表示有无雷。
  1. 假设我们用一个二维数组,用数据0表示没有雷,1表示有雷,但是当我们排查一个点之后需要显示周围雷的数量,假设也为1,那就会产生冲突,同时也不方便统计周围雷的数量。
  2. 还有一个问题数组容易出界,没次访问都需要判断,比较麻烦,容易出错。
解决办法:
  1. 我们使用两个数组,一个表示有无雷,一个展示给用户看,未排查用‘ *
    ’表示,已排查用周围雷的数量表示。为了统一,我们使用字符数组,遇到整数时将其转化为字符存放。
  2. 我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。
如下图:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

接下来让我们来实现主函数,简单菜单
#define _CRT_SECURE_NO_WARNINGS 1void Menu() { printf("********************************\n"); printf("*********1. play********\n"); printf("*********0. exit********\n"); printf("********************************\n"); } int main() { int input = 0; int count = 0; srand((unsigned int)time(NULL)); do {Menu(); //清除缓冲区,第一次不用 if(count!=0) {char ch; while ((ch = getchar()) != EOF && ch != '\n') {; } } count++; printf("请选择:>"); scanf("%d", &input); switch (input) {case 1: //将标记正确的数量重置为0 mark_count = 0; MineSweeper(); break; case 0: printf("退出游戏\n"); break; default: printf("选择错误,重新选择!\n"); break; } } while (input); return 0; }

设置清除缓冲区代码的目的:
防止输入一个有效数字但带有空格,后面又输入一个数字存放在缓冲区,导致下一次直接取缓冲区的数字,不符合本意。
如图:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

加上之后:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

三、具体实现 1.扫雷接口实现 先定义相关宏定义,方面后面修改
#define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include #include #include#define ROW 9//扫雷地图的数组的行 #define COL 9//扫雷地图的数组的行#define ROWS 11//真实数组的行,为了方便查找雷的数量,这样的话不用判断访问数组是否越界 #define COLS 11//真实数组的行,多加一圈#define MINE_COUNT 9 //设置雷的数量//设置全局变量 int find_count; //用于判断是否为第一次排雷,防止第一次被诈 int mark_count; //标记正确的数量

游戏接口,调用各函数实现扫雷。
void MineSweeper() { //为了统一符号,使用字符数组 char mine[ROWS][COLS] = { 0 }; //用于存放雷的数组,0表示没有雷,1表示有雷 char show[ROWS][COLS] = { 0 }; //用于存放打印给用户看的该点周围雷的数量的数组, //默认为*,输入坐标后其内容为周围雷的数量 // !为标记点 //数组初始化 InitBoard(mine, ROWS, COLS, '0'); InitBoard(show, ROWS, COLS, '*'); //设置雷 SetMine(mine, ROW, COL); system("cls"); //调试用,可以查看雷的位置 //DisplayBoard(mine, ROW, COL); //显示数组版面 DisplayBoard(show, ROW, COL); //游戏入口 PlayGame(mine, show, ROW, COL); }

2.地图初始化 因为两个数组最初都是只存放一种字符,’ 0 ‘和’ * ',所有可以直接把字符当作参数传入,这样就可以通用一个函数了。
//数组初始化 void InitBoard(char board[][COLS], int row, int col, char ch) { for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {board[i][j] = ch; } } }

3.设置雷 使用随机函数设置雷,即将mine数组的部分随机设置成’ 1 ',需要注意的是,我们只针对数组内部真正有效的部分,最外一圈不管,所以遍历是从1开始。
//设置雷 void SetMine(char board[][COLS], int row, int col) { int count = MINE_COUNT; while (count) {//随机获得雷的坐标 int x = rand() % row + 1; //从1到row-1 int y = rand() % col + 1; //判断是否已经是雷 if (board[x][y] != '1') {//不是雷,就放 board[x][y] = '1'; count--; } } }

4.显示界面 打印传入的数组,并打印行号,注意只打印数组内部,遍历从1开始。
如果做了标记用’ ! '表示。
//显示数组版面 void DisplayBoard(char board[][COLS], int row, int col) { printf("------------------------\n"); printf(""); for (int i = 1; i <= col; i++) {printf("%2d", i); } printf("\n"); for (int i = 1; i <= row; i++) {printf("%2d", i); for (int j = 1; j <= col; j++) {printf(" %c", board[i][j]); } printf("\n"); } printf("------------------------\n"); }

5.开始扫雷 排查过的数量等于不是雷的数量 或者 标记正确雷的数量等于设置雷的数量结束,排雷成功。
每次选择执行完后打印显示(show)数组。
//游戏入口,选择排查或者标记 void PlayGame(char mine[][COLS], char show[][COLS], int row, int col) { int win_count = 0; //排查过的数量 //排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功 while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT) {printf("################################\n"); printf("#########1. 排查雷########\n"); printf("#########2. 标记雷########\n"); printf("#########3. 取消标记 ########\n"); printf("################################\n"); int choice; printf("请选择:>"); //清除缓冲区 char ch; while ((ch = getchar()) != EOF && ch != '\n') {; } scanf("%d", &choice); if (choice != 1 && choice != 2 && choice != 3) {printf("输入错误,请重新输入\n"); //跳过本次循环 continue; } if (choice == 1) {//排查雷 int judge = FindMine(mine, show, row, col, &win_count); if (!judge) {//被雷炸死,打印藏雷的数组,并结束 DisplayBoard(mine, row, col); return; } } else {if (choice == 2) {//标记雷 MarkMine(mine, show, row, col); system("cls"); DisplayBoard(show, row, col); } else {//取消标记雷 CancelMark(mine, show, row, col); system("cls"); DisplayBoard(show, row, col); } } } if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT) {system("cls"); printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); return; } }

6.计算周围雷的数量 统计八个方向,上、下、左、右、左上,左下,右上,右下为雷的数量,因为存放的是字符,所有相加后得减去’ 0 ',即可得到整数。
//查找周围雷的数量 int GetMineCount(char mine[][COLS], int x, int y) { return (mine[x - 1][y] + mine[x + 1][y] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] + mine[x + 1][y + 1] - 8 * '0'); }

7.排查雷 排查雷需要设置第一次不被炸死,同时如果被标记则不能再排查。当被炸死或者输入坐标错误则返回。
//排查雷 //返回0代表结束,返回1代表继续 int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin) { int x, y; printf("请输入想要排查的坐标:>"); scanf("%d %d", &x, &y); //如果是第一次,重新设置后如果还是雷则继续循环 while (find_count == 0) {if (mine[x][y] == '1')//是雷 {//现将mine数组置空,即初始化 InitBoard(mine, row, col, '0'); //重新布雷 SetMine(mine, row, col); } else {break; } } if (x >= 1 && x <= row && y >= 1 && y <= col) {if (mine[x][y] == '0')//不是雷 {//如果已经标记,则不能排查 if (show[x][y] == '!') {printf("该点已经被标记,请重新输入\n"); return 1; } system("cls"); SpreadBlank(mine, show, x, y, pwin); DisplayBoard(show, row, col); } else {printf("很遗憾,你被炸死了!\n"); return 0; } } else {printf("输入坐标错误,请重新输入\n"); return 1; } }

标记后不能再被排查:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

第一次不会被炸死:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

选择之后:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

8.空白展开 目的:如果周围没有雷则继续展开(递归),遇到雷停止
这是较难实现的一个函数,需要使用递归实现,而使用递归就需要确定递归的终止条件,这里有三个

  1. 对最外层一圈不做计算,直接返回,这就是多设置一圈的好处
  2. 如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出。
  3. 如果周围有雷就停止。
//如果周围没有雷,则全部展开 //展开空白区域 void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin) { //对最外层一圈不做计算,直接返回,这就是多设置一圈的好处 if (x==0||y==0||x==ROWS-1||y==COLS-1) return; //如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出 if (show[x][y] != '*') return; int count = GetMineCount(mine, x, y); if (count > 0) {show[x][y] = count + '0'; //增加排查数量 (*pwin)++; return ; } else {//八个方向,上、下、左、右、左上,左下,右上,右下show[x][y] = '0'; //增加排查数量 (*pwin)++; SpreadBlank(mine, show, x - 1, y, pwin); SpreadBlank(mine, show, x + 1, y, pwin); SpreadBlank(mine, show, x, y - 1, pwin); SpreadBlank(mine, show, x, y + 1, pwin); SpreadBlank(mine, show, x - 1, y - 1, pwin); SpreadBlank(mine, show, x + 1, y - 1, pwin); SpreadBlank(mine, show, x - 1, y + 1, pwin); SpreadBlank(mine, show, x + 1, y + 1, pwin); } }

9.标记雷 用’ ! '为标记符号。如果正确标记雷点,则标记正确数+1
//标记雷点 void MarkMine(char mine[][COLS], char show[][COLS], int row, int col) { int x; int y; printf("请输入想要标记的坐标:>"); scanf("%d%d", &x, &y); //该点需未被探查,即在show数组为‘*’的点 if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*') {if (mine[x][y] == '1') {//正确标记雷点 mark_count++; } show[x][y] = '!'; } else {printf("输入坐标错误,请重新输入\n"); }}

10.取消标记 与标记类似,只需把标记点改为’ * '即可。如果该点本是正确标记雷点,则标记正确数-1
//取消标记雷点 void CancelMark(char mine[][COLS], char show[][COLS], int row, int col) { int x; int y; printf("请输入想要取消标记的坐标:>"); scanf("%d%d", &x, &y); //该点需已被标记 if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!') {if (mine[x][y] == '1') {//如果是正确标记雷点,数量减一 mark_count--; } show[x][y] = '*'; } else {printf("输入坐标错误或者不是被标记点,请重新输入\n"); }}

四、结果演示 我们将雷的数量设置为1,同时展示mine数组,让我们知道雷的位置,方便测试。
#define MINE_COUNT 1 //设置雷的数量 DisplayBoard(mine, ROW, COL);

C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

展开演示:
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

全部展开,排雷成功。
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

当然也有下面种情况。
C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析
文章图片

五、完整代码 MineSweeper.h
#define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include #include #include#define ROW 9//扫雷地图的数组的行 #define COL 9//扫雷地图的数组的行#define ROWS 11//真实数组的行,为了方便查找类的数量,这样的话不用判断访问数组是否越界 #define COLS 11//真实数组的行,多加一圈#define MINE_COUNT 1 //设置雷的数量int find_count; //用于判断是否为第一次排雷,防止第一次被诈 int mark_count; //标记正确的数量//数组初始化 void InitBoard(char board[][COLS], int row, int col, char ch); //显示数组版面 void DisplayBoard(char board[][COLS], int row, int col); //设置雷 void SetMine(char board[][COLS], int row, int col); //游戏入口,选择排查或者标记 void PlayGame(char mine[][COLS], char show[][COLS], int row, int col); //排查雷 //返回0代表结束,返回1代表继续 int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin); //查找周围雷的数量 int GetMineCount(char mine[][COLS], int x, int y); //如果周围没有雷,则全部展开 //展开空白区域 void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin); //标记雷点 void MarkMine(char mine[][COLS], char show[][COLS], int row, int col); //取消标记雷点 void CancelMark(char mine[][COLS], char show[][COLS], int row, int col);

MineSweeper.c
#pragma once#include"MineSweeper.h"//数组初始化 void InitBoard(char board[][COLS], int row, int col, char ch) { for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {board[i][j] = ch; } } }//显示数组版面 void DisplayBoard(char board[][COLS], int row, int col) { printf("------------------------\n"); printf(""); for (int i = 1; i <= col; i++) {printf("%2d", i); } printf("\n"); for (int i = 1; i <= row; i++) {printf("%2d", i); for (int j = 1; j <= col; j++) {printf(" %c", board[i][j]); } printf("\n"); } printf("------------------------\n"); }//设置雷 void SetMine(char board[][COLS], int row, int col) { int count = MINE_COUNT; while (count) {//随机获得雷的坐标 int x = rand() % row + 1; int y = rand() % col + 1; //判断是否已经是雷 if (board[x][y] != '1') {//不是雷,就放 board[x][y] = '1'; count--; } } }//游戏入口,选择排查或者标记 void PlayGame(char mine[][COLS], char show[][COLS], int row, int col) { int win_count = 0; //排查过的数量 //排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功 while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT) {printf("################################\n"); printf("#########1. 排查雷########\n"); printf("#########2. 标记雷########\n"); printf("#########3. 取消标记 ########\n"); printf("################################\n"); int choice; printf("请选择:>"); //清除缓冲区 char ch; while ((ch = getchar()) != EOF && ch != '\n') {; } scanf("%d", &choice); if (choice != 1 && choice != 2 && choice != 3) {printf("输入错误,请重新输入\n"); //跳过本次循环 continue; } if (choice == 1) {//排查雷 int judge = FindMine(mine, show, row, col, &win_count); if (!judge) {//被雷炸死,打印藏雷的数组,并结束 DisplayBoard(mine, row, col); return; } } else {if (choice == 2) {//标记雷 MarkMine(mine, show, row, col); system("cls"); DisplayBoard(show, row, col); } else {//取消标记雷 CancelMark(mine, show, row, col); system("cls"); DisplayBoard(show, row, col); } } } if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT) {system("cls"); printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); return; } }//排查雷 //返回0代表结束,返回1代表继续 int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin) { int x, y; printf("请输入想要排查的坐标:>"); scanf("%d %d", &x, &y); //如果是第一次 while (find_count == 0) {if (mine[x][y] == '1')//是雷 {//现将mine数组置空,即初始化 InitBoard(mine, row, col, '0'); //重新布雷 SetMine(mine, row, col); } else {break; } } if (x >= 1 && x <= row && y >= 1 && y <= col) {if (mine[x][y] == '0')//不是雷 {//如果已经标记,则不能排查 if (show[x][y] == '!') {printf("该点已经被标记,请重新输入\n"); return 1; }//int count = GetMineCount(mine, x, y); //show[x][y] = count + '0'; system("cls"); SpreadBlank(mine, show, x, y, pwin); DisplayBoard(show, row, col); //win++; } else {printf("很遗憾,你被炸死了!\n"); return 0; } } else {printf("输入坐标错误,请重新输入\n"); return 1; } }//查找周围雷的数量 int GetMineCount(char mine[][COLS], int x, int y) { return (mine[x - 1][y] + mine[x + 1][y] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] + mine[x + 1][y + 1] - 8 * '0'); }//如果周围没有雷,则全部展开 //展开空白区域 void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin) { //对最外层一圈不做计算 if (x==0||y==0||x==ROWS-1||y==COLS-1) return; //如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出 if (show[x][y] != '*') return; int count = GetMineCount(mine, x, y); if (count > 0) {show[x][y] = count + '0'; //增加排查数量 (*pwin)++; return ; } else {//mine[x][y] = '2'; //八个方向,上、下、左、右、左上,左下,右上,右下show[x][y] = '0'; //增加排查数量 (*pwin)++; SpreadBlank(mine, show, x - 1, y, pwin); SpreadBlank(mine, show, x + 1, y, pwin); SpreadBlank(mine, show, x, y - 1, pwin); SpreadBlank(mine, show, x, y + 1, pwin); SpreadBlank(mine, show, x - 1, y - 1, pwin); SpreadBlank(mine, show, x + 1, y - 1, pwin); SpreadBlank(mine, show, x - 1, y + 1, pwin); SpreadBlank(mine, show, x + 1, y + 1, pwin); } }//标记雷点 void MarkMine(char mine[][COLS], char show[][COLS], int row, int col) { int x; int y; printf("请输入想要标记的坐标:>"); scanf("%d%d", &x, &y); //该点需未被探查,即在show数组为‘*’的点 if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*') {if (mine[x][y] == '1') {//正确标记雷点 mark_count++; } show[x][y] = '!'; } else {printf("输入坐标错误,请重新输入\n"); }}//取消标记雷点 void CancelMark(char mine[][COLS], char show[][COLS], int row, int col) { int x; int y; printf("请输入想要取消标记的坐标:>"); scanf("%d%d", &x, &y); //该点需已被标记 if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!') {if (mine[x][y] == '1') {//如果是正确标记雷点,数量减一 mark_count--; } show[x][y] = '*'; } else {printf("输入坐标错误或者不是被标记点,请重新输入\n"); }}

【C语言复习总结|【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析】MineSweeperTest.c
#define _CRT_SECURE_NO_WARNINGS 1#include"MineSweeper.h"void Menu() { printf("********************************\n"); printf("*********1. play********\n"); printf("*********0. exit********\n"); printf("********************************\n"); }void MineSweeper() { //为了统一符号,使用字符数组 char mine[ROWS][COLS] = { 0 }; //用于存放雷的数组,0表示没有雷,1表示有雷 char show[ROWS][COLS] = { 0 }; //用于存放打印给用户看的该点周围雷的数量的数组, //默认为*,输入坐标后其内容为周围雷的数量 // !为标记点 //数组初始化 InitBoard(mine, ROWS, COLS, '0'); InitBoard(show, ROWS, COLS, '*'); //设置雷 SetMine(mine, ROW, COL); system("cls"); //调试用 DisplayBoard(mine, ROW, COL); //显示数组版面 DisplayBoard(show, ROW, COL); //游戏入口 PlayGame(mine, show, ROW, COL); }int main() { int input = 0; int count = 0; srand((unsigned int)time(NULL)); do {Menu(); //清除缓冲区,第一次不用 if(count!=0) {char ch; while ((ch = getchar()) != EOF && ch != '\n') {; } } count++; printf("请选择:>"); scanf("%d", &input); switch (input) {case 1: //将标记正确的数量重置为0 mark_count = 0; MineSweeper(); break; case 0: printf("退出游戏\n"); break; default: printf("选择错误,重新选择!\n"); break; } } while (input); return 0; }

总结 对扫雷的完善,将其除界面外全部功能实现,其中最难的一点是展开空白区域,因为涉及递归,并且在数组受限条件较多,需要多次调试。如有错误,欢迎大佬指正。

    推荐阅读