手把手带你实现C语言扫雷进阶(1)(接上回基础版扫雷,附上源码)

2024-09-01 07:28

本文主要是介绍手把手带你实现C语言扫雷进阶(1)(接上回基础版扫雷,附上源码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一、扫雷进阶留下的问题
  • 二.非雷扩展周围不是雷函数
  • 三、标记雷函数
  • 四.查看排雷总时间
  • 五、扫雷进阶源码及总结

一、扫雷进阶留下的问题

我们先来看看之前讲扫雷基础的时候留下的一些问题:

  • 是否可以选择游戏难度
        ◦ 简单 9 * 9 棋盘,10个雷
        ◦ 中等 16 * 16棋盘,40个雷
        ◦ 困难 30 * 16棋盘,99个雷

  • 如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚

  • 是否可以标记雷

  • 是否可以加上排雷总共耗费的时间

    要注意的一点是选择游戏难度以目前我们讲过的内容还暂时写不出来,在后面学习到相关内容时,我们再进行讲解,接下来就解决后三个问题
    我们的进阶还是在之前写过的基础版之上添加,如果还没有看过之前基础版,可以在我的主页查看,现在我们开始通过我们学过的知识解决这三个进阶需求

二.非雷扩展周围不是雷函数

    这个函数我们的目标就是实现:如果输入的坐标不是雷,那么查看周围是否有雷,如果至少有一个雷,那么就直接显示周围雷的个数,如果没有雷,把那个坐标换位字符’ ',也就是字符空格,而不再显示0,这样更加好看,随后向周围扩展,扩展到该坐标周围至少有一个雷时
    我们需要做一点更改,就是把输入的当前坐标,查看周围有几个雷放入这个函数,方便递归,现在可能有点懵,后面会慢慢解释
    这个函数实现起来有一定难度,必须要悟透递归的使用方法,同时还要注意数组越界等等问题,还要使用一点点超纲内容,就是指针,但是是最简单的指针内容,并且篇幅只有一点点,主要还是要体会循环递归的方式,以及递归的两个必要条件:

  • 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续,如果没有限制,可能会陷入死递归
  • 每次递归调⽤之后越来越接近这个限制条件

接下来我们就进入这个函数的设计:

  1. 函数的命名:我将其命名为exdboard,exd是extend的缩写,含义是扩展,函数名就理解为扩展棋盘,也可自行命名

  2. 函数参数:
    (1)由于需要操作数组show和数组hide,所以要将这两个数组传过去
                                                                                                (2) 由于需要扩展周围的坐标,我们可以直接把当前坐标也一次性涵盖进去,就是先对当前坐标进行操作,查看周围是否有雷,如果没有雷就开始对周围坐标递归,所以我们需要传过去当前玩家输入的坐标,也就是x,y                                                                                           
    (3)然后由于我们要避免函数递归导致数组越界访问,所以我们要把行和列传过去,用来判断递归坐标的合法性                                                                                           
    (4)最后由于我们每递归一次,可能就会扩展一个坐标,而每扩展一次坐标我们需要win++一次,但是由于直接传win过去属于传值调用,在exdboard里面对win进行更改不会对真实的win产生影响,所以我们需要使用指针进行传址调用,这里听不懂没关系,后面会专门详细的为大家介绍指针,现在只需要跟着使用就行,并且在该函数它只有一个作用,那就是每次递归,如果扩展了一个坐标,就对win++一次,所以函数exdboard的最后一个参数是win的地址                                                                                           
    (5)所以函数exdboard的参数有:show和hide数组,玩家输入的坐标x,y,棋盘行和列,win的地址

  3. 函数的声明:我们要将函数设计好后,直接放在函数findboard中,所以可以直接在函数findboard上方进行实现,免去了在game.h里声明

  4. 函数的实现:
    (1)首先我们需要获取玩家输入的坐标位置周围雷的个数,如果没有雷的话,也就是show[x][y]==‘0’时,把这个位置改成字符空格,这样更加好看                                                                                           
    (2)执行完(1)的操作,相当于就是已经排查了一个坐标,我们要win++一次,如果还不了解指针的可以先照抄,学习后再来理解,反正在这里只用了很简单的指针知识,如下:

//指针变量int* ptr
//用来接收win传来的地址
//对它解引用就找到了win
(*ptr)++

    (3)随后我们开始思考该怎么递归,我们可以这样想,想要实现这个函数的功能,我们不能一口气直接完成,需要一步一步来,也就是把大事化小,如果当前坐标周围没有雷,我们可以以当前坐标为中心,进行扩展,一个一个找出周围的坐标,将它们当作新的中心进行扩展,如图:
在这里插入图片描述
    这样我们就将大问题化为了小问题,把扩展中心1周围的雷,化解为找多个中心,扩展它们周围的雷,形成递归,因为解决中心1和解决中心2的扩展是同类问题,方法相似,只要写出解决中心1的扩展,也就解决了其它中心的扩展,难点在于怎么找其它的中心呢?

    (4)我们可以定义一个i和j,表示新的中心的行和列,这时候我们可以用一个循环,找出中心1周围的坐标的行和列,然后将它们作为新的中心进行递归,如下:

for(i=x-1;i<=x+1;i++)
{for(j=y-1;j<=y+1;j++){}
}

    这下我们就找出了坐标为x,y周围的所有坐标,可以将它们当作新的中心

    (5)我们现在开始思考整个递归的模型,我们说过递归一定要有尽头,有限制条件,每进行一次递归就要越来越靠近这个条件,我们可以称为递归的出口,经过思考,我们一定是要坐标周围没有雷,也就是show[x][y]!=‘ ’这个条件,如果这个条件满足,说明周围至少有一个雷,那么我们就可以返回了,不要递归下去了,由于每个中心的周围都只有8个坐标,再加上有雷的存在,所以迟早遇到某一个中心周围有雷,那么递归就开始返回了,不会死递归,如:

if (show[x][y]!=' ')
{return;
}

    (6)接下来思考从哪里开始递归,也就是递归的入口,经过前面的分析,很显而易见,我们要把递归的入口放在刚刚那个循环里面,这样构成了循环递归,将每一个周围的坐标作为中心,向周围扩展,但我们需要注意一些问题

  • 我们的递归不能让数组越界,也就是我们的新中心show[i][j],不能越界,必须满足
    i>=1 && i<=row && j>=1 && j<=col
  • 我们不能重复递归,比如2,4这个坐标已经做过中心了,如果递归递归着,2,4突然又做了中心,就重复递归了,就像使用递归求第n个斐波那契数一样,重复递归太多次导致效率太低,甚至低到我们不能接受,所以我们使用了循环,也就是迭代,那我们怎么避免重复递归呢?我们只需要判断一下show[i][j]是不是字符 *,如果是字符 ,说明这个坐标肯定还没有迭代过,可以放心迭代,所以我们可以再加一个条件show[i][j]=='',然后进入递归,如:
//循环递归:
for (i = x - 1; i <= x + 1; i++)
{for (j = y - 1; j <= y + 1; j++){if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col){ //递归入口:exdboard(show, hide, i, j, row, col, ptr);}		}
}

    (7)经过我们的努力分析,现在我们基本上可以将这个函数构建出来了,参考代码如下:

//查看排查雷的那个坐标周围是否有雷,如果至少有一个雷就直接显示有几个雷
//如果周围没有雷,那么就对周围进行扩展
void exdboard(char show[Rows][Cols], char hide[Rows][Cols], int x,int y ,int row,int col, int* ptr)
{int i = 0;int j = 0;int ret = getcount(hide, x, y);show[x][y] = ret + '0';(*ptr)++;if (show[x][y] == '0'){show[x][y] = ' ';}//递归出口:if (show[x][y]!=' '){return;}else{//循环递归:for (i = x - 1; i <= x + 1; i++){for (j = y - 1; j <= y + 1; j++){if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col){ //递归入口:exdboard(show, hide, i, j, row, col, ptr);}		}}}
}
  1. 函数的使用:我们可以直接放在排查雷函数中,如图:
    在这里插入图片描述

三、标记雷函数

    简单思路就是,每当玩家排查一次雷后,就询问是否要标记雷,如果回答是,那么就开始标记雷,回答否那么就继续排雷
    玩家要标记雷就是认为那个地方肯定不是雷,如果用一个通俗的符号表示否定,很容易就想到使用大写字母X,把它当做叉叉使用,所以实现标记雷就是把那个坐标位置的字符改为大写字符X,听上去就很容易实现,接下来我们仔细分析将其设计为一个函数:

  1. 函数命名:我命名为markmine,mark是标记,做记号的意思,mine有雷的意思,做一个参考,也可自行命名
  2. 函数参数: 由于我们需要更改展示给用户的数组show,所以我们需要把show数组传过去, 由于需要查看用户输入的坐标是否合法,所以要把实际棋盘大小的行和列,也就是Row和Col传过去。所以函数参数有show数组、Row和Col
  3. 函数声明:我们可以思考一下,我们设计的函数markmine,他的作用是玩家排查雷后,然后询问是否标记雷,回答是后,再进行标记,我们可以这样,将函数markmine直接放入函数findboard的最后,然后在findboard上方实现,这样就不用再gam.h中专门声明了
  4. 函数实现:现在我们按点来分析一下总体思路
    (1)在排查完一次雷后,进入函数询问玩家是否标记雷,然后让用户输入一个值,用来给我们判断,输入是就标记雷,输入否就不标记                                                                                           
    (2)由于要比较两个字符串是否相等,不能直接用==,要使用strcmp,并且在game.h声明头文件string.h,如果strcmp返回值是0,那么两个字符串相等,反之就不相等                                                                                           
    (3)有时玩家可能需要标记多个雷,所以我们可以在玩家输入是后再次询问,需要标记几个雷,并且给予提示,还需要标记多少个雷                                                                                           
    (4)我们可以使用变量num来接收玩家输入的数量,然后将其作为while的循环条件,每标记一次雷,num–,到最后num为0时循环就自动结束了                                                                                           
    (5)为了防止用户输入的坐标越界,我们需要对用户输入的坐标进行判断,必须满足棋盘大小                                                                                           
    (6)为了防止用户不小心输入已经排除的位置,比如坐标6,5已经知道不是雷了,用户却不小心输入错误,标记成雷了,我们可以加一个限制条件,就是show[x][y]的位置必须是字符*,也就保证了标记的位置不会是已经排除的位置                                                                                           
    (7)最后一步就很简单了,只需要将用户输入坐标的位置改成大写字母‘X’                                                                                           
    (8)代码如下:
//实现标记雷函数:
void markmine(char show[Rows][Cols], int row, int col)
{int x = 0;int y = 0;int i = 0;int num = 0;char arr[10] = { 0 };printf("\n*****是否标记雷*****:");scanf("%s",arr);if (strcmp(arr, "是") == 0){printf("\n***请输入要标记的雷的个数***:");scanf("%d", &num);while (num){printf("\n请输入要标记的雷的坐标,还需要标记%d个雷:",num);scanf("%d %d", &x, &y);if (show[x][y]=='*' && x >= 1 && x <= row && y >= 1 && y <= col){show[x][y] = 'X';num--;}else{printf("输入不合法,请重新输入!\n");}}}else{printf("\n");}
}
  1. 函数运用:最后运用函数时记得将其放在排查雷函数后,如图:
    在这里插入图片描述

四.查看排雷总时间

    从原理上这个问题很好解决,就是游戏结束的时间减去游戏开始的时间,要解决这样一个问题我们就要重新回顾一下time函数
    当time函数的参数为NULL时,它会返回一个时间戳,就是从1970年1月1日0时0分0秒到现在这个时间有多少秒,返回类型是time_t,本质上是一个32位或者64位的整型,可以使用占位符%td打印它
    如果我们要算游戏总共花费多少时间,我们可以直接在游戏开始时,在game函数最上面创建一个变量start来接收游戏开始时,在game函数最下面,也就是游戏结束时创建一个变量end来接收结束时的时间戳,然后将它们相减即可
    为了方便,我们可以显示精确到秒的时间,同时显示大约多少分钟,分钟就用秒数除以60即可,代码如下:

void game()
{//游戏开始的时间戳time_t start = time(NULL);//不显示出来,用于布置雷char hide[Rows][Cols];//显示出来,用于排查雷char show[Rows][Cols];//初始化棋盘:initboard(hide, Rows, Cols, '0');initboard(show, Rows, Cols, '*');//布置雷:setboard(hide, Count);//打印棋盘:printboard(show, Row, Col);printboard(hide, Row, Col);//排查雷:findboard(show, hide, Row, Col, Count);//游戏结束时的时间戳time_t end = time(NULL);printf("共花费%td秒,大约%td分钟\n\n",end-start,(end-start)/60);
}

五、扫雷进阶源码及总结

game.h

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>//显示的棋盘大小
#define Row 9
#define Col 9//实际的棋盘大小
#define Rows Row+2
#define Cols Col+2//雷的个数:
#define Count 10//声明初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x);//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);//声明布置雷函数:
void setboard(char board[Rows][Cols], int count);//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row, int col, int count);

game.c

#define _CRT_SECURE_NO_WARNINGS#define _CRT_SECURE_NO_WARNINGS#include "game.h"//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{int i = 0;int j = 0;for (i = 0; i < rows; i++){for (j = 0; j < cols; j++){board[i][j] = x;}}
}//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{int i = 1;int j = 1;//打印棋盘标志,让棋盘更显眼printf("------ 扫雷 -------\n");//打印列号:for (i = 0; i <= col; i++){printf("%d ", i);}//列号打印完进行换行printf("\n");for (i = 1; i <= row; i++){//打印行号printf("%d ", i);for (j = 1; j <= col; j++){printf("%c ", board[i][j]);}//每打印完一行就要进行一次换行:printf("\n");}//打印棋盘标志,让棋盘更显眼printf("------ 扫雷 -------\n");//棋盘打印完之后进行换行printf("\n");
}//实现布置雷函数:
void setboard(char board[Rows][Cols], int count)
{while (count){int x = rand() % Row + 1;int y = rand() % Col + 1;if (board[x][y] != '1'){board[x][y] = '1';count--;}}
}//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{return (hide[x][y - 1] + hide[x][y + 1] +hide[x - 1][y] + hide[x - 1][y - 1] + hide[x - 1][y + 1] +hide[x + 1][y] + hide[x + 1][y - 1] + hide[x + 1][y + 1] - 8 * '0');
}//查看排查雷的那个坐标周围是否有雷,如果至少有一个雷就直接显示有几个雷
//如果周围没有雷,那么就对周围进行扩展
void exdboard(char show[Rows][Cols], char hide[Rows][Cols], int x,int y ,int row,int col, int* ptr)
{int i = 0;int j = 0;int ret = getcount(hide, x, y);show[x][y] = ret + '0';(*ptr)++;if (show[x][y] == '0'){show[x][y] = ' ';}//递归出口:if (show[x][y]!=' '){return;}else{//循环递归:for (i = x - 1; i <= x + 1; i++){for (j = y - 1; j <= y + 1; j++){if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col){ //递归入口:exdboard(show, hide, i, j, row, col, ptr);}		}}}}//实现标记雷函数:
void markmine(char show[Rows][Cols], int row, int col)
{int x = 0;int y = 0;int i = 0;int num = 0;char arr[10] = { 0 };printf("\n*****是否标记雷*****:");scanf("%s",arr);if (strcmp(arr, "是") == 0){printf("\n***请输入要标记的雷的个数***:");scanf("%d", &num);while (num){printf("\n请输入要标记的雷的坐标,还需要标记%d个雷:",num);scanf("%d %d", &x, &y);if (show[x][y]=='*' && x >= 1 && x <= row && y >= 1 && y <= col){show[x][y] = 'X';num--;}else{printf("输入不合法,请重新输入!\n");}}}else{printf("\n");}
}//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row, int col, int count)
{int x = 0;int y = 0;int win = 0;printf("请输入英文逗号隔开坐标!\n\n");while (win < Row * Col - Count){printf("请输入要排查的坐标:");//这里需要用到之前学过的%*c,就是//scanf的赋值忽略符*//使用过后就可以输入空格或者英文逗号隔开坐标,最好英文逗号//但是不能用中文//这里中文和英文逗号不能都使用//所以我们最好在开始时给予一些提示scanf("%d%*c%d", &x, &y);printf("\n");if (x >= 1 && x <= row && y >= 1 && y <= col){if (hide[x][y] == '1'){printf("很遗憾,你踩到雷了,游戏失败!\n\n");printf("显示的0代表不是雷,1表示雷\n\n");printboard(hide, Row, Col);break;}else{//扩展棋盘:exdboard(show, hide,x,y, row, col, &win);printboard(show, Row, Col);//标记雷markmine(show, row, col);printboard(show, Row, Col);}}else{printf("输入不合法,请重新输入!\n");}}if (win == Row * Col - Count){printf("恭喜你,扫雷成功!\n\n");printf("显示的0代表不是雷,1表示雷\n\n");printboard(hide, Row, Col);}
}

test.c

#define _CRT_SECURE_NO_WARNINGS#define _CRT_SECURE_NO_WARNINGS#include "game.h"void menu()
{printf("********************\n");printf("**** 1.开始游戏 ****\n");printf("**** 0.退出游戏 ****\n");printf("********************\n\n");printf("提示:请输入1或0\n");
}void game()
{//游戏开始的时间戳time_t start = time(NULL);//不显示出来,用于布置雷char hide[Rows][Cols];//显示出来,用于排查雷char show[Rows][Cols];//初始化棋盘:initboard(hide, Rows, Cols, '0');initboard(show, Rows, Cols, '*');//布置雷:setboard(hide, Count);//打印棋盘:printboard(show, Row, Col);//printboard(hide, Row, Col);//排查雷:findboard(show, hide, Row, Col, Count);//游戏结束时的时间戳time_t end = time(NULL);printf("共花费%td秒,大约%td分钟\n\n",end-start,(end-start)/60);
}int main()
{srand((unsigned int)time(NULL));int input = 0;do{menu();printf("\n请选择:");scanf("%d", &input);switch (input){case 1:game();break;case 0:printf("游戏退出成功\n");break;default:printf("\n选择错误,请重新输入!\n\n");break;}} while (input);return 0;
}

    本文内容偏难,如果有疑问欢迎在评论区提问,一定会及时答复
    对于扫雷进阶(2)也就是最后一点内容:选择游戏难度,我会在后面讲到相关知识点后出一篇文,希望不要被当前这个扫雷进阶(1)难到而放弃,继续往后面学习,你就会发现它很简单,在扫雷进阶(2)的时候是否会觉得扫雷进阶(1)很简单呢?我们拭目以待吧!

这篇关于手把手带你实现C语言扫雷进阶(1)(接上回基础版扫雷,附上源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1126305

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]