【C/C++】题解:扫雷游戏的实现
摘要:
- 本篇文章将会实现一个扫雷游戏,从题目需求入手分析需要的数据结构以及程序流程,再通过分析编写程序,最后完成总结;
- 本题的目的在于可以在完成本题的过程中,掌握以往学习过的C语言知识点,对基础知识的完成整合并掌握对零散知识完成综合运用的能力;
- 阅读完这篇文章后,更希望读者也可以在无参考的环境下,完成思考并且实现该程序,最后还可以分享更多的新想法,这样会让每个人更有收获;
文章目录
一. 说明与分析
-
题目要求
我们可以观察一下普通的游戏功能,首先它是通过一个简单的二维结构来描述雷区的,图片如下:
在对某个点进行选择之后,根据规则,如果该点附近有雷,该点就会显示出附近的雷数,如果该点附近没有地雷,就会往四周寻找有雷 的点,直到找不到为止,图片如下:
当全部雷找出后,就会提示游戏胜利,如果踩雷的话就会使得游戏结束,图片如下:
-
程序分析
从数据结构进行分析,可以看出,棋盘是一个二维的结构,所以我们可以通过一个二维数组来储存雷区情况,但是如果只使用一个数组来储存雷区结构的话,不好进行展示,所以我们就采取两个二位数组来完成存储雷区信息以及完成展示。
再对这两个数组进行说明:首先我们把储存雷区信息的数组名为Mine_area,通过 1 表示地雷,通过 0 表示安全区域,通过 1 与 0 来表示地雷是否存在,主要原因是便于统计附近地雷的个数,在后面将会提到,最后还需要通过 # 来表示已经扫过该点状态。其次,我们把展示雷区信息的数组名为Mine_show,其中储存的数字表示了,该点附近雷区中的数量。
再从程序的流程进行分析,首先为了更好的引导使用者,将会先打印一个目录作为引导,选择相应的选项——进入游戏、退出程序。为了可以持续的进行游戏,我们在外加一个循环结构。进入有游戏后,就可以打印棋盘并对游戏进行展开,但踩雷或者是扫出全部地雷就可以退出游戏。根据以上思路,我们可以绘制流程图:
在对游戏过程进行细化处理中,进入游戏后,首先要进行的就是对两个数组的初始化,为了使得游戏可以动态自定义游戏的规模和雷数,我们采用二维指针的方式来记录两个数组中的内容。当然在初始化之前,我们需要进行输入自定义的内容。建立数据结构之后,就可以正式开始游戏了,游戏的流程便是输入坐标,然后对其判断是否为雷,如果是则游戏结束,否则就进行修改Mine_showd的数据进行雷区展示,这是一个循环的过程,退出的条件就是踩雷或者是扫出全部的雷。但游戏进行结束后,则可以打印出雷区的真实状况。根据分析,我们绘制一下流程图:
-
二. 程序书写
-
程序主体框架书写
根据流程图,先对一个个小模块下手,再完成组合,首先是对菜单打印的实现,只需直接打印即可:
void menu() { printf("**********菜单**********\n"); printf("***** 1.PLAY *****\n"); printf("***** 0.EXIT *****\n"); printf("***** 请输入选项 *****\n"); printf("************************\n"); }
再根据流程图将其组装,为了更好的实现循环,我们设置了一个变量 choose 作为玩家的选择,初始化为 1 ,通过作为循环条件并进入循环,进入循环后通过输入选择来进入不同子程序,具体代码如下:
int choose = 1; while (choose) { //菜单显示 menu(); scanf("%d", &choose); switch (choose) { case 1: //玩游戏 MineSweepergame(); break; case 0: printf("****** 游戏结束 ******"); break; default: printf("**** 输入错误,请输入正确选项 ====>\n"); break; } }
-
MineSweepergame( )的实现
根据流程图,同样对子模块进行实现,再增添逻辑选项。
首先进行输入自定义条件与开辟空间,在此我们通过简单的设置变量与scanf( )来完成函数,再通过malloc( ) 创建开辟内存空间,其中的细节为将数组范围扩大,这样统计雷数时,就不需要判断边界范围,具体代码如下:
int Row; int Column; int Mine_num; printf("**** 请输入长 ====>\n"); scanf("%d" ,& Column); printf("**** 请输入宽 ====>\n"); scanf("%d" ,&Row); printf("**** 请输入雷数====>\n"); scanf("%d", &Mine_num); char** Mine_area; char** Mine_show; //开辟空间 Mine_area = (char**)malloc(sizeof(char*) * (Row+2)); for (int i = 0; i < Row+2; i++) { Mine_area[i] = (char*)malloc(sizeof(char) * (Column+2)); } Mine_show = (char**)malloc(sizeof(char*) * (Row + 2)); for (int i = 0; i < Row + 2; i++) { Mine_show[i] = (char*)malloc(sizeof(char) * (Column + 2)); }
完成初始化后,再对开辟好的空间进行初始化,初始化的过程就是对两个数组进行遍历,对每个数组都先设置 0 作为初始化,在通过随机数根据雷数完成Mine_area的雷区设置,因此函数的参数就是两个数组以及雷数以及长与宽,代码实现如下:
void init_Area_Show(char** Mine_area, char** Mine_show, int Row, int Column,int Mine_num) { for (int i = 0; i < Row+2; i++) { for (int j = 0; j < Column+2; j++) { Mine_area[i][j] = '0'; Mine_show[i][j] = '0'; } } for (int i = 0; i < Mine_num; ) { int x = (rand() % Row)+1; int y = (rand() % Column)+1; if (Mine_area[x][y] == '0') { Mine_area[x][y] = '1'; i++; } } }
为了使游戏者有更好的体验,我们再此处展示一下雷区,实现对雷区展示的打印,此处打印方式采取双循环,在通过 “|“与”----” 完成对雷区边框的打印,其中的细节是对扫描过的点进行打印(Mine_area [ ] [ ] == #),如果是附近没有雷的点会打印图形(正方形)代码如下:
void display_MineShow(char** Mine_area,char** Mine_show, int Row, int Column) { printf(" \t"); for (int i = 1; i < Column + 1; i++) printf(" %-2d ", i); printf("\n"); for (int i = 1; i < Row + 1; i++) { printf(" %d ==>\t", i); for (int j = 1; j < Column + 1; j++) { if (Mine_area[i][j] == '#') { if (j != Column) if (Mine_show[i][j] == '0') printf(" █ |"); else printf(" %c |", Mine_show[i][j]); else if (Mine_show[i][j] == '0') printf(" █ "); else printf(" %c ", Mine_show[i][j]); } else { if (j != Column) printf(" |"); else printf(" "); } } printf("\n"); if (i != Row) { printf(" \t"); for (int j = 0; j < Column; j++) { printf("----"); } } printf("\n"); } }
与此十分相似就是对雷区的打印,我们只需在有雷的地方打印即可,剩余的空间布局不作过多解释,代码如下:
void display_MineArea(char** Mine_area, int Row, int Column) { printf(" \t"); for (int i = 1; i < Column + 1; i++) printf(" %-2d ", i); printf("\n"); for (int i = 1; i < Row + 1; i++) { printf(" %d ==>\t", i); for (int j = 1; j < Column + 1; j++) { if (j != Column) { if (Mine_area[i][j] == '1') printf(" * |"); else printf(" |"); } else { if (Mine_area[i][j] == '1' ) printf(" * "); else printf(" "); } } printf("\n"); if (i != Row) { printf(" \t"); for (int j = 0; j < Column; j++) { printf("----"); } } printf("\n"); } }
我们继续根据流程图进行分析,我们就可以游戏交互的主体过程,这个过程我们用find_mine( ) 来定义,同时我们通过 win 来表示找雷的个数,但找出的雷的个数为雷区大小 - 雷数的时候就可以退出循环,同时为了设置踩雷的退出循环,我们通过设置 find_mine( ) 的返回类型为 int,表示在踩雷后返回为 0 作为退出循环条件,并打印出失败提示,主体代码如下:
int win = 0; for (win = 0; win < Row * Column - Mine_num;) { int temp; temp = find_mine(Mine_area, Mine_show, Row, Column,&win); if (temp == 0) break; display_MineShow(Mine_area,Mine_show, Row, Column); }
而find_mine( ) 过程为先输入坐标,并判断坐标下的情况,分别判断是否越界,是否扫过该点坐标,最后判断是否踩雷,如果踩雷会打印相应信息,如果符合这些过程,我们就对相应点做出统计,代码如下:
int find_mine(char** Mine_area, char** Mine_show, int Row, int Column,int* win) { int x, y; while (1) { printf("**** 请输入你猜测的坐标:====>\n"); scanf("%d%d", &y, &x); if (x<1 || x>Row || y<1 || y>Column) { printf("**** 输入坐标越界 ****\n"); continue; } if (Mine_area[x][y] == '#') { printf("**** 你已经扫过这个点 ****\n"); continue; } if (Mine_area[x][y] == '1') { printf("**** 对不起,你失败了 ****\n"); return 0; } get_count(Mine_show, Mine_area, x, y, Row, Column,win); break; } return 1; }
而对附近雷数进行统计是最重要的一步,首先通过循环的方式完成对此进行统计,并将其计入到Mine_show中,其中细节要加上’0’,因为这个是char型的指针。不仅如此,由于附近为0的雷数的坐标是无意义的,所以我们通过递归来对附近的点进行探究,直到每个点雷数不为为止,其中代码如下:
void get_count(char** Mine_show, char** Mine_area, int x, int y,int Row,int Column,int *win) { int count = 0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i == 1 && j == 1) continue; if (Mine_area[x - 1 + i][y - 1 + j] == '1') count++; } } Mine_area[x][y] = '#'; Mine_show[x][y] = count + '0'; (*win)++; if (count == 0) { if (x - 1 >= 1 && y - 1 >= 1 && Mine_area[x - 1][y - 1] == '0') get_count(Mine_show, Mine_area, x - 1, y - 1, Row, Column,win); if (x - 1 >= 1 && Mine_area[x - 1][y] == '0') get_count(Mine_show, Mine_area, x - 1, y, Row, Column, win); if(x-1>=1 && y+1 <=Column && Mine_area[x-1][y+1] == '0') get_count(Mine_show, Mine_area, x - 1, y + 1, Row, Column,win); if (y - 1 >= 1 && Mine_area[x][y - 1] == '0') get_count(Mine_show, Mine_area, x, y - 1, Row, Column, win); if (y + 1 <= Column && Mine_area[x][y + 1] == '0') get_count(Mine_show, Mine_area, x, y + 1, Row, Column, win); if (x + 1 <= Row && y - 1 >= 1 && Mine_area[x + 1][y - 1]=='0') get_count(Mine_show, Mine_area, x + 1, y - 1, Row, Column, win); if (x + 1 <= Row && Mine_area[x + 1][y] == '0') get_count(Mine_show, Mine_area, x + 1, y, Row, Column, win); if (x + 1 <= Row && y+1 <= Column && Mine_area[x + 1][y + 1] == '0') get_count(Mine_show, Mine_area, x + 1, y + 1, Row, Column, win); } return; }
三. 框架整合
- 完成每个步骤的编写之后,我们对其进行相应的整合,我们这里设置通过头文件与源文件来对框架进行优化。其中设置 MineSweeper.c 作为主函数运行的源文件;
- 再设置 game.c 与 game.h 来存放游戏的实现过程以及实现定义。
四. 源码展示
// MineSweeper.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
#include<stdlib.h>
void menu() {
printf("**********菜单**********\n");
printf("***** 1.PLAY *****\n");
printf("***** 0.EXIT *****\n");
printf("***** 请输入选项 *****\n");
printf("************************\n");
}
void MineSweepergame() {
int Row;
int Column;
int Mine_num;
printf("**** 请输入长 ====>\n");
scanf("%d" ,& Column);
printf("**** 请输入宽 ====>\n");
scanf("%d" ,&Row);
printf("**** 请输入雷数====>\n");
scanf("%d", &Mine_num);
char** Mine_area;
char** Mine_show;
//开辟空间
Mine_area = (char**)malloc(sizeof(char*) * (Row+2));
for (int i = 0; i < Row+2; i++) {
Mine_area[i] = (char*)malloc(sizeof(char) * (Column+2));
}
Mine_show = (char**)malloc(sizeof(char*) * (Row + 2));
for (int i = 0; i < Row + 2; i++) {
Mine_show[i] = (char*)malloc(sizeof(char) * (Column + 2));
}
//初始化
init_Area_Show(Mine_area, Mine_show, Row, Column, Mine_num);
//display_MineArea(Mine_area, Row, Column);
display_MineShow(Mine_area,Mine_show, Row, Column);
int win = 0;
for (win = 0; win < Row * Column - Mine_num;) {
int temp;
temp = find_mine(Mine_area, Mine_show, Row, Column,&win);
//display_MineArea(Mine_area, Row, Column);
if (temp == 0)
break;
display_MineShow(Mine_area,Mine_show, Row, Column);
}
if (win == Row * Column - Mine_num)
printf("**** 恭喜你,你赢了!****\n");
display_MineArea(Mine_area, Row, Column);
//释放空间
for (int i = 0; i < Row+2; i++) {
free(Mine_area[i]);
free(Mine_show[i]);
}
free(Mine_area);
free(Mine_show);
}
int main() {
//时间种子设置
srand((unsigned int)time(NULL));
//用于记录玩家的选择
int choose = 1;
while (choose)
{
//菜单显示
menu();
scanf("%d", &choose);
switch (choose)
{
case 1:
//玩游戏
MineSweepergame();
break;
case 0:
printf("****** 游戏结束 ******");
break;
default:
printf("**** 输入错误,请输入正确选项 ====>\n");
break;
}
}
return 0;
}
// game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void init_Area_Show(char** Mine_area, char** Mine_show, int Row, int Column,int Mine_num) {
for (int i = 0; i < Row+2; i++) {
for (int j = 0; j < Column+2; j++) {
Mine_area[i][j] = '0';
Mine_show[i][j] = '0';
}
}
for (int i = 0; i < Mine_num; ) {
int x = (rand() % Row)+1;
int y = (rand() % Column)+1;
if (Mine_area[x][y] == '0') {
Mine_area[x][y] = '1';
i++;
}
}
}
void display_MineArea(char** Mine_area, int Row, int Column) {
printf(" \t");
for (int i = 1; i < Column + 1; i++)
printf(" %-2d ", i);
printf("\n");
for (int i = 1; i < Row + 1; i++) {
printf(" %d ==>\t", i);
for (int j = 1; j < Column + 1; j++) {
if (j != Column) {
if (Mine_area[i][j] == '1')
printf(" * |");
else
printf(" |");
}
else {
if (Mine_area[i][j] == '1' )
printf(" * ");
else
printf(" ");
}
}
printf("\n");
if (i != Row) {
printf(" \t");
for (int j = 0; j < Column; j++) {
printf("----");
}
}
printf("\n");
}
}
void display_MineShow(char** Mine_area,char** Mine_show, int Row, int Column) {
printf(" \t");
for (int i = 1; i < Column + 1; i++)
printf(" %-2d ", i);
printf("\n");
for (int i = 1; i < Row + 1; i++) {
printf(" %d ==>\t", i);
for (int j = 1; j < Column + 1; j++) {
if (Mine_area[i][j] == '#') {
if (j != Column)
if (Mine_show[i][j] == '0')
printf(" █ |");
else
printf(" %c |", Mine_show[i][j]);
else
if (Mine_show[i][j] == '0')
printf(" █ ");
else
printf(" %c ", Mine_show[i][j]);
}
else {
if (j != Column)
printf(" |");
else
printf(" ");
}
}
printf("\n");
if (i != Row) {
printf(" \t");
for (int j = 0; j < Column; j++) {
printf("----");
}
}
printf("\n");
}
}
void get_count(char** Mine_show, char** Mine_area, int x, int y,int Row,int Column,int *win) {
int count = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1)
continue;
if (Mine_area[x - 1 + i][y - 1 + j] == '1')
count++;
}
}
Mine_area[x][y] = '#';
Mine_show[x][y] = count + '0';
(*win)++;
if (count == 0) {
if (x - 1 >= 1 && y - 1 >= 1 && Mine_area[x - 1][y - 1] == '0')
get_count(Mine_show, Mine_area, x - 1, y - 1, Row, Column,win);
if (x - 1 >= 1 && Mine_area[x - 1][y] == '0')
get_count(Mine_show, Mine_area, x - 1, y, Row, Column, win);
if(x-1>=1 && y+1 <=Column && Mine_area[x-1][y+1] == '0')
get_count(Mine_show, Mine_area, x - 1, y + 1, Row, Column,win);
if (y - 1 >= 1 && Mine_area[x][y - 1] == '0')
get_count(Mine_show, Mine_area, x, y - 1, Row, Column, win);
if (y + 1 <= Column && Mine_area[x][y + 1] == '0')
get_count(Mine_show, Mine_area, x, y + 1, Row, Column, win);
if (x + 1 <= Row && y - 1 >= 1 && Mine_area[x + 1][y - 1]=='0')
get_count(Mine_show, Mine_area, x + 1, y - 1, Row, Column, win);
if (x + 1 <= Row && Mine_area[x + 1][y] == '0')
get_count(Mine_show, Mine_area, x + 1, y, Row, Column, win);
if (x + 1 <= Row && y+1 <= Column && Mine_area[x + 1][y + 1] == '0')
get_count(Mine_show, Mine_area, x + 1, y + 1, Row, Column, win);
}
return;
}
int find_mine(char** Mine_area, char** Mine_show, int Row, int Column,int* win) {
int x, y;
while (1)
{
printf("**** 请输入你猜测的坐标:====>\n");
scanf("%d%d", &y, &x);
if (x<1 || x>Row || y<1 || y>Column) {
printf("**** 输入坐标越界 ****\n");
continue;
}
if (Mine_area[x][y] == '#') {
printf("**** 你已经扫过这个点 ****\n");
continue;
}
if (Mine_area[x][y] == '1') {
printf("**** 对不起,你失败了 ****\n");
return 0;
}
get_count(Mine_show, Mine_area, x, y, Row, Column,win);
break;
}
return 1;
}
//game.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
void init_Area_Show(char** Mine_area,char** Mine_show,int Row,int Column,int Mine_num);
void display_MineArea(char** Mine_area,int Row,int Column);
void display_MineShow(char** Mine_area, char** Mine_show, int Row, int Column);
int find_mine(char** Mine_area,char** Mine_show,int Row,int Column,int *win);
五. 效果展示
六. 总结
-
扫雷这题目是非常好的综合综合题,覆盖了很全面的知识点,与此同时还蕴含着非常严密的逻辑;
-
扫雷这题不仅要在意完整的框架,还要在意许多细节,比如对于雷区的扩展初始化,以及对于两个数组设置等问题,需要重点关注并思考清楚;
-
文章的实现与大部分题解存在不同的是关于对雷数为零的坐标进行递归,使其可以搜索出无用的坐标,提供有效信息,读者可以在此思考,如有更好的方案,欢迎在评论区中提出;
-
程序不足:其中在编写程序过程中,作者也遇到了些许困难,主要原因是没有将整个框架思考好后进行编写,因此代码的逻辑还存在可以提升的地方,希望有读者能对此升级完成改善分享,万分感谢。
-
最后,希望读者可以在完成学习后,能够自己在无参考的环境下实现该题目,这样会有更大的收获。
声明:
- 作者写博客的C/C++内容主要是想形成一套实用的使用手册,主要追求的方便,以及记录一下自己在重新看C和C++内容的思考,所以常规文章一般都比较追求实用性,深层次内容是不及各位大神所写的博客那样深刻清晰;
- 欢迎各位在评论区中指正指导,非常感谢;
- 更新也已经提上日程,不过因为时间关系,可能无法写出完整完全构建C/C++的内容,因此以后主要会分享一些新颖的、总结性或让自己有收获的内容给大家。
- C/C++的代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!