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

来源:互联网 发布:金庸小说排名 知乎 编辑:程序博客网 时间:2024/05/16 14:38

该程序分为三个文件:
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;}

截图:
程序开始运行
这里写图片描述
这里写图片描述
排雷过程
这里写图片描述
标记雷
这里写图片描述
这里写图片描述
取消标记
这里写图片描述
这里写图片描述
扫雷失败
这里写图片描述
这里写图片描述
重新开始游戏
这里写图片描述
这里写图片描述
扫雷成功
这里写图片描述
这里写图片描述

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

//调用排雷的功能函数,将win传给形参numexpend(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)

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

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

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

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

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

原创粉丝点击