扫雷游戏详解(基础版+扩展版)

目录

前言

一、游戏规则

 二、程序大体思路

(1)多文件编程

(2)设计思路

棋盘的选择与制定

所需函数

大致流程

三、程序代码实现

(1)菜单menu函数

 (2)设计main函数

随机数

rand函数

srand函数

time函数

(3)设计game函数

(4)棋盘初始化InitBoard函数

(5)打印棋盘PrintBoard函数

(6)布置地雷SetMine函数

(7)周围雷个数MineCount函数

(8)排查雷FindMine函数

(9)头文件中函数声明

四、全部代码(基础版)

minesweeper.c

game.c

game.h

五、运行实操

六、扩展版(仅供参考)

(1)清屏效果

(2)标记地雷

(3)展开周围一片

(4)选择游戏难度

七、扩展版运行视频

八、尾声


前言

扫雷都玩过吧,没玩过也总得听说过吧。这篇文章将详细讲解扫雷游戏的代码实现,带领大家探索扫雷游戏背后有趣的奥秘。

    (网页扫雷游戏:http://www.minesweeper.cn/

    

一、游戏规则

盘面上有许多方格,方格中随机分布着一些雷。 你的目标是避开雷,打开其他所有格子。 一个非雷格中的数字表示其相邻8格中的雷数,你可以利用这个信息推导出安全格和雷的位置。 你可以用右键在你认为是雷的地方插旗(称为标雷)。 你可以用左键打开安全的地方,左键打开雷将被判定为失败。

 二、程序大体思路

(1)多文件编程

    扫雷游戏的代码较多,而且需要很多自定义函数。应用多文件编程,在另一个文件中定义这些函数,通过引用文件,可以在主文件中直接调用函数。这样可以使编程界面整洁、思路清晰,有利于查找并修正错误,提高效率。

    各个功能模块分成多个文件同时编辑,可以有效的提高团队开发的分工协作效率,养成多文件编程的好习惯,有利于未来学习和工作。

    在该项目中,需要创建三个文件。一个.h结尾的头文件,两个.c结尾的源文件

     

minesweeper.c    负责编写程序主体(整体构架),含有main函数,是程序运行的入口。

game.c    负责对程序中几个重要函数进行定义。

game.h    引用多个标准库的头文件,对程序中几个重要的变量进行宏定义,声明game.c中的函                      数,在主文件中只需要引用game.h即可。

(2)设计思路

    我们第一个目标是9*9的最基础版扫雷(只能逐个展开,无法标记雷)。

棋盘的选择与制定

棋盘要通过二维数组来实现,我们先考虑一个9*9二维数组

游戏规则规定,若查找的位置没有地雷,需要显示周围8格中地雷的个数,对于棋盘边缘的格子而言,它的周围不足8个格子,这样会导致写代码很麻烦,而且很容易出错(越界)。因此,我们可以考虑在棋盘周围再加一圈格子,即11*11,但无需在最外圈布置地雷,这样就完美解决了越界问题。

用字符0表示没有雷,用字符1表示有雷。进行排查后会显示雷的数量信息,若只在一个棋盘上操作,会扰乱原本信息而出错。因此,我们可以定义两个棋盘,一个用于布置雷,储存雷的信息,是隐藏起来的(mine棋盘),另外一个用于显示给玩家,存放排查后的雷的信息(show棋盘)。

(需要用到字符,方便起见,两个棋盘都定义为字符型二维数组,因此mine棋盘中用的是字符0和字符1)

所需函数

game.c:

    1.对棋盘进行初始化的函数

    2.打印棋盘的函数

    3.随机布置雷的函数

    4.玩家排查雷的函数(主体)

    5.计算周围雷的个数的函数 

minesweeper.c:

    1.打印游戏菜单函数(menu())

    2.进入游戏的函数(game())

    3.随机布置雷,需要rand(),srand(),time()函数

    4.main函数

大致流程

1.打印菜单,玩家选择是否进入游戏

2.进入游戏,对两个棋盘进行处理,玩家开始游戏

3.游戏:玩家输入坐标,若是雷,则游戏结束,并打印mine棋盘;若不是雷,显示周围雷数      量,打印更新后的show棋盘,继续输入坐标。若最后游戏成功,结束游戏,打印mine棋盘

三、程序代码实现

(1)菜单menu函数

 “1”开始游戏,“0”退出游戏

代码:(minesweeper.c)

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

 (2)设计main函数

1.玩家要做出选择,需要定义变量 input

2.

随机数

后期需要随机埋雷,要用到随机数,这里介绍一下。

rand函数

函数原型:

int rand(void);

    rand函数会返回一个伪随机数(多次运行后发现每一次的“随机数”相同),这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是依赖编译器上实现的,但是大部分编译器上是32767。

    其实rand函数是对⼀个叫“种子”的基准值进行运算生成的随机数。之所以每次运行程序产生的随机数序列是一样的,那是因为rand函数生成随机数的默认种子是1。

    rand函数的使用需要包含⼀个头文件是:stdlib.h

srand函数

函数原型:

void srand (unsigned int seed);

    用于改变前面所说的“种子”,但这样还不够,因为种子一旦确定了,rand函数返回的依旧是“伪随机数”。

time函数

函数原型:

time_t time (time_t* timer);

    在程序中我们一般是使用程序运行的时间作为种子的,因为时间时刻在发生变化的。 在C语言中有一个函数叫time,就可以获得这个时间。

    time函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。time函数返回的这个时间差也被叫做:时间戳。

    time函数的时候需要包含头文件:time.h

(此处不再细讲,上网查找即可)

3.  do...while循环实现玩家选择进入游戏或退出游戏。

代码:(minesweeper.c)

int main()
{
    int input = 0;

    srand((unsigned int)time(NULL));

    do
    {
        menu();

        printf("请选择:>");

        scanf("%d", &input);

        switch(input)
        {
            case 1:
                game();
                break;
            case 0:
                printf("退出游戏\n");
                break;
            default:
                printf("请重新输入\n");
        }

    }while(input);

    return 0;
}

(3)设计game函数

1.创建两个棋盘,即两个11*11字符型二维数组,ROWS表示行数,COLS表示列数

    之后会常用到这两个数据,所以在头文件game.h中做如下宏定义

    

#define ROW 9
#define COL 9

#define ROWS ROW + 2
#define COLS COL + 2

2.对两个棋盘进行初始化,打印show棋盘,布置雷,扫雷

    全部通过调用函数实现

代码:(minesweeper.c)

void game()
{
    char mine[ROWS][COLS];

    char show[ROWS][COLS];

    InitBoard(mine, ROWS, COLS, '0');//初始化mine

    InitBoard(show, ROWS, COLS, '*');//初始化show

    PrintBoard(show, ROW, COL);//打印show

    SetMine(mine, ROW, COL);//在mine布置雷

    FindMine(mine, show, ROW, COL);//扫雷
}

(4)棋盘初始化InitBoard函数

四个形参:字符型二维数组,行,列,初始化的字符

设计思路是:通过双重for循环(行,列)为所有格子赋值,这里我们为mine棋盘所有格子赋                        值字符‘0’,为show棋盘所有格子赋值字符‘*’。

代码:(game.c)

void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark)
{
    int i = 0, j = 0;

    for (i = 0; i < rows; i++)
    {
        for (j = 0; j < cols; j++)
        {
            board[i][j] = mark;
        }
    }
}

(5)打印棋盘PrintBoard函数

三个形参:字符型二维数组,行,列

设计思路:打印棋盘内层9*9格子,为了优化输出效果,可以表明行、列序号,方便查找。                      另外,还可以在棋盘前面加上"------扫雷游戏------"这样的标头,整洁且直观。

代码:(game.c)

void PrintBoard(char board[ROWS][COLS], int row, int col)
{
    int i = 0, j = 0;

    printf("------扫雷游戏------\n");

    for (i = 0; i <= col; i++)//第一行,列标(‘0’用于空出第一列行标)
    {
        printf("%d ", i);
    }

    printf("\n");

    for (i = 1; i <= row; i++)
    {
        printf("%d ", i);//第一列,行标

        for (j = 1; j <= col; j++)
        {
            printf("%c ", board[i][j]);
        }

        printf("\n");
    }
}

(6)布置地雷SetMine函数

三个形参:字符型二维数组,行,列

设计思路:1.设置地雷个数,现在头文件中宏定义,再在.c文件中定义、初始化一个变量

                    game.h中

                    game.c中

                  2.while循环中布置雷,每布置一个,number--,减到零结束循环。

                  3.制造随机位置:

                     行列坐标x、y都是随机且独立的(1-9),对随机数函数rand函数进行如下操                         作,先除以9取余(rand()%9),这样得到的余数是0-8再加一(rand()                       %9+1),得到1-9的随机数。(函数中,我们对row和col进行取余)

                  4.布置雷,若随机坐标没有雷,就埋一个雷。

代码(game.h)

#define Mine_Number 10

代码(game.c)

void SetMine(char board[ROWS][COLS], int row, int col)
{
    int number = Mine_Number;

    while(number)
    {
        int x = rand() % row + 1;

        int y = rand() % col + 1;

        if(board[x][y] == '0')
        {
            board[x][y] = '1';

            number--;
        }
    }
}

(7)周围雷个数MineCount函数

三个形参:字符型二维数组,行坐标,列坐标

设计思路:通过双重for循环和if语句,对目标坐标为中心的九宫格进行统计,每有一个雷,                      变量number++。

                  注意这是一个int类型函数,最后要返回int类型值number。

代码:(game.c)

int MineCount(char mine[ROWS][COLS], int x, int y)
{
    int number = 0;

    for(int i = x-1; i <= x+1; i++)
    {
        for(int j = y-1; j <= y+1; j++)
        {
            if(mine[i][j] == '1')
            {
                number++;
            }
        }
    }

    return number;
}

(8)排查雷FindMine函数

四个形参:两个字符型二维数组,行,列

设计思路:1.定义三个变量,x(行坐标),y(列坐标),win(记录已排查数)

                  2.while循环,保证玩家排查出所有非雷区域后,显示游戏成功,正常退出。

                  3.玩家输入坐标,判断坐标合法性,若是雷,游戏结束,跳出循环;若不是雷,                       显示雷个数,继续排查。若坐标非法,让玩家重新输入。循环结束后,判断是                       否成功。

                  4.需要调用SetMine函数。

代码:(game.c)

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;//行坐标

    int y = 0;//列坐标

    int win = 0;//记录已排查数

    while (win < row*col - Mine_Number)
    {
        printf("请输入要排查的坐标:>");

        scanf("%d %d", &x, &y);

        if(x >= 1 && x <= row && y >= 1 && y <= col)
        {
            if(mine[x][y] == '1')
            {
                printf("嘿嘿嘿,你被炸死了\n");

                PrintBoard(mine, ROW, COL);

                break;
            }
            else
            {
                int count = MineCount(mine, x, y);

                show[x][y] = count + '0';//将个数(整型数字)转换为字符

                PrintBoard(show, ROW, COL);//展示当前状况

                win++;
            }
        }
        else
        {
            PrintBoard(show, ROW, COL);
            
            printf("输入坐标错误,请重新输入\n");
        }
    }

    if(win == row*col - Mine_Number)
    {
        printf("666,排雷成功\n");

        PrintBoard(mine, ROW, COL);
    }
}

(9)头文件中函数声明

再头文件game.h中对minesweeper.c需要调用的函数进行声明。

代码:(game.h)

void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark);//初始化

void PrintBoard(char board[ROWS][COLS], int row, int col);//打印

void SetMine(char board[ROWS][COLS], int row, int col);//设置地雷

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//扫雷

四、全部代码(基础版)

minesweeper.c

#include"game.h"

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

void game()
{
    char mine[ROWS][COLS];

    char show[ROWS][COLS];

    InitBoard(mine, ROWS, COLS, '0');//初始化mine

    InitBoard(show, ROWS, COLS, '*');//初始化show

    PrintBoard(show, ROW, COL);//打印show

    SetMine(mine, ROW, COL);//在mine布置雷

    FindMine(mine, show, ROW, COL);//扫雷
}

int main()
{
    int input = 0;

    srand((unsigned int)time(NULL));

    do
    {
        menu();

        printf("请选择:>");

        scanf("%d", &input);

        switch(input)
        {
            case 1:
                game();
                break;
            case 0:
                printf("退出游戏\n");
                break;
            default:
                printf("请重新输入\n");
        }

    }while(input);

    return 0;
}

game.c

#include"game.h"

//对棋盘进行初始化函数
void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark)
{
    int i = 0, j = 0;

    for (i = 0; i < rows; i++)
    {
        for (j = 0; j < cols; j++)
        {
            board[i][j] = mark;
        }
    }
}

//打印棋盘,注意只打印内层
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
    int i = 0, j = 0;

    printf("------扫雷游戏------\n");

    for (i = 0; i <= col; i++)
    {
        printf("%d ", i);
    }

    printf("\n");

    for (i = 1; i <= row; i++)
    {
        printf("%d ", i);

        for (j = 1; j <= col; j++)
        {
            printf("%c ", board[i][j]);
        }

        printf("\n");
    }
}

//在mine棋盘上布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
    int number = Mine_Number;

    while(number)
    {
        int x = rand() % row + 1;

        int y = rand() % col + 1;

        if(board[x][y] == '0')
        {
            board[x][y] = '1';

            number--;
        }
    }
}

//排查点周围雷的个数
int MineCount(char mine[ROWS][COLS], int x, int y)
{
    int number = 0;

    for(int i = x-1; i <= x+1; i++)
    {
        for(int j = y-1; j <= y+1; j++)
        {
            if(mine[i][j] == '1')
            {
                number++;
            }
        }
    }

    return number;
}

//最重要的函数,游戏的主体
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;//行坐标

    int y = 0;//列坐标

    int win = 0;//记录已排查数

    while (win < row*col - Mine_Number)
    {
        printf("请输入要排查的坐标:>");

        scanf("%d %d", &x, &y);

        if(x >= 1 && x <= row && y >= 1 && y <= col)
        {
            if(mine[x][y] == '1')
            {
                printf("嘿嘿嘿,你被炸死了\n");

                PrintBoard(mine, ROW, COL);

                break;
            }
            else
            {
                int count = MineCount(mine, x, y);

                show[x][y] = count + '0';//将个数(整型数字)转换为字符

                PrintBoard(show, ROW, COL);//展示当前状况

                win++;
            }
        }
        else
        {
            PrintBoard(show, ROW, COL);

            printf("输入坐标错误,请重新输入\n");
        }
    }

    if(win == row*col - Mine_Number)
    {
        printf("666,排雷成功\n");

        PrintBoard(mine, ROW, COL);
    }
}

game.h

#pragma once

#include<stdio.h>
#include<time.h>//time()函数需要
#include<stdlib.h>//rand()函数需要

#define ROW 30
#define COL 16

#define ROWS ROW + 2
#define COLS COL + 2

#define Mine_Number 99

void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark);//初始化

void PrintBoard(char board[ROWS][COLS], int row, int col);//打印

void SetMine(char board[ROWS][COLS], int row, int col);//设置地雷

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//扫雷

五、运行实操

输入0、1以外的数:

提示重新输入

输入1,进入游戏

输入非法坐标,提示错误,重新输入

没有踩雷,显示周围地雷个数,打印最新show棋盘,继续输入

多次操作后:

失败结局:(失败后,显示结果)

成功结局:

游戏结束后会再次显示菜单,输入0,退出游戏

六、扩展版(仅供参考)

(1)清屏效果

    运行实操中,可以发现已经“过时”的show棋盘依旧显示在屏幕上,导致输出界面繁杂。因此,考虑使用清屏,只显示当前最新棋盘。

    清屏要用到system函数,需要在适当位置添加以下语句:

system("cls");

调用system函数,需要头文件windows.h,在game.h文件中引用即可

    插入位置:需要在玩家输入坐标后清屏,并显示新棋盘,可以将插入到FindMine函数“输入xy语句”之后。

代码

minesweeper.c文件FindMine函数中:

printf("请输入要排查的坐标:>");

scanf("%d %d", &x, &y);

system("cls");//新加入的

game.h文件中:

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>//新加入的

效果展示:

(2)标记地雷

目标:通过输入,将show棋盘目标坐标的‘*’改为‘#’,并打印出来。通过再次输入,可将‘#’改             回‘*’。标记功能,有利于游戏体验。

修改位置:FindMine函数

思路:再定义变量z,输入x.y.z,在xy合法情况下,z=0则正常排雷,z=1修改字符,其余重               新输入。

代码(FindMine部分

int x = 0;

int y = 0;

int z = 0;//判断是否进行标记

int win = 0;

while (win < row * col - Mine_Number)
{
    printf("请输入坐标:>");

    scanf("%d %d", &x, &y);

    printf("请选择要进行的操作(0:排查,1:标记或去除标记):>");

    scanf("%d", &z);

    system("cls");

    if (x >= 1 && x <= row && y >= 1 && y <= col && z == 0)//z为0情况
    {
        if (mine[x][y] == '1')
        {
            printf("嘿嘿嘿,你被炸死了\n");

            PrintBoard(mine, ROW, COL);

            break;
        }
        else
        {
            int count = MineCount(mine, x, y);

            show[x][y] = count + '0';//将个数(整型数字)转换为字符

            PrintBoard(show, ROW, COL);//展示当前状况

            win++;
        }
    }
    else if (x >= 1 && x <= row && y >= 1 && y <= col && z == 1)//z为1情况
    {
        if (show[x][y] == '*')
        {
            show[x][y] = '#';

            PrintBoard(show, ROW, COL);
        }
        else if (show[x][y] == '#')
        {
            show[x][y] = '*';

            PrintBoard(show, ROW, COL);
        }
    }
    else
    {
        printf("输入坐标错误,请重新输入\n");
    }
}

(3)展开周围一片

目标:当查找坐标不是雷,而且周围连续一片也都不是雷时,会将这一片区域全部展示出来

           

           只排查红框格子,实际排查出的是一大片。

流程:每次对一个格子进行排查时,若周围8个没有雷,则展示这9个格子,并且会对其                    周围8个格子进行同样操作,以此类推。当周围有雷时,停止。

设计思路:使用递归函数,注意越界问题,注意避免进入死循环递归。

步骤一:定义“展开函数”Unfold()(在game.c文件中)

void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int row, int col)
{
    if (x<1 || x>row || y<1 || y>col)//坐标非法,退出函数
    {
        return;
    }

    if (show[x][y] != '*')//已被排查过,退出函数,这里一定要注意,避免进入死循环
    {
        return;
    }

    int count = MineCount(mine, x, y);

    if (count != 0)//周围有雷,显示雷数量
    {
        show[x][y] = count + '0';

        return;
    }
    else if(count == 0)//周围没有雷
    {
        show[x][y] = ' ';//将‘0’改为空格,是游戏画面更简洁、易分辨,增强游戏体验

        for (int i = x - 1; i <= x + 1; i++)对周围8格进行处理
        {
            for (int j = y - 1; j <= y + 1; j++)
            {
                Unfold(mine, show, i, j, ROW, COL);
            }
        }
    }
}

步骤二:对FindMine函数进行修改,这里只展示一部分代码(在game.c文件中)

if (x >= 1 && x <= row && y >= 1 && y <= col && z == 0)
{
    if (mine[x][y] == '1')
    {
        printf("嘿嘿嘿,你被炸死了\n");

        PrintBoard(mine, ROW, COL);

        break;
    }
    else
    {
        Unfold(mine, show, x, y, ROW, COL);//先进行连续展开操作

        PrintBoard(show, ROW, COL);//然后打印show棋盘

        int i;

        int j;

        for (win = 0, i = 1; i <= ROW; i++)//每一次通过遍历数组更新win
        {
            for (j = 1; j <= COL; j++)
            {
                if (show[i][j] != '*' && show[i][j] != '#')
                {
                    win++;
                }
            }
        }
    }
}

(4)选择游戏难度

目标:设置三种难度,供玩家选择
           简单9*9棋盘,10个雷

           中等16*16棋盘,40个雷

           困难30*16棋盘,99个雷

 问题一:按照之前的代码,输出16*16:

             

              可以发现,由于在PrintBoard函数中,只在%d和%c后加了一个空格,导致当行、列是两位数时,出现格式混乱的情况。若将空格换为\t,又会导致空格太多,运行界面盛不下。因此,考虑修改域宽,并进行缩进。

              代码如下:

for (i = 0; i <= col; i++)
{
    printf("%-3d", i);//改为%-3d,即占三个格,向左缩进,多余位置打印空格
}

printf("\n");

for (i = 1; i <= row; i++)
{
    printf("%-3d", i);//%-3d

    for (j = 1; j <= col; j++)
    {
        printf("%-3c", board[i][j]);//%-3c
    }

    printf("\n");
}

效果:

问题二:如何选择难度?

   不怕大家笑话,我指针还没学明白,只能用了一个非常麻烦、非常笨的办法:

我几乎把整个所有代码又复制了两份(包括宏定义、函数声明...),也就是将之前的ROW改成了ROW1、ROW2、ROW3......

    这里就不再拿全部代码献丑了,只放几张截图吧。

唯一值得说的是,我又定义了两个函数,让玩家做出选择,再根据玩家输入通过switch语句调用相应的函数。

//难度选项菜单
void DifficultyMenu()
{
    printf("***************************\n");
    printf("**** 1.简单  9*9  10雷*****\n");
    printf("**** 2.中等 16*16 40雷*****\n");
    printf("**** 3.困难 30*16 99雷*****\n");
    printf("***************************\n");
    printf("请选择难度:>");
}

//选择难度
int SelectDifficulty()
{
    int x;

    scanf("%d", &x);

    system("cls");

    return x;
}

七、扩展版运行视频

扫雷(扩展版)

八、尾声

    我用了完完整整的三天时间来完成的扫雷游戏代码以及这篇博客,这期间遇到了很多很多困难,毕竟我的知识储备太有限了。

    一开始我的目标是不仅完成基础的代码,还要实现很多扩展功能。但连续三天不停写代码、修bug,对我这个初学者来说简直就是地狱般的折磨。我多次想过放弃,但我终究坚持了下来。

    因为,我想,既然做了,就做到最好,不辜负最初那个目标坚定的自己。

    最后我想说,非常非常非常非常感谢大家的支持!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值