欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

小游戏:扫雷 (C语言实现扫雷的基本功能)

程序员文章站 2022-04-07 15:41:55
...

该程序分为三个文件:
1.game.h :包含头文件的引用、函数的声明和宏定义
2.game.c :包含各功能函数的具体实现
3.test.c :各功能函数的调用(程序的流程)

功能介绍:
1.初始化雷盘
2.打印雷盘
3.随机设置雷的分布
4.统计坐标位置周围的雷数
5.扩展式排雷
6.给所选坐标位置做标记
7.取消标记
8.第一次排雷不会被炸死

读者可以自己额外增加新功能,比如计时功能等。

作者提醒:
具体的内容都在代码注释里详细解释(我觉得是非常详细了)
当然,本人并不推荐这个程序中的注释风格,以后编代码千万别这么写注释
这样注释纯粹是为了初学者能轻松理解所有代码的作用

game.h

#ifndef __GAME_H__
#define __GAME_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>



#define ROW 10//雷盘行数
#define COL 10//雷盘列数
#define ROWS ROW+2//数组(实际)行数
#define COLS COL+2//数组(实际)列数
#define EASY_COUNT 10//初级难度的雷数

void InitBoard(char board[ROWS][COLS], int row, int col, char set);//初始化雷盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);//打印雷盘
void SetBoard(char board[ROWS][COLS], int row, int col, int count);//布雷
int GetCount(char board[ROWS][COLS], int x, int y);//统计雷数
void expend(char board1[ROWS][COLS], char board[ROWS][COLS], int x, int y,int *num);//扩展式排雷
void sign(char board[ROWS][COLS], int x, int y);//标记猴头(雷)
void unsign(char board[ROWS][COLS], int x, int y);//取消标记

#endif//__GAME_H__

game.c

#include "game.h"

void InitBoard(char board[ROWS][COLS], int row, int col, char set)//初始化雷盘
{
    memset(board,set,row*col*sizeof(board[0][0]));//利用memset初始化,memset用法自行看书学习
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)//打印雷盘
{
    system("CLS");//每次打印雷盘之前清屏一次
    int i = 0;
    int j = 0;
    printf("   ");//为了打印列坐标时对齐
    for (i = 1; i <= row; i++)
        printf("%d ", i);//打印列坐标1 2 3 4 5 6 7 8 9 10
    printf("\n");
    for (i = 1; i <=row; i++)
    {
        printf("%2d ", i);//打印行坐标1 2 3 4 5 6 7 8 9 10
        for (j = 1; j <=row; j++)
        {
            printf("%c ", board[i][j]);
        }
        printf("\n");
    }
}

void SetBoard(char board[ROWS][COLS], int row, int col,int count)//布雷
{
    int x = 0;
    int y = 0; 
    while (count)
    {
        x = rand() % row + 1;//保证行坐标在1到10之间
        y = rand() % col + 1;//保证列坐标在1到10之间
        if (board[x][y] == '0')//判断该位置是否布过雷
        {
            board[x][y] = '1';//‘1’代表有雷
            count--;//布一次雷,雷数减一
        }
    }

}

int GetCount(char board[ROWS][COLS], int x, int y)//统计雷数
{
    return board[x - 1][y] +
        board[x - 1][y - 1] +
        board[x][y - 1] +
        board[x + 1][y - 1] +
        board[x + 1][y] +
        board[x + 1][y + 1] +
        board[x][y + 1] +
        board[x - 1][y + 1] - 8 * '0';//将该位置周围八个位置的情况(是否有雷)计数
}

void expend(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y, int *num)//扩展式排雷(递归)
{
    int i = 0;
    int j = 0;
    if (board2[x][y]=='*')//如果该位置字符为'*',则该位置是未排过的,进行排雷
    {
        (*num)++;//排雷次数加一
        int count = GetCount(board1,x,y);//统计该位置周围的雷数
        if (count != 0)//如果该位置周围的雷数不为0
        {
            board2[x][y] = count + '0';//显示雷数,则该位置的字符不为'*',可避免下次排雷重复排到该位置
        }
        else//如果该位置的雷数为0,则向它周围八个位置扩展排雷
        {
            board2[x][y] = '0';//该位置的字符显示为‘0’
            for (i = -1; i <= 1; i++)
            {
                for (j = -1; j <= 1; j++)
                {
                    if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)//耗费我3个小时调试时间的不起眼的小错误!!!!!!!
                    {
                        if (i != 0 || j != 0)//避免重复排到自己
                            expend(board1, board2, x + i, y + j, num);
                    }
                }
            }
        }
    }
}

void sign(char board[ROWS][COLS], int x, int y)//用‘@’标记雷
{
    if (board[x][y] == '*')
    {
        board[x][y] = '@';
    }
}

void unsign(char board[ROWS][COLS], int x, int y)//取消标记
{
    if (board[x][y] == '@')
    {
        board[x][y] = '*';
    }
}

test.c

#include "game.h"

void game()
{
    int x = 0;
    int y = 0;
    int win = 0;//判定输赢的排雷次数
    int select = 0;//决定游戏功能的变量
    char mine[ROWS][COLS] = { 0 };//存雷的数组(雷盘)
    char show[ROWS][COLS] = { 0 };//展示的数组(雷盘)

    InitBoard(show, ROWS, COLS, '*');//展示的数组(雷盘)初始化为'*'
    InitBoard(mine, ROWS, COLS, '0');//存雷的数字(雷盘)初始化为'0'

    SetBoard(mine, ROW, COL, EASY_COUNT);//布雷
    DisplayBoard(show, ROW, COL);//打印雷盘

    while (win<(ROW*COL-EASY_COUNT))//当排雷的次数不少于无雷格数(雷盘格数 减 雷数)时,停止排雷
    {
        printf("请选择功能:\n1 for 排雷;2 for 标记 3 for 取消标记 4 for 再来一局\n");
        scanf("%d", &select);
        fflush(stdin);//清空输入缓冲区,避免多输造成的影响
        if (select == 1)//1 for 排雷
        {
            printf("请输入坐标:\n");
            scanf("%d%d", &x, &y);
            fflush(stdin);//清空输入缓冲区,避免多输造成的影响
            if (x >= 1 && x <= ROW&&y >= 1 && y <= COL)//检验坐标是否合法
            {
                if (mine[x][y] == '1')//如果所选位置有雷,判断是否为第一次排雷
                {
                    if (win != 0)//如果不是第一次排雷,宣布游戏结束
                    {
                        DisplayBoard(mine, ROW, COL);//排雷失败后打印一下雷的分布
                        printf("你被炸死了!\n");
                        return;
                    }
                    else//如果是第一次排雷,将这颗雷转移到其他位置,保证第一次不会排到雷
                    {
                        mine[x][y] = '0';
                        SetBoard(mine, ROW, COL, 1);
                        expend(mine, show, x, y, &win);
                    }
                }
                else//如果所选位置没有雷,进行扩展式排雷
                {
                    expend(mine, show, x, y, &win);
                }
                DisplayBoard(show, ROW, COL);//打印排雷后的雷盘
            }
            else
            {
                printf("错误坐标:\n");
            }
        }
        else if (select==2)//2 for 标记
        {
            printf("请输入坐标:\n");
            scanf("%d%d", &x, &y);
            fflush(stdin);//清空输入缓冲区,避免多输造成的影响
            sign(show,x,y);
            DisplayBoard(show,ROW,COL);
        }
        else if (select==3)//3 for 取消标记
        {
            printf("请输入坐标:\n");
            scanf("%d%d", &x, &y);
            fflush(stdin);//清空输入缓冲区,避免多输造成的影响
            unsign(show, x, y);
            DisplayBoard(show, ROW, COL);
        }
        else if (select==4)//4 for 结束游戏
        {
            return;
        }

    }
    printf("排雷成功!\n");
}

void menu()
{
    int num = 0;
    srand((unsigned int)time(NULL));//产生随机种子,用于随机布雷
    do
    {
        printf("*********1.play           0.exit**********");
        printf("请选择:\n");
        scanf("%d", &num);
        switch (num)
        {
        case 1:
            game();
            break;
        case 0:
            break;
        default:
            printf("选择错误!\n");
            break;
        }
    } while (num);

}

void test()
{
    menu();
}

int main()
{
    test();
    return 0;
}

截图:
程序开始运行
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)
排雷过程
小游戏:扫雷 (C语言实现扫雷的基本功能)
标记雷
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)
取消标记
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)
扫雷失败
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)
重新开始游戏
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)
扫雷成功
小游戏:扫雷 (C语言实现扫雷的基本功能)
小游戏:扫雷 (C语言实现扫雷的基本功能)

心得体会:
这个扫雷可以算是个小小小小的项目,对现阶段的我来说还不算难,但过程并不轻松。
我针对其中一个bug进行解释说明,为什么说过程并不轻松?
该程序判定扫雷成功的方式是:
在expend函数中传形参win计数(每排一次雷,win自加一次,当win不小于无雷格数时游戏结束)
代码如下:

//调用排雷的功能函数,将win传给形参num
expend(mine, show, x, y, &win);
//功能函数的具体内容
void expend(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y, int *num)//扩展式排雷(递归)
{
    int i = 0;
    int j = 0;
    if (board2[x][y]=='*')//如果该位置字符为'*',则该位置是未排过的,进行排雷
    {
        (*num)++;//排雷次数加一
        int count = GetCount(board1,x,y);//统计该位置周围的雷数
        if (count != 0)//如果该位置周围的雷数不为0
        {
            board2[x][y] = count + '0';//显示雷数,则该位置的字符不为'*',可避免下次排雷重复排到该位置
        }
        else//如果该位置的雷数为0,则向它周围八个位置扩展排雷
        {
            board2[x][y] = '0';//该位置的字符显示为‘0’
            for (i = -1; i <= 1; i++)
            {
                for (j = -1; j <= 1; j++)
                {
                    if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)//耗费我3个小时调试时间的不起眼的小错误!!!!!!!
                    {
                        if (i != 0 || j != 0)//避免重复排到自己
                            expend(board1, board2, x + i, y + j, num);
                    }
                }
            }
        }
    }
}

我程序中出现的bug是win计数出错(win总是大于实际排雷次数),
这个bug的具体错误耗费了我3个小时才找到,其实就是我把

   if (x + i >= 1 && x + i <=ROW && y + j >= 1 && y + j <=COL)
写成了:
 if (x + i >= 1 && x + i <ROWS && y + j >= 1 && y + j <COLS)

这样就导致排雷排到雷盘行和列的最边缘的位置时会出错
如图:
小游戏:扫雷 (C语言实现扫雷的基本功能)
排雷应该在1到10坐标之间(即黑框里)进行,当我们定义i和j小于ROWS的时候,在这个程序中就是小于12,则坐标为0到11(即红框里).
简单来说,就是本应该只在黑框内排雷,但是却把最外层(非雷区)也排了,这样就会增加排雷次数,从而导致win的数值不正常,总是大于90(雷格数10x10-雷数10=90)
在这里看来,您可能会想,“这么简单的bug你竟然花了3个小时!啧啧啧。。。”

其次,我觉得,真的是“当局者迷”,有时候自己真的很难发现自己的微不足道的错误=》小bug

唉。。。
正应了那句话,“程序员20%的时间花在编代码上,80%的时间花在调试上”。

OK!在此,我建议和我一样初学者们,在对一个程序编写之前,首先分析整个程序的流程和功能, 做到有模有块,如果脑海里实在理不出来步骤,不妨在纸上慢慢画出来或者写出来,
莫急于上手编代码!

最后,送给大家一句陈正冲先生的话,“调试代码才是最长水平的!”

相关标签: 扫雷 C语言