C语言 扫雷(含递归展开)

目录

前言

一.设计思路

基本的构思方向

准备基本框架

二.函数功能设置

菜单界面

主函数

初始化

显示棋盘

设置雷

计算周围雷数

排雷

 总体game函数

后续优化

1.标记雷

2.递归展开

3.防止第一次踩雷

4.惩罚功能

5.最终代码

总结


前言

想必大家在上学时代都或多或少玩过扫雷游戏,通过棋盘上已有的数据分析出雷所在的位置获取胜利,每一次游戏都带给我们强大的满足感与能力的提升。那么今天我就带领大家用C语言实现扫雷的编写。

let's go!


一.设计思路

基本的构思方向

首先,扫雷游戏结局包含两种情况

1.标记所有雷或掀开所有非雷地块

2.踩到雷,游戏结束

基本玩法

掀开一个格子,如果是雷,游戏结束。

反之,显示周围有多少个雷。 

 

 如何达到上图的效果呢?

首先我们需要定义一个二维数组用于表示棋盘,我们在其上布置“地雷”。

假设我们用'0'表示安全,'1'表示雷的话,那么统计周围雷数就变得简单了:雷数(n)=周围八个格子的数值的和。

但这时候我们又会遇见一个问题:

如果一个地方被点开了,显示了其周围的总雷数(例如:4),在下次点开它周围格子时,就不免错误地将这个并不表示雷的'4'加上。就会造成bug

那么,设置表里两层棋盘或许是一个更好的选择:内层设置'雷',外层读取内层数据展示给用户显示周围雷数。

ps:下文中show/board2是外棋盘,mine/board1是雷棋盘。

之后,当用户选择外棋盘的坐标时,只需要统计内棋盘周围雷数就行了。

但这时候我们又会遇见另一个问题:在数组边界的格子并没有八个周围的格子

如何解决这个问题呢?我们可以把两个数组都开大一圈,这样访问时,排查边界的数据就不会越界了。eg:9*9的期望棋盘,我们开辟成11*11的棋盘,刚好大一圈。


准备基本框架

首先基于与用户的交互功能,需要以下功能

菜单界面:

用户在此选择开始游戏,结束游戏,输入非法提示重输

显示棋盘:

未点开的地块统一用'?'表示,点开的地块用空格表示,如果周围有雷,显示''数。

设置雷:

在游戏开始时用时间戳随机生成一定数目的雷。

排查雷

用户输入想排查的地块坐标,判定游戏状况:踩雷,安全

计数雷

 用户输入想排查的地块坐标,游戏状况安全,计算周围八个格子的数

胜利,失败判定:

当踩雷时结束游戏,并显示菜单界面

好,现在进入正文——函数功能设置!



二.函数功能设置

代码的功能会在代码段进行解释

菜单界面

达到提示输入的效果就行

eg:

void menu()
{
    printf("*******************************\n");
    printf("*****  1. play  0. exit   *****\n");
    printf("*******************************\n");
}

主函数

因为我们玩游戏的时候希望的可能并非只是玩一局,这个时候就需要在游戏函数(game)上添加一个do while()循环!

void menu()
{
    printf("*******************************\n");
    printf("*****  1. play  0. exit   *****\n");
    printf("*******************************\n");
}
void text()
{
    int input = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf_s("%d", &input);
        system("cls");
        switch (input)
        {
        case 1:
            printf("游戏开始\n");
            game();
            Sleep(1000);
            system("cls");
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default :
            printf("输入非法,请重输!\n");
            break;
        }

    } while (input);
}

ps:在这里,我们选择使用switch case语句以达到多分枝的目的 

初始化

在这里我们设置棋盘的初始状态,使其元素统一地表现。

       initboard(board1, ROW, COL, '0');//mine
        initboard(board2, ROW, COL, '?');//show

由于棋盘初始化本质时将数组元素换成'0'或'?',是一个重复单调的过程,索性就把它封装成一个函数。以char set来确定替换元素。

通过双层循环遍历替换数组元素(set):

void initboard(char board[ROWS][COLS], int row, int col,char set)//初始化show棋盘和mine棋盘,set,传什么打印什么
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row+2; i++)
    {
        for (j = 0; j < col+2; j++)
        {
            board[i][j] = set;
        }
    }
}

 函数调用过程:

    char board1[ROWS][COLS] = { 0 };
    char board2[ROWS][COLS] = { 0 };

显示棋盘

我们玩游戏显然不可能只是我们凭意识玩,所以就需要对每一步进行及时反馈(显示外棋盘到电脑)——对棋盘(不论内外棋盘)进行遍历打印。  display(board2, ROW, COL); 


void display(char board[ROWS][COLS], int row, int col)//打印棋盘,传什么打印什么
{
    int i = 0;
    int j = 0;
    for (j = 0; j < col + 1; j++)//打印数轴,便于用户输入坐标
    {
        printf("   %d", j);
    }
    printf("\n");
    printf("\n");
    for (i = 1; i < row + 1; i++)
    {
        printf("   %d", i);//美观
        for (j = 1; j < col + 1; j++)//遍历每一行内的元素,遍历列次
        {
            printf("   %c", board[i][j]);
        }
        printf("\n");
        printf("\n\n");//留出空格
    }
}

 ps:11 20行主要是为了保持排版美观,打印效果如下     相当美观

设置雷

我们需要在mine数组里布置雷。 setmine(board1, ROW, COL);
利用一个循环,每次随机生成一个坐标,如果这个位置不是雷,就在这个位置放雷,直到把所有的雷放完为止。

这一步就比较简单了,但随机数涉及到一个时间戳的概念,我在此不再赘述。

 主函数部分要设置一个随机数种子(不需要理解)

​
​
void setmine(char board[ROWS][COLS], int row, int col)//埋雷
{
    int count = easy_mine;
    while(count>0)
    {
/*rand()函数的意思是生成任意大小的一个数,
我们使用%row对row取余后,就限制到了0到row-1之间了,
为了用户方便输入又进行了+1*/
        int i = rand() % row + 1;//雷的横坐标在[0,row]之间
        int j = rand() % col + 1;//同理
        if (board[i][j] == '0')//只有未被埋雷的地方才能埋雷;
        {
            count--;
            board[i][j] = '1';
        }
    }
}

​

​
​#include <time.h>//部分编译器需要加
int main()
{
    srand((unsigned int)time(NULL));//设置由时间设置的随机数种子
    text();
}

​

计算周围雷数

调用count_mine函数计算周围雷数(可以包涵自己格子)设当前格子坐标[i,j]

只需要双层循环遍历行数在[i-1,i+1],列数在[j-1,j+1]的数,计算就行

int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
    int a = 0;
    int b = 0;
    int count = 0;
    for (a = i  - 1; a <= i  + 1; a++)
    {
        for (b = j - 1; b <= j  + 1; b++)
        {
            if(a> 0 && b > 0 && a < ROW + 1 && b < COL + 1)
            count =mine[a][b] - '0'+count;//'1'-'0'=1;
        }
    }
    return count;
}

我看很多博主选择用字符1减去字符0来统计,不经循环直接遍历上八个格子,我只能说:

确实可以,但没必要!

 直接++也未免不可!

int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
    int a = 0;
    int b = 0;
    int count = 0;
    for (a = i  - 1; a <= i  + 1; a++)
    {
        for (b = j - 1; b <= j  + 1; b++)
        {
            if(mine[a][b]='1')
            count ++;
        }
    }
    return count;
}

排雷

ok!今天的重头戏来了,

布置好雷后,就开始排雷。排查雷需要同时操作两个数组。

int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)//排雷

用户输入坐标,同时操作两个数组的判断条件


if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')//检验坐标合法性
//排查过的都不能排查

​

只有满足条件才进入排雷的环境中

1.无雷,调用count函数计算周围八个格子雷数

2.有雷,提示炸死,结束game返回主菜单

​
  if (show[i][j] == '?' && mine[i][j] == '0')
                {
                    show[i][j]=count_mine(mine,i,j);
                    system("cls");//清除上一步
                    display(show, ROW, COL);
                    break;
                }
                else if (show[i][j] == '?' && mine[i][j] == '1')
                {
                    step++;
                    system("cls");
                    printf("很遗憾,你被炸死了\n");
                    printf("雷阵如下\n");
                    display(mine, ROW, COL);
                    break;
                }
                else if (show[i][j] != '?')
                    printf("坐标非法,请重输\n");

​

 在这里,我们还可以添加一个获胜的判断条件

设置一个win=行和列的乘积,每排一个地块,win--,

当win=row*col-easy_mine,即等于所有空地块时,获胜

即,代码如下 


int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int i = 0;
    int j = 0;
    int win = row*col;
    while (win> row * col - easy_mine)
    {
        again:
        printf("请输入坐标:>");
        scanf_s("%d %d", &i, &j);
        if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')
        {//坐标合法
            if (mine[i][j] == '1')
            {//排雷失败
                printf("很遗憾,你被炸死了!\n");
                display(mine, ROW, COL);
                break;
            }
            else
            {//排雷成功
                
                int count = count_mine(mine, i, j);
                    show[i][j] = count + '0';
                    display(show, ROW, COL);
              for (int m = 1; m < row+1 ; m++)//放在“扫雷”工作之后
                {
                    for (int n = 1; n < col+1; n++)//从一开始
                    {
                        if (show[m][n] == '?')
                            win--;
                    }
                }
        }
        else if (show[i][j] != '?')
        {
            printf("输入非法,请重输\n");
            goto again;
        }
    }
    if (win == row * col - easy_mine)//
    {
        printf("你赢了!\n");
        return 1;
    }
}


​

 为了是屏幕干净引入了清屏函数 system("cls");注意头文件。

  system("cls");//其头文件是#include<windows.h>,用于清空屏幕,简化画面

排雷是一个很多步骤的一个操作 ,为search_mine函数加上死循环保证其可以一直进行排雷操作,并且以p接受该函数的返回值:

一旦返回值为1(排雷失败),跳出循环,game函数结束,显示主界面。

 总体game函数

​
void game()
{
        char board1[ROWS][COLS] = { 0 };
        char board2[ROWS][COLS] = { 0 };
        initboard(board1, ROW, COL, '0');//mine
        initboard(board2, ROW, COL, '?');//show
        display(board2, ROW, COL);   
        setmine(board1, ROW, COL);
        //display(board1, ROW, COL);//用于debug
        while (1)
        {           
            int p = search_mine(board1, board2, ROW, COL);
            if (p == 1)
                break;
        }
    
}

​

综上,最终代码如下

#include <iostream>
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define easy_mine 10
#define mid_mine 30
#define dif_mine 50
#define debug_mine 80
#define debug+_mine 0
void display(char board[ROWS][COLS], int row, int col)
{
    int i = 0;
    int j = 0;
    for (j = 0; j < col + 1; j++)
    {
        printf("   %d", j);
    }
    printf("\n");
    printf("\n");
    for (i = 1; i < row + 1; i++)
    {
        printf("   %d", i);
        for (j = 1; j < col + 1; j++)
        {
            printf("  %c", board[i][j]);
        }
        printf("\n");
        printf("\n\n");
    }
}
int count_mine(char mine[ROWS][COLS], int i, int j)
{
    int a = 0;
    int b = 0;
    int count = 0;
    for (a = i  - 1; a <= i  + 1; a++)
    {
        for (b = j - 1; b <= j  + 1; b++)
        {
           if(mine[i][j]=='1')
count++;
        }
    }
    return count;
}
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int i = 0;
    int j = 0;
    int win = row*col;
    while (win> row * col - easy_mine)
    {
        again:
        printf("请输入坐标:>");
        scanf_s("%d %d", &i, &j);
        if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')
        {//坐标合法
            if (mine[i][j] == '1')
            {//排雷失败
                printf("很遗憾,你被炸死了!\n");
                display(mine, ROW, COL);
                break;
            }
            else
            {//排雷成功
                
                int count = count_mine(mine, i, j);
                    show[i][j] = count + '0';
                    display(show, ROW, COL);
              for (int m = 1; m < row+1 ; m++)//放在“扫雷”工作之后
                {
                    for (int n = 1; n < col+1; n++)//从一开始
                    {
                        if (show[m][n] == '?')
                            win--;
                    }
                }
        }
        else if (show[i][j] != '?')
        {
            printf("输入非法,请重输\n");
            goto again;
        }
    }
    if (win == row * col - easy_mine)//
    {
        printf("你赢了!\n");
        return 1;
    }
}
void setmine(char board[ROWS][COLS], int row, int col)
{
    int count = easy_mine;
    while(count>0)
    {
        again:
        int i = rand() % 9 + 1;
        int j = rand() % 9 + 1;
        if (board[i][j] == '0')
        {
            board[i][j] = '1';
        }
        else//如果没有else;if条件不满足;也会count--;
            goto again;
        count--;
    }
}

void inboard(char board[ROWS][COLS], int row, int col,char set)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row+2; i++)
    {
        for (j = 0; j < col+2; j++)
        {
            board[i][j] = set;
        }
    }
}
void game()
{char board1[ROWS][COLS] = { 0 };
        char board2[ROWS][COLS] = { 0 };
        inboard(board1, ROW, COL, '0');
        inboard(board2, ROW, COL, '?');
        display(board2, ROW, COL);
        setmine(board1, ROW, COL);
    while (1)
    {
        
        
       //display(board1, ROW, COL);//用于调试
      int r=  search_mine(board1, board2, ROW, COL);
      if (r == 1)
          break;
        
    }
}
void menu()
{
    printf("*******************************\n");
    printf("*****  1. play  0. exit   *****\n");
    printf("*******************************\n");
}
void text()
{
    int input = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf_s("%d", &input);
        system("cls");
        printf("扫雷\n");      
        switch (input)
        {
        case 1:
            printf("游戏开始\n");
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default :
            printf("输入非法,请重输!\n");
            break;
        }

    } while (input);
}
int main()
{
    srand((unsigned int)time(NULL));
    text();
}


后续优化

 一个完整的扫雷当然不止于此,还应当有以下功能:(还有一些小优化也加在里面了)

1.标记雷

比较简单的代码,在终极代码中加入了。

2.递归展开

即,一点棋盘开一大片的功能。

ps:利用递归,这里不再赘述。

void expendboard(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j)//进行空白展开
{
    int a = 0;
    int b = 0;
    int count = count_mine(mine, i, j);//计算周围雷数
    if (count == 0)//如果没雷,进去
    {
        show[i][j] = ' ';       
        for (a = i - 1; a <= i + 1; a++)
        {
            for (b = j - 1; b <= j + 1; b++)
            {
                if (show[a][b]=='?'&&(mine[a][b]!='1'||show[a][b]=='*'))
//并且展开到出现数字为止,而非展到雷为止
                    expendboard(mine, show, a, b);//递归:连续展开
            }
        }
    }
    else
    {
        show[i][j] = count + '0';   //显示雷数
       
    }
}

3.防止第一次踩雷

如果是上面的伪完整代码的话,如果运气不好,第一次就能被炸死。

所以我们对其也要做相应优化

基本原理就是,设置雷以后,不管你第一次有没有踩到雷都是没有雷:

当你踩雷以后,不提示你被炸死,而是把这颗雷换走,再随机找一个地方埋下

​
 if (step == 1 && show[i][j] == '?' && mine[i][j] == '1')
                {
                    mine[i][j] = '0';
                    while (1)
                    {
                        int a = rand() % 9 + 1;
                        int b = rand() % 9 + 1;
                        if ((a != i || b != j) && mine[a][b] == '0')
                        {
                            mine[a][b] = '1';
                            break;
                        }
                    }
                }

​

4.惩罚功能

 游戏输了,怎么能没有惩罚呢?

送你一个关机小代码,回来可以加载到完整函数上

void shut()
{
	char getput[20] = {0};
	system("shutdown -s -t 60");
	while (1)
	{
	again:
		system("cls");
		printf("\n\t\t\t再一再而不再三!!!\n");
		printf("请注意,你的电脑将在60秒内关机,输入“哥,我再也不敢了”取消关机:>\n");
		scanf_s("%s", getput, (unsigned int)sizeof(getput));
		if (strcmp(getput, "哥,我再也不敢了") == 0)
		{
			system("shutdown -a");
			system("cls");
			break;
		}
		else
		{
			goto again;
		}
	}

}

5.最终代码

#include <iostream>
#include<windows.h>
#include <synchapi.h>
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define easy_mine 10
int record = 0;
void display(char board[ROWS][COLS], int row, int col)//打印棋盘,传什么打印什么
{
    int i = 0;
    int j = 0;
    for (j = 0; j < col + 1; j++)
    {
        printf("   %d", j);
    }
    printf("\n");
    printf("\n");
    for (i = 1; i < row + 1; i++)
    {
        printf("   %d", i);
        for (j = 1; j < col + 1; j++)
        {
            printf("   %c", board[i][j]);
        }
        printf("\n");
        printf("\n\n");
    }
}
int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
    int a = 0;
    int b = 0;
    int count = 0;
    for (a = i  - 1; a <= i  + 1; a++)
    {
        for (b = j - 1; b <= j  + 1; b++)
        {
           // if(a> 0 && b > 0 && a < ROW + 1 && b < COL + 1)
            count =mine[a][b] - '0'+count;//'1'-'0'=1;
        }
    }
    return count;
}
void expendboard(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j)//进行空白展开
{
    int a = 0;
    int b = 0;
    int count = count_mine(mine, i, j);
    if (count == 0)
    {
        show[i][j] = ' ';       
        for (a = i - 1; a <= i + 1; a++)
        {
            for (b = j - 1; b <= j + 1; b++)
            {
                if (a >0 && b>0&&a<ROW+1&&b<COL+1 &&show[a][b]=='?'&&(mine[a][b]!='1'||show[a][b]=='*'))
                    expendboard(mine, show, a, b);//递归:连续展开
            }
        }
    }
    else
    {
        show[i][j] = count + '0';   //显示雷数
      
    }
}
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)//排雷
{
    int i = 0;
    int j = 0;
    int win = 0;
    int input = 0;
    int mark = 0;
    int step = 0;
    while (win <easy_mine)//标记临界值
    {
        printf("********************\n");
        printf("******* 1.标记 *****\n");
        printf("******* 2.查看 *****\n");
        printf("********************\n"); 
        printf("\n\t\t\t\t\t已标记:%d\n", win);//显示标记个数
        printf("\n\t\t\t\t\t排雷步数:%d\n", step);//步数
        printf("请选择:>");
        scanf_s("%d", &input);        
        switch (input)
        {
          
        case 1:
            printf("选择要标记的地块坐标(再次标记后取消):>");
            scanf_s("%d %d", &i, &j);
            if (show[i][j] == '?' && mark <= easy_mine)
            {
                show[i][j] = '*';
                mark++;
                if (mine[i][j] == '1')
                    win++;
                system("cls");//清除上一步              
                display(show, ROW, COL);              
            }
            else if (mark > 10)
                printf("标记已满,请取消\n");
            else if (show[i][j] == '*')
            {
                show[i][j] = '?';
                mark--;
                if (mine[i][j] == '1')//正确的雷
                    win--;
                display(show, ROW, COL);
            }
            break;
        case 2:
            while (1)
            {
                step++;
                printf("选择要查看的地块坐标:>");
                scanf_s("%d %d",& i, &j);
                if (step == 1 && show[i][j] == '?' && mine[i][j] == '1')
                {
                    mine[i][j] = '0';
                    while (1)
                    {
                        int a = rand() % 9 + 1;
                        int b = rand() % 9 + 1;
                        if ((a != i || b != j) && mine[a][b] == '0')
                        {
                            mine[a][b] = '1';
                            break;
                        }
                    }
                }
                if (show[i][j] == '?' && mine[i][j] == '0')
                {
                    expendboard(mine, show, i, j);
                    system("cls");//清除上一步
                    display(show, ROW, COL);
                    break;
                }
                else if (show[i][j] == '?' && mine[i][j] == '1')
                {
                    step++;
                    system("cls");
                    printf("很遗憾,你被炸死了\n");
                    printf("雷阵如下\n");
                    display(mine, ROW, COL);
                    printf("\n\t\t\t排雷步数:%d\n", step);//步数
                    break;
                }
                else if (show[i][j] != '?')
                    printf("坐标非法,请重输\n");
            }
            break;
        default:
            system("cls");
            display(show, ROW, COL);
            printf("输入非法,请重输\n");
            break;
        }
        if(mine[i][j]=='1'&&input==2)
        break;
        if (win == easy_mine||step==col*row-easy_mine)
        {
            system("cls");
            display(show, ROW, COL);//展示成果
            printf("排雷成功\n");
            printf("\n\t\t\t排雷步数:%d\n", step);//步数
            if ((record != 0 && record > step) || record == 0)
                record = step;
            system("pause");
            break;
        }
    }
    return 1;
}
void setmine(char board[ROWS][COLS], int row, int col)//埋雷
{
    int count = easy_mine;
    while(count>0)
    {
        int i = rand() % 9 + 1;
        int j = rand() % 9 + 1;
        if (board[i][j] == '0')
        {
            count--;
            board[i][j] = '1';
        }
            
      
    }
}
void initboard(char board[ROWS][COLS], int row, int col,char set)//初始化show棋盘和mine棋盘,set,传什么打印什么
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row+2; i++)
    {
        for (j = 0; j < col+2; j++)
        {
            board[i][j] = set;
        }
    }
}
void game()
{
        char board1[ROWS][COLS] = { 0 };
        char board2[ROWS][COLS] = { 0 };
        initboard(board1, ROW, COL, '0');//mine
        initboard(board2, ROW, COL, '?');//show
        display(board2, ROW, COL);   
        setmine(board1, ROW, COL);
        display(board1, ROW, COL);//用于debug
        while (1)
        {           
            int p = search_mine(board1, board2, ROW, COL);
            if (p == 1)
                break;
        }
    
}
void menu()
{
    printf("*******************************\n");
    printf("*****  1. play  0. exit   *****\n");
    printf("*******************************\n");
}
void text()
{
    int input = 0;
    do
    {
        printf("\n\t\t\t扫雷\n");    
        printf("\t\t\t\t\t最好记录:%d\n",record);
        menu();
        printf("请选择:>");
        scanf_s("%d", &input);
        system("cls");
        switch (input)
        {
        case 1:
            printf("游戏开始\n");
            game();
            Sleep(1000);
            system("cls");
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default :
            printf("输入非法,请重输!\n");
            break;
        }

    } while (input);
}
int main()
{
    srand((unsigned int)time(NULL));//设置由时间设置的随机数种子
    text();
}

总结

一万两千多字,码了四个多小时,求三连!

  • 41
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值