C语言之扫雷

17 篇文章 0 订阅

前言

用C语言写出一个扫雷程序。像大家熟悉的电脑扫雷一样,如果选的位置有雷就结束游戏;如果没有雷,就标记出来周围格子雷的数量。这里写出来的扫雷不能用鼠标点击,也没有提供标记雷的功能,只在CMD黑框框里展示出来,所以看上去比较难看、用起来也没有windows系统自带的扫雷那样方便。


设计要求


  1. 首先需要写一个展示功能,否则玩家拿什么扫
  2. 其次,要对雷区进行随机布雷。这里布雷的要求是第一步永远不能被炸死。我想到了两种方法解决:第一种方法是先生成随机雷阵,待玩家走第一步后判断这一步的位置是否有雷,如果有雷就把雷随机放在一个没有雷的位置;第二种方法是让玩家先走第一步,然后在避开玩家走第一步的位置的前提下进行随机布雷。本人觉得第二种容易实现且代码简单,就选择了第二种。
  3. 然后,需要扫雷里最重要的东西–玩家扫雷。玩家通过输入坐标的方式选择要扫的位置,然后程序检查该位置是否有雷,有雷就失败,没有就标记出该位置或者该位置的扩展位置的周围8个位置的雷数。如果雷数为0则不显示。
  4. 最后就是对前面3步的调度,玩什么难度、失败后继续还是不玩了。。。 一切由玩家选择。

补充一点,我所使用的是VS2008, 采用TDD模式,扫雷程序分为3个源文件,分别是:game.h 、game.c 、index.c

正文

为了更好地解释代码和体现思维的连贯性,按照代码的顺序进行说明。文末附有扫雷的全部代码。

先来看 index.c 中的代码:

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

int main(){
    char mine[MROW][MCOL];
    char show[MROW][MCOL];
    int row=0, col=0;
    int count=0;
    int dead[2]={0};
    char again[3]={'5'};
    int c=0;
    int Fx=-1, Fy=-1;
    int WinSign=0;
    srand((unsigned int) time(NULL));

AGAIN : Init_Board(mine,show, &row, &col);
    Show_Board(show, row, col);
    while(1)
    {
        printf("选择位置:>");
        scanf("%d%d", &Fx, &Fy);
        while((c = getchar()) != '\n' && c != EOF);     //清除键盘缓冲区的万能写法
        if ( Check_Legal(row, col, Fx, Fy) !=1)
        {
            printf("输入位置不合法,再次");
            continue;
        }
        break;
    }
    PutMine(mine, row, col, Fx, Fy, &count);
    while(Player_Do(mine, show, row, col, dead, &Fx, &Fy) ==0)
    {
        if(!Check_Win(show, row, col, count))
        {
            WinSign=0;
    //      Show_Board(mine, row, col);
            Show_Board(show, row, col);
            printf("*********************共有%d颗雷********************\n", count);
        }
        else
        {
            WinSign=1;
            break;
        }
    }
        if (WinSign==1)
        {
            Show_Board(show, row, col);
            printf("恭喜你,排完了所有雷!\n");
        }
        else
        {
            show[dead[0]][dead[1]] = '*'; 
            Show_Board(show, row, col);
        }
        printf("**********************************游戏结束****************************************\n");
        Sleep(2500);
        printf("你还想再玩一局吗?(Y/N)  >");
        scanf("%c", &again);
        while((c = getchar()) != '\n' && c != EOF);     //清除键盘缓冲区的万能写法
        if ( strcmp(again, "Y")==0 || strcmp(again, "y")==0 || strcmp(again, "YES")==0 || strcmp(again, "yes")==0 || strcmp(again, "Yes")==0 ) goto AGAIN ;

    return  0;
}

这里引入头文件 game.c 是因为所有的函数实现是放在game.c文件中,函数和全局变量、对标准库的引用全是放在game.h文件中的。

mine 矩阵是雷矩阵,用于内部逻辑处理; show 矩阵是展示矩阵,用于和玩家交互。
MROW、MCOL 是在game.h中用define定义的两个宏常量。

row、col是玩家选择板子的行数和列数。

Init_Board 函数的作用是随机布雷并由玩家决定板子的行数row和列数col。

Show_Board 函数的作用是显示板子的内容。

Check_Legal 函数的作用是检测玩家选择的第一个位置是否合法,合法则返回1。

PutMine 函数的作用是利用玩家走完第一步、用Fx、Fy记录下该位置后在避开该位置的前提下布雷,同时将雷的总数放入 count 中。

Player_Do 函数的作用是玩家选择要排的位置,然后判断,该位置不是雷就扩展,是雷的话就在 dead[2] 数组中写入这个游戏结束位置。

Check_Win 函数的作用是通过检查剩余未判断区域的数量是否大于总雷数 count 来判断是否排完全部的雷,是则返回1。

接下来对各个函数进行说明

Init_Board

static void Create_Board(char mine[MROW][MCOL], char show[MROW][MCOL], const int level, int *row, int *col){
        int i=0, j=0;
        *row=level;
        *col=level;
        for(i=0; i<MROW; i++)
        {
            for(j=0; j<MCOL; j++)
            {
                mine[i][j]='0';
                show[i][j]='#';
            }
        }
}

void Init_Board(char mine[MROW][MCOL], char show[MROW][MCOL], int *row, int *col)
{
    int choose;
    int c=0;
    printf("请选择难度(  1:容易  2:一般  3:困难 )>");
    scanf("%d", &choose);
    while((c = getchar()) != '\n' && c != EOF);
    while(choose!=1 && choose!=2 && choose!=3)
    {
        printf("输入有误,请重新选择难度(  1:容易  2:一般  3:困难 )>");
        scanf("%d", &choose);
        while((c = getchar()) != '\n' && c != EOF);
    }
    switch (choose){
        case 1:Create_Board(mine,show, EAZY, row, col); break;
        case 2:Create_Board(mine, show,ORDINARY, row, col); break;
        case 3: Create_Board(mine,show, DIFFICUTE, row, col);break;
        default: printf("输入错误\n");
    }
}

这里使用开关语句比较 choose 的值,根据 choose 值的不同,调用静态函数 Create_Board 创建用不同枚举常量的 mine 、 show 矩阵且将玩家选择的板子的行数和列数用 rwo、col 记录。注意这里的初始化并内有布雷,只是无差别地让 mine 的每个单元为字符O、让 show 的每个单元为字符#。布雷函数单独存在。

PutMine

void PutMine(char mine[MROW][MCOL], const int row, const int col, int Fx, int Fy, int *count)
{
    int i=0, j=0;
    int x=0, y=0;
    int ss=0;

    *count = row + (int)pow(2,(col/4))-3;           //雷数   一级: 14颗      二级:32颗    三级:88for (i=0; i<*count; i++)
    {
        x=rand()%row + 1;
        y=rand()%col + 1;
        if(mine[x][y]=='1' || ((x==Fx) && (y==Fy)) )
        {
            i--;
            continue;
        }
        mine[x][y]='1';
        ss++;
        //          printf("第%d颗雷的位置是 (%d, %d)\n", ss, x ,y);
    }
}

这里根据初始化板子时确定的 row、col 来指定雷数 count,采用随机函数布雷, 有雷的位置在 mine 矩阵里用字符1代表。

Show_Board

void Show_Board( const char board[MROW][MCOL], const int row, const int col){
    int i = 0, j = 0;
    for(i=0; i<row+1; i++)
    {
        if (i==0)
        {
            for (j=0; j<row+1; j++)     (j<10) ? printf("  %d ", j) : printf(" %d ", j);

            printf("\n");
        }
        else
        {
            for(j=0; j<col+1; j++)      (j==0) ? printf("   ") : printf(" ---");

            printf("\n");
            for (j=0; j<col+1; j++)
            {
                if (j==0)   (i<10) ? printf("  %d", i) : printf(" %d", i); 

                else printf("| %c ",board[i][j]);
            }

            printf("|\n");
            if(i==row)
                for(j=0; j<col+1; j++)      (j==0) ? printf("   ") : printf(" ---");

        }

    }
    printf("\n\n");
}

这里没什么可说。

Check_Legal

int Check_Legal(const int row, const int col, const int x, const int y)
{
    return ( x<row+1  && x>0 && y<col+1 && y>0 ); 
}

当玩家选择的第一步的位置不在板子的范围类判定为非法,返回0。

Check_Win

int Check_Win(static char show[MROW][MCOL], static int row, static int col, int count)
{
    int i=0, j=0;
    for (i=1; i<=row; i++)
    {
        for(j=1; j<=col; j++)
        {
            if (show[i][j]=='#')
            {
//              printf("show[%d][%d]=%c  ", i, j, show[i][j]);
                count--;
            }
//          else printf("show[%d][%d]=  ", i, j);
        }
//      printf("\n");
    }
//  printf("\n");
    if (count<0)
    {
        return 0;
    }
    return 1;
}

遍历 row*col show矩阵, 若 count 小于0,说明剩余 ‘#’个数大于雷的总数,没有排完雷,返回0。否则,返回1。

Player_Do

该函数最为复杂,也是扫雷的核心程序,故而放在最后。先放出 Player_Do 函数的实现,接下来分别对 Player_Do 函数内部的一些调用做出解释。

int  Player_Do(char mine[MROW][MCOL], char show[MROW][MCOL], const int row, const int col, int dead[2], int *Fx, int *Fy)
{
    int x=0, y=0;
    int c=0;
    x=*Fx; y=*Fy;
Reput:  
    if (*Fx ==-1 && *Fy ==-1)
    {
        printf("选择位置:>");
        scanf("%d%d", &x, &y);
        while((c = getchar()) != '\n' && c != EOF);
    }

    *Fx=-1;  *Fy=-1;
    if(Check_Legal(row, col, x, y)==1)
    {
        if(mine[x][y]=='1')
        {
            dead[0] = x; 
            dead[1] = y;
            return 1;
        }
        else if (mine[x][y] == '-')
        {
            printf("此位置无效, 再次");
            goto Reput;
        }
        else
        {
            expand(mine, show, row, col, x, y);
        }
    }

    else
    {
        printf("输入位置不合法,再次");
        goto Reput;
    }
    return 0;
}

该函数对玩家第一次选择以后的位置做出合法性、该位置是否已排过检查,若通过检查,则调用 expand 函数进行判断扩展,达到类似爆炸一样的效果。

Player_Do 调用的 expand
int expand(const char mine[MROW][MCOL], char show[MROW][MCOL], const int row, const int col, const int x, const int y)
{

    if (  Check_Legal(row, col, x, y) !=1 )
    {
        return -1;
    }
    else if (mine[x][y] == '1' || mine[x][y] == '-')
    {
        return -2;
    }

    else
    {
        Mark_MineCount(mine, show, row, col, x, y);
        if(show[x][y] == ' ')
        {
            expand(mine, show, row, col, x-1, y-1);
            expand(mine, show, row, col, x-1, y);
            expand(mine, show, row, col, x-1, y+1);
            expand(mine, show, row, col, x, y-1);
            expand(mine, show, row, col, x, y+1);
            expand(mine, show, row, col, x+1, y-1);
            expand(mine, show, row, col, x+1, y);
            expand(mine, show, row, col, x+1, y+1);
        }
    }
    return 0;
}

首先进行的仍是对传入的位置进行检查,若排查位置超出板子范围Check_Legal(row, col, x, y) !=1、排查位置有雷mine[x][y] == ‘1’、排查位置已排查过(mine[x][y] == ‘-‘),则函数返回以作为递归结束条件。 这里分别返回 -1和-2 是为了在编写代码的过程中思路更清晰,其实该函数的返回值并没有在后续的过程中用到,只是起到结束递归的作用。
写成这样也可以:

if(Check_Legal(row, col, x, y) !=1 || mine[x][y] == '1' || mine[x][y] == '-') 
    return 0;

甚至还可以将 expand 函数的返回类型设为 viod ,返回时写

if(Check_Legal(row, col, x, y) !=1 || mine[x][y] == '1' || mine[x][y] == '-') 
    return;

如果通过检查,就调用 Mark_MineCount 函数标记该位置周围8个位置雷的数量,然后在该位置周围雷数为0的情况下再递归地调用 expand 函数对传入位置的周围8个位置进行扩展。为什么是 “在该位置周围雷数为0的情况下” 呢? 因为在扫雷的过程中,如果一个位置本身没有雷但它周围8个位置有雷,那么这个位置应该显示出一个数字,并且停止扩展。

expand 调用的 Mark_MineCount
static void Mark_MineCount(char mine[MROW][MCOL], char show[MROW][MCOL], const int  row, const int col, const int x, const int y)
{
    char c = '0';

    if (  Check_Legal(row, col, x, y) !=1 )
    {
        return;
    }
    else if (mine[x][y] == '1' || mine[x][y] == '-' || show[x][y]!='#')
    {
        return;
    }
    else
    {
        if(mine[x-1][y-1] !='-')  (c = c + mine[x-1][y-1] - '0');
        if(mine[x-1][y] !='-')   (c = c + mine[x-1][y] - '0');
        if(mine[x-1][y+1] !='-')  (c = c + mine[x-1][y+1] - '0');

        if(mine[x][y-1] !='-')  (c = c + mine[x][y-1] - '0');
        if(mine[x][y+1] !='-')  (c = c + mine[x][y+1] - '0');

        if(mine[x+1][y-1] !='-')  (c = c + mine[x+1][y-1] - '0');
        if(mine[x+1][y] !='-')  (c = c + mine[x+1][y] - '0');
        if(mine[x+1][y+1] !='-')  (c = c + mine[x+1][y+1] - '0');

        if(c == '0') show[x][y] = ' ';
        else show[x][y] = c;
        mine[x][y] = '-';
    }
}

这个函数的参数太多,足足有6个,这样做没有错,但是非常不提倡。因为函数的参数一旦多了之后,就会使别人容易看不懂你的代码。我这里的 Mark_MineCount 是静态函数,仅供 game.c 内部使用,6个参数勉强能说的过去。。。 但身为开发人员,应尽量避免这种问题。
这里对参数做出解释:

char mine[MROW][MCOL] : mine板子的最大范围

char show[MROW][MCOL] : show板子的最大范围

const int row : 实际使用的板子的行数

const int col : 实际使用的板子的列数

const int x : 想要进行周围雷数标记的位置的所在行

const int y : 想要进行周围雷数标记的位置的所在列


函数一开始进行的仍是检查: 检查位置的合法性、检查该位置是否有雷、是否已经排过,如果没有通过检查,那么说明这个位置上不能用数字标示出它周围8个位置的雷数; 若通过检查,就以变量 C 来记录周围的雷数。并且如果雷数为0,说明这个位置周围没有雷,可以进行扩展。 最后的C
mine[x][y] = '-';
将 mine 矩阵的[x][y]位置标记为’-‘用来告诉 expand 函数这个位置不是雷并且已经排查过了。

源代码

扫雷程序的完成代码放在了 GitHub 上,点击这里获取完整代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值