扫雷——完整版!!!!!!

今天来分享一下写的扫雷小游戏,其中包括了 展开一片的递归操作,标记,和取消标记。

除了操作界面不一样之外,逻辑和平时玩的扫雷基本是一样的。


游戏规则

  1. 给定一个棋盘,期棋盘中有若干个雷,玩家需要通过分析,把所有不是雷的格子给排查,也就是翻开

  1. 操作

1)点击格子进行排雷

排雷后的格子有两种状态

  • 该位置是雷,游戏结束。

  • 该位置不是雷,显示该位置周围八个坐标中雷的个数,如果周围八个坐标没有雷,则这里显示空格。

2)右击格子进行标记

标记分为两种

  • 玩家确定这个位置是雷,给上小红旗。

  • 玩家不确定这个位置是雷,给上一个问号。

  1. 胜利条件

玩家通过排雷和标记的配合,结合排雷后的格子上提示的信息,将棋盘中所有不是雷的格子都排查完毕。


游戏实现

棋盘

棋牌作用

  • 埋雷

  • 显示排雷情况

看一下实际的效果:

游戏中的扫雷棋盘

模拟的扫雷棋盘

埋雷的棋盘

扫雷棋盘,就是可以看成一个矩阵,这里面的是9*9的一个矩阵,所以我们要用二维数组来模拟。

规定:

  • 棋盘可操作的区域是9*9的二维字符数组

  • 棋盘有两个,一个用来埋雷,一个用来显示排查雷的情况

  • 埋雷的棋盘,‘1’代表该位置有雷,‘0’代表该位置没雷

  • 实际的棋盘要多出两行两列

解释

首先,我们先回忆一下,排雷的规则,给出一个坐标,如果该位置不是雷,对其周围八个格子雷的数量进行统计,显示在该位置。

但问题是,一定是8个吗,显然是不一定的,对于边上的坐标,理应排查五个位置,角上的坐标,理应排查三个位置,但是这样考虑,是不是显得很麻烦,因为大部分坐标排查的都是8个位置,所以我们做一下处理,增加两行两列,这些位置不埋雷,也就是雷的个数是 0,但这些位置需要可以访问,所以实际数组的大小为,11*11,如图所示:

如图,中间蓝色9*9的区域,是我们要排雷的区域,这些区域埋的有雷,而外层橙色的区域是多加的两行两列,这些区域没有雷,方便我们对特殊位置的排雷,保证每个位置都是对周围8个位置进行统计雷的个数。

为何用两个棋盘

你想,按照目前的规定,假如只用一个棋盘,我们对一个位置排雷了,并将这个位置周围雷的信息显示到该位置,比如是3个雷,那么如果,我们排查其它位置的时候,如果也需要排查这个位置,但我们用的是‘0’或‘1’进行判断是否为雷,但这个‘3’是不是就影响我们的判断了,而且处理起来也比较麻烦。

所以,我们干脆使用两个棋盘,它们的大小是一模一样的,排雷使用的是埋的有雷的棋盘,之后,我们将信息显示到另一个棋盘对应的位置,之后只显示该棋盘,这个问题就解决了。

埋雷的棋盘

显示信息的棋盘


辅助函数

接下来给出一些变量和常量的定义

#define rows 9//可操作的行
#define cols 9//可操作的列
#define ROWS rows+2//实际的行
#define COLS cols+2//实际的列
#define MINES 10//雷的个数
char board[ROWS][COLS] = { 0 };//埋雷 排雷的棋盘
char show[ROWS][COLS] = { 0 };//打印效果的棋盘

上面这些变量,是在函数里面要传的参数。

下面三个函数是这个游戏中要一直用的函数,先介绍一下:

    • 初始化函数

两个棋盘一个埋雷,一个显示,规定,埋雷的棋盘全部初始化为字符‘0’,而显示的棋盘,全部初始化为‘*’。

注意,这里需要传实际的行和列,尤其是对于埋雷的棋盘,那多加的两行两列要初始化为‘0’。

void InitBoard(char board[ROWS][COLS], int row, int col,char cur)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            board[i][j] = cur;
        }
    }
}

这个函数比较简单,比较巧的一点是,两个棋盘大小一样,但需要初始化的值不一样,那么就把需要的初始化值作为参数 cur传进来,初始化就好了。

    • 打印棋盘

我们打印棋盘肯定要打印的是中间那块能操作的区域,也就是9*9那块,所以干脆就传这个行和列。

对于这个打印我们提供了两个函数,其实也就是加上了边框,看一下效果

没加边框打印

加边框打印

代码如下,自取哦~

没加边框打印:

void ShowBoard(char board[ROWS][COLS], int row, int col)
{
    printf("----------扫雷游戏----------\n");
    for (int i = 0; i <= col; i++)
    {
        printf(" %d ", i);
    }
    printf("\n");
    for (int i = 1; i <= row; i++)
    {
        printf(" %d ", i);
        for (int j = 1; j <= col; j++)
        {
            printf(" %c ", board[i][j]);
        }
        printf("\n");
    }
}

加边框打印:

void ShowBoard(char board[ROWS][COLS], int row, int col)
{
    printf("----------扫雷游戏----------\n");
    for (int i = 0; i <= col; i++)
    {
        if (i == 0)
            printf(" %d ",i);
        else
        {
    printf(" %d  ", i);
        }
        
    }
    printf("\n");
    for (int i = 0; i <= col; i++)
    {
        if (i == 0)
        {
            printf("  |");
        }
        else
        {
            printf("---|");
        }
    }
    printf("\n");
    for (int i = 1; i <= row; i++)
    {
        printf(" %d|", i);
        for (int j = 1; j <= col; j++)
        {
            printf(" %c |", board[i][j]);
        }
        printf("\n");
        for (int k = 1; k <= row; k++)
        {
            if (k == 1)
            {
                printf("  ");
            }
            if(k==1)
            printf("|---|");
            else
            {
                printf("---|");
            }
        }
        printf("\n");
    }
}

    • 埋雷

埋雷就是生成一个坐标,只要这个位置还没雷,就在这个位置埋一个雷。

void Setmine(char board[ROWS][COLS], int row, int col)
{
    //坐标
    int x = 0;
    int y = 0;
    int mines = MINES;//10个雷
    while (mines)
    {
        //生成1到9的坐标
        x = rand() % 9 + 1;
        y = rand() % 9 + 1;
        if (board[x][y] == '0')
        {
            board[x][y] = '1';
            mines--;
        }
    }
}

    • 统计雷

写统计雷之前,我们先复习一个小的知识点,就是0到9的数字它对应的字符的ASCII码值和这个数字的关系。

字符零‘0’它的ASCII值是48,‘1’的ASCII值是49,‘2’的ASCII值是50,后边的依次类推。

那么通过字符‘1’如何得到整数1,是不是就用49减去48,就得到整数1了,其它的数字也是同样的道理,因为对字符进行算数运算,操作的还是它的ASCII值。

代码如下:

int MineCount(char board[ROWS][COLS], int x, int y)
{
    return  board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] +
            board[x][y - 1] +                          +board[x][y + 1] +
            board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1]
        - 8 * '0';
}

看这个函数,传过来了我们埋雷的数组,和一个坐标,对于埋雷的数组,它的每个位置不是‘0’就是‘1’,我们把它的周围八个位置上字符加起来,其实加的是ASCII值,然后根据上面的小知识点,再减去8*‘0’,其实是8*48,得到的整数不就是周围雷的个数吗,所以这个代码就是这个原理。

再举个实例,比如一个坐标周围有5个雷,也就是 5个‘1’和3个‘0’,加起来是

3*48+5*49=389,8*48=384,389-384=5,5不就是雷的个数吗,5再加上48,对应的字符不就是‘5’吗。

看一下排雷的效果:

显示的棋盘

埋雷的棋盘

如图所示,显示棋盘中,左上角的 2,就代表周围红方框内有两个雷,中间的2 就代表,中间红方框内有两个雷,对应到埋雷的棋盘,确实是如此。


标记和取消标记

这两个操作相对简单,先介绍这两个操作,最后再介绍排雷的操作。

标记

1.注意
  • 坐标输入要合理

  • 操作的是显示的棋盘

  • 已标记的位置不重复标记

  • 已排查的位置不能标记

  • 标记分为两种,‘!’和'?'

  • 被标记的位置不能排查

前面四种比较好理解,重点解释一下最后两种。

标记分为两种:

  • '!'代表玩家确定这个位置是雷,

  • '?'代表玩家不确定这个位置是否有雷,可能是有的。

被标记的位置不能排查:

就是说这个位置玩家认为它是雷或者可能为雷,在排雷的过程中,这些位置是不能被排查,在递归的过程中,也要考虑这种情况,因为递归的过程也是在排查雷。

2.代码
void PosMark(char show[ROWS][COLS], int row, int col)
{
    int x = 0, y = 0;
    printf("请输入标记的坐标:>");
    scanf("%d%d", &x, &y);
    system("cls");
    if (x >= 1 && x <= row && y >= 1 && y <= col)//输入的坐标数值合法
    {
        if (show[x][y] == '*')
        {
            show[x][y] = '!';
        }
        else
        {
            if (show[x][y] == '!'||show[x][y]=='?')
            {
                printf("该位置,已标记不可重复标记!!\n");
            }
            else
            {
                printf("该位置已经排查过,不能标记!!\n");
            }
        }
    }
    else
    {
        printf("输入坐标不合法,请重新输入!!\n");
    }
}

对于正确的标记,只需把原来位置上的'*'改为!即可,其它错误的标记,给出合理的提示即可。

取消标记

    • 注意
  • 坐标输入要合理

  • 操作的是显示的棋盘

  • 未标记的、已排查的位置不能取消标记

    • 正确的取消标记
  • 第一次,先把!改为?

  • 第二次,再把?改为*

    • 代码
void EraMark(char show[ROWS][COLS], int row, int col)
{
    int x = 0, y = 0;
    printf("请输入取消标记的坐标:>");
    scanf("%d%d", &x, &y);
    system("cls");
    if (x >= 1 && x <= row && y >= 1 && y <= 9)//坐标合理
    {
        if (show[x][y] == '!')//标记为 '!'
        {
            show[x][y] = '?';//标记改为 '?'
        }
        else if (show[x][y]=='?')//标记为'?'
        {
            show[x][y] = '*';//改为初始值
        }
        else
        {
            if (show[x][y] == '*')//对未标记的坐标进行取消标记
            {
                printf("该位置未被标记,不可取消标记!!\n");
            }
            else//对排查过的位置进行取消标记
            {
                printf("该位置已排查,不可取消标记!!\n");
            }
        }
    }
    else//坐标输入错误
    {
        printf("坐标输入错误!!\n");
    }
}

只有当坐标输入正确且该位置已经标记,才能取消标记,其它位置给出合理提示即可。

排雷

排雷中,难点在于那种一大片的效果,那是采用的递归算法。

    • 注意

  • 该位置有雷,游戏结束

  • 该位置没雷,进行排雷

  • 坐标不越界、不重复排雷

    • 代码

int  Findmine(char board[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
    int x = 0, y = 0;
    //system("cls");
    printf("请输入排雷坐标:>");
    scanf("%d%d", &x, &y);
    system("cls");
    if (x >= 1 && x <= row && y >= 1 && y <= col) {//坐标合理
        if (show[x][y] != '*') {//该位置不是初始值,不能排雷
            if (show[x][y] == '!' || show[x][y] == '?') {//该位置被标记,不能排雷
                printf("该位置已被标记,暂时不可排雷,请取消标记后,再进行排雷!!\n");
                return 1;
            } else {//该位置已经排雷,不能排雷
                printf("该位置已排雷,不可重复排雷!!\n");
                return 1;
            }

        } else { //统计雷的个数
            if (board[x][y] == '1') { //该位置是雷,游戏结束
                return  0;
            } else {//该位置需要排雷

                RecuMine(board, show, x, y);//递归排雷
                return 1;
            }
        }
    } else {//坐标输入不合法
        printf("坐标输入错误!!!\n");
        return 1;
    }
}

排雷本身的逻辑并不复杂,只要在正确的位置排雷即可,对于不同排雷的情况,我们给出不同的提示。

里面返回值: 1 代表本次排雷后,游戏继续(输赢是在这个函数外面判断的),0 代表排到雷了,游戏结束。

    • 递归排雷

1)条件:
  • 该位置周围没有雷(该位置显示的是空格)

  • 该位置没有被排查过(不重复递归)

  • 坐标不越界

2)递归的代码
void RecuMine(char board[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
    if (x < 1 || x>9 || y < 1 || y>9)//坐标不合理不递归
        return;
    if (show[x][y] == '!'||show[x][y]!='*'||show[x][y]=='?')
// 该位置被标记(!或?)、该位置被排查过 不递归
//您想被排查的坐标只要不是雷,那肯定有信息,一定不会是初始值 ‘*’
//那么就可确定只要不等于 * 的位置,就排查过了
        return;
    int count = 0;
    count = MineCount(board,x, y);//先统计改坐标周围雷的个数
    if (count == 0)//雷的个数为 0,即周围 8个位置都不是雷,那么这个八个位置就放心排雷了
    {
        show[x][y] = ' ';//先把该位置置为空格
//递归它周围 8个位置的坐标,进行排雷
        RecuMine(board, show, x - 1, y - 1);
        RecuMine(board, show, x - 1, y );
        RecuMine(board, show, x - 1, y + 1);
        RecuMine(board, show, x, y - 1);
        RecuMine(board, show, x, y + 1);
        RecuMine(board, show,x+1, y - 1);
        RecuMine(board, show, x+1, y);
        RecuMine(board, show, x+1, y +1);
    }
    else//这里代表这个位置周围是有雷的,那么把的个数显示上去,程序走到尾部,递归也就返回了
    {
        show[x][y] = count + '0';
    }
}

递归的代码如上,接下来看一下递归产生的效果。

排查的是(4,4)这个位置

排查的是(3,6)这个位置

以上就是对这个递归排雷的介绍,我们尤其要注意的是递归进行的条件!!


判断输赢

这个比较简单,只要所有的非雷格子都被翻开,那么就赢了,我们只需要统计这些格子的数量就好了。

代码

//判断输赢
int  IsWin(char show[ROWS][COLS], int row, int col)
{
    //所有不是雷的格子都被掀开了,就赢了
    int count = 0;

    for (int i = 1; i <= row; i++)
    {
        for (int j = 1; j <= col; j++)
        {
            if (show[i][j]!='*'&&show[i][j]!='!'&&show[i][j]!='?')
                //这个地方不是雷  他被掀开了
                count++;
        }
    }
    if (count ==  rows*cols-MINES)
        return 1;
    else
        return 0;
}

这个的功能其实是判断是否赢了,输了我们是在排雷那里根据返回值判断的,这里返回值为1 代表赢了,返回值为 0代表没有赢,游戏继续。

if (show[i][j]!='*'&&show[i][j]!='!'&&show[i][j]!='?')
                //这个地方不是雷  他被掀开了
                count++;

这个语句的意思是,该位置已经被排查过了(同时不能被标记),那么这个格子就是我们要统计的。

再来

if (count ==  rows*cols-MINES)
        return 1;
    else
        return 0;

总的格子数减去埋有雷的格子数,就是我们要统计的个数,只要是这个数,那么就赢了。


总结

    • 主函数

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu() {
    printf("*************************\n");
    printf("*********  1.play *******\n");
    printf("*********  0.exit *******\n");
    printf("*************************\n");
}
void  menu2() {
    printf("1.排雷      2.标记      3.取消标记\n");
}
void game() {
    system("cls");
    char board[ROWS][COLS] = { 0 };//埋雷 排雷的棋盘
    char show[ROWS][COLS] = { 0 };//打印效果的棋盘
    //初始化棋盘
    InitBoard(board, ROWS, COLS, '0');
    InitBoard(show, ROWS, COLS, '*');
    //打印棋盘

    //ShowBoard(show, rows, cols);
    //埋雷
    Setmine(board, rows, cols);
    ShowBoard(board, rows, cols);
    int cur = 1;
//这里初始值设为1,是为了防止第一次没有排雷,直接判断cur等于0就结束了
//所以这个初始值 要非零
    int input = 0;
    while (1) {
        ShowBoard(show, rows, cols);
        menu2();
        printf("请选择操作:>");
        scanf("%d", &input);

        switch (input) {
            case 1:
                //排雷
                cur = Findmine(board, show, rows, cols);
                break;
            case 2:
                //标记
                PosMark(show, rows, cols);
                break;
            case 3:
                //取消标记
                EraMark(show, rows, cols);
            default:
                printf("输入错误,请重新输入!!\n");
                break;
        }
        if (cur == 0) {
            printf("很遗憾,你被炸死了,游戏结束!!!\n");
            ShowBoard(board, rows, cols);
            break;
        }
        if (IsWin(show, rows, cols)) {
            printf("恭喜你,你赢了!!!\n");
            ShowBoard(show, rows, cols);
            ShowBoard(board, rows, cols);
            break;
        }
    }

}
int main() {
    srand((unsigned int)time(NULL));//随机数的种子
    int input = 0;
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input) {
            case 1:
                game();
                break;
            case 0:
                printf("退出游戏!!\n");
                break;
            default:
                break;
        }

    } while (input);

    return 0;
}
    • 注意

  • 把游戏各个功能分成一个个模块,逐个去写,并且写一部分调试一部分

  • 扫雷游戏难点在棋盘的设计和操作的实现


以上就是本次分享的全部内容啦


如果对您有帮助,希望给博主点点赞,收藏,分享一下,感谢您的支持!!

源代码和往常一下,放在gitee仓库里面啦,有需自取哦~~~

我们下期,再见~

链接如下~~

扫雷完整版_1_2_25 · 琦琦爱敲代码/琦琦的日常代码 - 码云 - 开源中国 (gitee.com)

老铁们,可以玩一玩这个扫雷哦,评论区可以留下你成功了多少次,用时多少哦~~

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值