• 用C语言写个控制台扫雷游戏(附完整代码)


    前言

      扫雷游戏大家应该都玩过,虽然win10以后更新了,但还是win7版的好用,在代码写烦了来上几把换换脑子还是不错滴。

      下面用c语言模仿win7版的写一个控制台扫雷游戏。


    一、对控制台的控制

      游戏中要对控制台的大小、文字颜色、光标大小、光标位置、鼠标输入等需要进行一些设置,下面列出所需要的一些函数。

    1.控制台大小

      控制台大小通过mode命令设置。这个命令中的大小是指行和列的字符数而非像素。

    //设置控制台大小
    void SetSize(unsigned uCol, unsigned uLine)
    {
        char cmd[64];
        sprintf(cmd, "mode con cols=%d lines=%d", uCol, uLine);
        system(cmd);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.控制台颜色

      控制台颜色设置详情可查看: 用C代码设置Windows控制台颜色

    //更改文字颜色
    // color为每一种颜色所代表的数字,范围是0~15
    void setColor(WORD color)
    {
        HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
        SetConsoleTextAttribute(HOutput, color);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.控制台光标样式

      更改光标样式要用到结构体CONSOLE_CURSOR_INFO 和控制台API函数SetConsoleCursorInfo,通过这个结构体和函数可以设置光标的可见性和大小。

    // 设置光标样式
    void SetCursorType(BOOL bVisible)
    {
        HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_CURSOR_INFO curInfo;
        curInfo.bVisible = bVisible; //更改光标显示状态,TRUE显示光标,FALSE隐藏光标
        curInfo.dwSize = 100;        // 光标大小1到100之间。 范围从完全填充单元格到单元底部的水平线条。
        SetConsoleCursorInfo(HOutput, &curInfo);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4.控制台光标位置

      这个函数用的比较多,用来定位要输出的字符的位置。

    //设置光标位置,起点从1开始
    void MoveCursorTo(int nCols, int nRows)
    {
        COORD crdLocation = {nCols, nRows};
        HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
        SetConsoleCursorPosition(HOutput, crdLocation); //设置光标位置
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5.获取控制台光标位置字符

      这个函数用的比较多,用来定位要输出的字符的位置。

    /// 提取出窗口中第x行y列的位置的字符
    /// @return 返回指定位置显示的字符,空白处返回空格
    char GetCursorStr(int x, int y)
    {
        COORD pos = {x, y};
        char str = 0;
        DWORD read;
        // HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
        ReadConsoleOutputCharacterA(HOutput, &str, 1, pos, &read);
        return str;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    6.控制台显示位置

      通过API函数MoveWindow设置窗口的位置。在设置窗口位置之前,要先取得控制台窗口大小和屏幕大小,通过计算得出屏幕居中的位置。

    //居中显示控制台
    BOOL MoveConsoleWindow()
    {
        //获取屏幕大小
        HDC hdc = GetDC(NULL);
        int cx = GetDeviceCaps(hdc, DESKTOPHORZRES); //窗口左侧位置
        int cy = GetDeviceCaps(hdc, DESKTOPVERTRES); //窗口顶部位置
        ReleaseDC(NULL, hdc);
    
        //获取控制台大小
        HWND HOutput = GetConsoleWindow();
        RECT rect;
        GetWindowRect(HOutput, &rect);
    
        int w = rect.right - rect.left; //窗口宽度
        int h = rect.bottom - rect.top; //窗口高度
        cx = (cx - w) / 2;
        cy = (cy - h) / 2;
        return MoveWindow(HOutput, cx, cy, w, h, TRUE); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    7.控制台鼠标操作设置

      除了上面,还需要对控制台的鼠标操作进行屏蔽。

    
    #define DISABLE_QUICK_EDIT_MODE 0x01
    #define DISABLE_INSERT_MODE 0x02
    #define DISABLE_MOUSE_INPUT 0x03
    #define DISABLE_ALL (DISABLE_QUICK_EDIT_MODE | DISABLE_INSERT_MODE | DISABLE_MOUSE_INPUT)
    
    //执行控制台相关设置
    void CloseConsoleMode(UINT uTag)
    {
        HANDLE HInput = GetStdHandle(STD_INPUT_HANDLE);//这里用标准输入设备
        DWORD mode;
        GetConsoleMode(HInput, &mode);
        if (uTag & DISABLE_QUICK_EDIT_MODE)
            mode &= ~ENABLE_QUICK_EDIT_MODE; //移除快速编辑模式
        if (uTag & DISABLE_INSERT_MODE)
            mode &= ~ENABLE_INSERT_MODE; //移除插入模式
        if (uTag & DISABLE_MOUSE_INPUT)
            mode &= ~ENABLE_MOUSE_INPUT; //移除鼠标输入
        SetConsoleMode(HInput, mode);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

      

    二、游戏界面

    1.游戏难度选择界面

      win版的扫雷有3种难度和自定义难度,这里通过选择界面选择游戏难度。
    先看看效果
    选择界面_自定义

    1.1 基本定义

      定义结构体保存游戏的格子数量和地雷数量。

    
    #define CONSOLEWIDTH 80  //控制台宽度
    #define CONSOLEHEIGHT 40 //控制台高度
    #define DEFCOLOR 0x7     //控制台默认文字颜色
    
    typedef struct _point
    {
        short x;
        short y;
    } Point;
    
    //格子大小
    typedef struct _MapSize
    {
        Point MapSize; //格子数量
        short Mine_Count; //地雷数量
    } Map_Size;
    
    //按照win扫雷定义尺寸和雷数
    Map_Size defMapSize[3] = {{{9, 9}, 10}, {{16, 16}, 40}, {{30, 16}, 99}};
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.2显示标题信息

    • 使用setColor更改文字颜色
    • sprintf给buf输出字符宽度控制居中显示。
    • 显示输出后文字颜色改回默认颜色
    void PrintName()
    {
        system("cls");
        setColor(0x2);
        char buf[100];
        int l = (CONSOLEWIDTH - 20) / 2 + 20;
        sprintf(buf, "\n%%%ds\n", l);
        printf(buf, "欢迎来到扫雷小游戏");
        sprintf(buf, "%%%ds\n", (CONSOLEWIDTH - 16) / 2 + 16);
        printf(buf, "-- by Apull --");
        setColor(DEFCOLOR);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.3显示难度选项

    • 不同难度使用不同颜色显示
    • 游戏界面中横向格子之间用空格隔开,因此在选定难度后,横向格子数量需要翻倍,更改为MapSize.MapSize.x * 2 - 1
    //难度选择
    void Choose()
    {
        MapSize = defMapSize[0];
        PrintName();
        printf("\t\t选择游戏难度:\n");
        
        setColor(0xA);
        printf("\t\t  1、初级:%dx%d,共%d个地雷\n", defMapSize[0].MapSize.x, defMapSize[0].MapSize.y, defMapSize[0].Mine_Count);
        setColor(0xE);
        printf("\t\t  2、中级:%dx%d,共%d个地雷\n", defMapSize[1].MapSize.x, defMapSize[1].MapSize.y, defMapSize[1].Mine_Count);
        setColor(0xc);
        printf("\t\t  3、高级:%dx%d,共%d个地雷\n", defMapSize[2].MapSize.x, defMapSize[2].MapSize.y, defMapSize[2].Mine_Count);
        setColor(0x9);
        printf("\t\t  4、自定义难度\n");
        setColor(DEFCOLOR);
        printf("\t\t  0、退出\n");
        printf("\t\t输入你的选择(回车默认选择1): ");
        
        char ch;
        BOOL inputOK = TRUE;
        while (inputOK)
        {
            ch = _getch();
            switch (ch)
            {
            case '1':
            case Key_ENTER:
                MapSize = defMapSize[0];
                inputOK = FALSE;
                break;
            case '2':
                MapSize = defMapSize[1];
                inputOK = FALSE;
                break;
            case '3':
                MapSize = defMapSize[2];
                inputOK = FALSE;
                break;
            case '4':
                printf("\n");
                MapSize.MapSize.y = OptMines("\t\t  高度:", 9, 24);
                MapSize.MapSize.x = OptMines("\t\t  宽度:", 9, 30);
                MapSize.Mine_Count = OptMines("\t\t  雷数:", 10, 668);
                inputOK = FALSE;
                break;
            case '0':
            case Key_ESC:
                EXIT(0);
                break;
            default:
                break;
            }
        }
        MapSize.MapSize.x = MapSize.MapSize.x * 2 - 1;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    1.4 选择自定义难度

    • 为了防止输入错误后的无限循环,这里使用字符串接收输入,再转换成int类型。
    • 设定地雷数量是所有格子数量的20%
    • 输入错误使用红色文字提示
    //自定义难度
    int OptMines(char *str, int min, int max)
    {
        int x;
        char buf[20] = {0};
        if (max > 30) // max > 30 表示输入的是雷数
        {
            max = (int)(MapSize.MapSize.x * MapSize.MapSize.y * 0.2); //地雷数量是所有格子数的20%
        }
        while (TRUE)
        {
            printf(str);
            printf("(%d-%d):", min, max);
            gets_s(buf, 20);
    
            if (buf[0] == '0')
                EXIT(0);
            x = atoi(buf);
            if (x < min || x > max)
            {
                setColor(0x04);
                printf("\t\t输入超出范围,请重新输入。\n");
                setColor(DEFCOLOR);
            }
            else
                return x;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2 游戏界面

    游戏界面

    2.1 基本定义

    2.1.1 定义地雷格子结构

      结构体struct _mine中定义一个格子要显示的字符、周围8个方向上地雷数量以及格子本身的状态。

    //定义地雷格子状态
    enum Mine_STATU
    {
        HIDDEN_SAFE, 
        VISIBLE_SAFE,
        HIDDEN_MINE
    };
    
    //地雷格子定义
    typedef struct _mine
    {
        char ch;
        char hasMines;
        enum Mine_STATU statu;
    } Mine;
    
    #define FLAG '@'		//标记的地雷
    #define MAPNORMAL 'o'	//正常格子
    #define MINEICO '*'		//地雷
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    2.1.2 填充地雷格子

      使用二维数组minefield存储每个格子,根据难度选择给二维数组分配内存大小。

    Mine **minefield = NULL;    //格子数组
    
    //生成格子数组,并填充显示字符
    Mine **initMapArr()
    {
        Mine **minefield = (Mine **)calloc(MapSize.MapSize.y, sizeof(Mine *));
        if (minefield == NULL)
        {
            printf("内存分配错误!");
            EXIT(-1);
        }
        for (int i = 0; i < MapSize.MapSize.y; i++)
        {
            minefield[i] = (Mine *)calloc(MapSize.MapSize.x, sizeof(Mine));
            if (minefield == NULL)
            {
                printf("内存分配错误!");
                EXIT(-1);
            }
            for (int j = 0; j < MapSize.MapSize.x; j++)
            {
    
                minefield[i][j].ch = (j % 2) ? ' ' : MAPNORMAL;
            }
        }
        return minefield;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    2.1.3 产生地雷
    • 使用动态数组MinesPos保存地雷所在位置
    • 用随机数产生地雷
    • 检索重复项,有重复的则重新生成地雷。
    Point *MinesPos = NULL;     //地雷所在位置
    Point *FindMinesPos = NULL; //标记的地雷所在位置
    int MineCount = 0;          //地雷总数
    
    //产生地雷
    void init_Mine(Point *point, int MineCount)
    {
        Point pot;
        for (int i = 0, j; MineCount > i;)
        {
            do
            {
                pot.x = rand() % MapSize.MapSize.x;
            } while (pot.x % 2 != 0);
    
            pot.y = rand() % MapSize.MapSize.y;
            for (j = 0; j < i; j++)
            {
                if (point[j].x == pot.x && point[j].y == pot.y)
                    break;
            }
            if (i == j)
            {
                point[i].x = pot.x;
                point[i].y = pot.y;
                i++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    2.1.4 将地雷放置到格子中

      数组MinesPos中保存着地雷的坐标,直接在格子中把这些坐标标记为地雷。

    //写入雷的位置
    void init_Map(Mine **minefield, Point *point, int MineCount)
    {
        for (int i = 0; i < MineCount; i++)
        {
            minefield[point[i].y][point[i].x].statu = HIDDEN_MINE;
            minefield[point[i].y][point[i].x].hasMines = -1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    2.1.5 计算周围地雷数量
    • 按行列扫描所有格子
    • 对一个格子的周围8个位置的地雷数量进行统计
    • 将统计数量保存到格子的hasMines属性中
    //计算周围地雷数量
    void calMine(Mine **minefield)
    {
        int count;
        for (int i = 0; i < MapSize.MapSize.y; i++)
        {
            for (int j = 0; j < MapSize.MapSize.x; j += 2)
            {
                if (minefield[i][j].statu == HIDDEN_MINE)
                    continue;
    
                count = 0;
                if (i > 0 && j > 1 && minefield[i - 1][j - 2].statu == HIDDEN_MINE) //左上角
                    count++;
                if (i > 0 && minefield[i - 1][j].statu == HIDDEN_MINE) //上面
                    count++;
                if (i > 0 && j < MapSize.MapSize.x - 2 && minefield[i - 1][j + 2].statu == HIDDEN_MINE) //右上角
                    count++;
    
                if (j < MapSize.MapSize.x - 1 && minefield[i][j + 2].statu == HIDDEN_MINE) //右面
                    count++;
                if (i < MapSize.MapSize.y - 1 && j < MapSize.MapSize.x - 2 && minefield[i + 1][j + 2].statu == HIDDEN_MINE) //右下角
                    count++;
    
                if (i < MapSize.MapSize.y - 1 && minefield[i + 1][j].statu == HIDDEN_MINE) //下面
                    count++;
                if (i < MapSize.MapSize.y - 1 && j > 0 && minefield[i + 1][j - 2].statu == HIDDEN_MINE) //左下角
                    count++;
    
                if (j > 0 && minefield[i][j - 2].statu == HIDDEN_MINE) //左面
                    count++;
                    
                minefield[i][j].hasMines = count;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    2.1.6 其他变量

      

    Point StatuRow;             //状态显示行位置
    clock_t start_t, end_t;     //记录游戏时间
    //格子范围,限定地雷显示和操作的范围
    int top = 0, left = 0, right = 0, bottom = 0;
    
    • 1
    • 2
    • 3
    • 4

    2.2 显示游戏介绍

      显示游戏介绍,并返回显示的行数。

    // 输出游戏介绍
    int intro()
    {
        int line = 3; //加上之前的标题信息2行
        line += 7;
        StatuRow.y = line - 2;
        StatuRow.x = left;
    
        PrintName(); //显示标题信息
        printf("\t\t游戏方法介绍:\n");
        
        setColor(0xE);
        printf("\t\t  W、S、A、D/↑、↓、←、→");
        setColor(DEFCOLOR);
        printf(":控制光标上下左右移动\n");
        
        setColor(0xE);
        printf("\t\t  PageUP、PageDown、Home、End");
        setColor(DEFCOLOR);
        printf(":控制光标跳到四边位置\n");
        
        setColor(0xE);
        printf("\t\t  空格");
        setColor(DEFCOLOR);
        printf(":打开当前格子");
        
        setColor(0xE);
        printf("  R");
        setColor(DEFCOLOR);
        printf(":回到选择界面");
        
        setColor(0xE);
        printf("  ESC");
        setColor(DEFCOLOR);
        printf(":结束游戏\n");
        
        ShowStatu();
        printf("\n\n");
        
        return line;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    2.3 显示游戏状态

      显示游戏信息和游戏时间,光标移动到要输出的位置,输出内容覆盖就内容,完成状态刷新。
      在输出状态的时候要移动光标,游戏过程中会看到光标跳到状态显示位置后有会跳回格子中,为避免这种情况,在开始输出状态时使用SetCursorType隐藏光标,输出完毕后再显示光标

    //显示游戏状态
    void ShowStatu()
    {
        static int total_t = 0;
        end_t = clock();
        total_t = (end_t - start_t) / CLOCKS_PER_SEC;
        SetCursorType(FALSE);
        MoveCursorTo(StatuRow.x, StatuRow.y);
        printf("\t\t共有雷数:%-3d  剩余雷数:%-3d  用时:%ds   ", MapSize.Mine_Count, MineCount, total_t);
        SetCursorType(TRUE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.4 显示地雷格子

    • 根据上面显示的信息行数得到地图格子要显示的起始行
    • 根据上面基本定义的宽度CONSOLEWIDTH计算居中显示的起始列
    • 每一行光标移动到左边起始列后输出行
    
    //确定绘制地雷格子范围
    void DrawMine()
    {
        left = right = bottom = 0;
        top = intro();
        drawMap(minefield);
    
        right = MapSize.MapSize.x + left;
        bottom += MapSize.MapSize.y + top - 1;
        MoveCursorTo(left, top);
        start_t = clock(); //开始计时
    }
    
    //绘制地雷格子和外框,设置各边范围
    void drawMap(Mine **minefield)
    {
        int l = (CONSOLEWIDTH - MapSize.MapSize.x) / 2 - 4;
        if (l % 2 == 0)
            l++;
        int t = top;
        MoveCursorTo(l, t++);
    
        printf("┌");
        for (int i = 0; i <= MapSize.MapSize.x; i++)
            printf("─");
            
        printf("┐\n");
        MoveCursorTo(l, t++);
        // 输出中间部分
        for (int i = 0; i < MapSize.MapSize.y; i++)
        {
            printf("│ ");
            for (int j = 0; j < MapSize.MapSize.x; j++)
            {
                printf("%c", minefield[i][j].ch);
            }
            printf("│\n");
            MoveCursorTo(l, t++);
        }
        printf("└");
        for (int i = 0; i <= MapSize.MapSize.x; i++)
            printf("─");
            
        printf("┘\n");
    
        top += 1;
        left = l + 2;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

      至此地雷界面绘制基本完成,下面来看看操作部分。
      

    三、游戏操作

      

    1 游戏按键输入

    1.1 定义按键

      定义出后面要用到达按键变量

    //按键定义
    enum KB_KEY
    {
        Key_FN = 0xE0,       // 功能键标志
        Key_Up = 0x48,       // 向上方向键   
        Key_Down = 0x50,     // 向下方向键
        Key_Left = 0x4b,     // 向左方向键
        Key_Right = 0x4d,    // 向右方向键
        Key_Home = 0x47,     // Home键
        Key_End = 0x4f,      // End键
        Key_PageUp = 0x49,   // PageUp键
        Key_PageDown = 0x51, // PageDown键
        Key_ESC = 0x1B,      // ESC键
        Key_ENTER = 0xD,     // 回车键
        Key_SPACE = 0x20,    // 空格键
        Key_A = 'A',
        Key_a = 'a',
        Key_D = 'D',
        Key_d = 'd',
        Key_W = 'W',
        Key_w = 'w',
        Key_S = 'S',
        Key_s = 's',
        Key_R = 'R',
        Key_r = 'r'
    } KEY;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    1.2 读取按键输入

    • 需要注意的是方向键的输入,方向键是先输入0xE0或0,再输入对应的键值,因此输入方向键的输入需要读取2次才能确定。
    • 使用rowcol记录当前光标位置,列使用奇数位,因此列的移动要±2。
    • 检测移动后光标会不会超出现实范围,如果有超出情况则取消移动。
    • 回车 标记地雷
    • 空格 点开地雷格子
    • R 结束游戏回到选择界面
    • ESC 退出游戏
    int row = top; //行
    int col = left; //列
    
    char ch;
    int chInput = 0;
    int isGame = 1;  //游戏继续标志
    int x, y;
    while (isGame)
    {
    	if (_kbhit() != 0) //当_kbhit返回值不为0的时候,代表用户有按键按下。
    	{
    		chInput = _getch();
    		if (chInput == Key_FN || chInput == 0) // 方向键读2次
    			chInput = _getch();
    
    		switch (chInput) //控制操作
    		{
    		case 'a':
    		case 'A':
    		case Key_Left:
    			col -= 2;
    			if (col <= left)
    				col = left;
    			break;
    		case 'd':
    		case 'D':
    		case Key_Right:
    			col += 2;
    			if (col >= right)
    				col = right - 1;
    			break;
    		case 'w':
    		case 'W':
    		case Key_Up:
    			row--;
    			if (row <= top)
    				row = top;
    			break;
    		case 's':
    		case 'S':
    		case Key_Down:
    			row++;
    			if (row >= bottom)
    				row = bottom;
    			break;
    		case Key_SPACE: //空格点开
    			...
    			break;
    		case Key_ENTER: //标记地雷
    			...
    			break;
    		case Key_Home: //跳到第一列
    			col = left;
    			break;
    		case Key_End: //跳到最后一列
    			col = right - 1;
    			break;
    		case Key_PageUp: //跳到第一行
    			row = top;
    			break;
    		case Key_PageDown: //跳到最后一行
    			row = bottom;
    			break;
    		case Key_ESC: //退出游戏
    			isGame = 0;
    			break;
    		case Key_R:
    		case Key_r: //复位到选择界面
    			init();
    			DrawMine();
    			break;
    		default: //其他输入字符不动作
    			break;
    		}
    
    		MoveCursorTo(col, row);
    	}
    	Sleep(150);
    	ShowStatu();
    	MoveCursorTo(col, row);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    2 标记地雷

    • 回车 标记地雷。
    • 读取光标处的字符,光标处是未点开格子则标记地雷。
    • 标记的地雷坐标保存到FindMinesPos数组中。
    • 如果该位置已标记则取消标记,同时从FindMinesPos数组中删除。
    • 标记后判断所有的地雷是否都已标记,如果都已标记则判赢
    • 标记后光标右移到下一个地雷格子
    case Key_ENTER: //标记地雷
    	if (flagMine(row, col))
    		GameOver(WIN_GAME);
    	col += 2;
    	if (col >= right)
    		col = right - 1;
    	break;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //标记地雷
    BOOL flagMine(int row, int col)
    {
        char ch;
        BOOL iswin = FALSE;
        ch = GetCursorStr(col, row); //获取光标处字符
        if (ch == FLAG && MineCount < MapSize.Mine_Count) //切换已标记
        {
            FindMinesPos[MineCount].x = FindMinesPos[MineCount].y = -1;
            setColor(DEFCOLOR);
            putchar(MAPNORMAL);
            MineCount++;
        }
        else if (ch == MAPNORMAL && MineCount > 0)
        {
            MineCount--;
            FindMinesPos[MineCount].x = col - left;
            FindMinesPos[MineCount].y = row - top;
    
            setColor(0xC);
            putchar(FLAG);
    
            if (MineCount == 0)
            {
                int i;
                for (i = 0; i < MapSize.Mine_Count; i++) // 确定是否标记了所有的雷
                {
                    if (findMine4Pos(FindMinesPos[i].x, FindMinesPos[i].y) == FALSE)
                        break;
                }
                iswin = i == MapSize.Mine_Count;
            }
        }
    
        setColor(DEFCOLOR);
        ShowStatu(); //更新标记的雷数
        MoveCursorTo(col, row);
        return iswin;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3 点开地雷操作

    3.1 点开地雷格子

    • 空格 点开地雷格子
    • 读取光标位置字符,如果是已标记地雷则结束点开操作。
    • 如果点开的位置是地雷,游戏结束,显示游戏结束画面
    • 点开位置安全,则自动点开相连的安全区域。
    • 自动点开后判断是否一点开所有安全区,判断胜负。
    • 点开后光标右移到下一个地雷格子
    case Key_SPACE: //空格点开
    	x = col - left;
    	y = row - top;
    	ch = GetCursorStr(col, row);
    	if (ch == FLAG) //忽略点开已标记地雷
    		break;
    
    	if (minefield[y][x].statu == HIDDEN_MINE) //点开雷,游戏结束
    	{
    		isGame = GameOver(LOSS_GAME);
    		row = top;
    		col = left;
    		continue;
    	}
    	else //自动点开连续的安全区
    	{
    		SetCursorType(FALSE);
    		OpenMine(y, x);
    		SetCursorType(TRUE);
    		if (isWin())
    		{
    			isGame = GameOver(WIN_GAME);
    			row = top;
    			col = left;
    			continue;
    		}
    	}
    
    	col += 2;
    	if (col >= right)
    		col = right - 1;
    	break;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    3.2 自动展开安全区域

    • 递归对当前格子周围的安全区域进行展开操作
    • 递归到边界或已点开区域终止
    • 点开时使用不用的颜色显示格子周围地雷数。
    • 格子周围地雷数为0的显示为空格’ ’
    //自动打开安全区
    void OpenMine(int row, int col)
    {
        int x = 0;
        x = col + left;
        int y = 0;
        y = row + top;
        char ch = 0;
    
        if (row < 0 || row >= MapSize.MapSize.y)
            return;
        if (col < 0 || col >= MapSize.MapSize.x)
            return;
    
        if (minefield[row][col].statu == VISIBLE_SAFE)
            return;
    
        if (minefield[row][col].hasMines == 0)
            ch = ' ';
        else if (minefield[row][col].hasMines > 0)
            ch = minefield[row][col].hasMines + '0';
        else
            return;
    
        if (minefield[row][col].hasMines >= 0)
        {
            MoveCursorTo(x, y);
            int n = minefield[row][col].hasMines;
            switch (n)
            {
            case 1:
                setColor(0x9); //淡蓝色
                break;
            case 2:
                setColor(0x2); //绿色
                break;
            case 3:
                setColor(0xC); //淡红色
                break;
            case 4:
                setColor(0x1); //蓝色
                break;
            case 5:
                setColor(0x4); //红色
                break;
            case 6:
                setColor(0xD); //淡紫色
                break;
            case 7:
                setColor(0x5); //紫色
                break;
            case 8:
                setColor(0x6); //黄色
                break;
            }
            putchar(ch);
            setColor(DEFCOLOR);
            minefield[row][col].ch = ch;
            minefield[row][col].statu = VISIBLE_SAFE;
            if (ch != ' ')
                return;
        }
    
        if (row > 1)
            OpenMine(row - 1, col);
        if (col < MapSize.MapSize.x)
            OpenMine(row, col + 2);
        if (row < MapSize.MapSize.y)
            OpenMine(row + 1, col);
        if (col >= 2 && col % 2 == 0)
            OpenMine(row, col - 2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    四、游戏胜负

    1 胜负判定

    • 游戏过程中如果点开了地雷,则直接判负。
    • 比对所有未点开的地雷格子和地雷数组的坐标,如果刚好符合则判赢。
    //检查标记地雷是否都正确
    BOOL isWin()
    {
        BOOL iswin = FALSE;
        int cnt = 0;
        for (int i = 0; i < MapSize.MapSize.y; i++)
        {
            for (int j = 0; j < MapSize.MapSize.x; j++)
            {
                if (minefield[i][j].ch == MAPNORMAL)
                {
                    if (findMine4Pos(j, i) == FALSE)
                    {
                        i = MapSize.MapSize.y + 1;
                        break;
                    }
                    cnt++;
                }
            }
        }
    
        iswin = cnt == MapSize.Mine_Count;
    
        return iswin;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2 胜负界面

    • 外部函数判断胜负后传递胜负结果变量给GameOver函数。
    • 根据参数使用不同颜色显示不同提示。
    • 光标定位到地雷格子往下2行位置输出胜负信息。
    • 根据提示返回是否继续游戏
    //胜负结果变量
    enum GAME_RESULT
    {
        WIN_GAME,
        LOSS_GAME
    };
    
    //游戏结束画面
    BOOL GameOver(enum GAME_RESULT statu)
    {
        MoveCursorTo(0, bottom + 2);
        if (statu == WIN_GAME)
        {
            // system("color 2F");//改变背景为绿色
            showMine(TRUE);
            setColor(0x02);
            printf("\t    恭喜,你找出了所有的地雷!\n");
        }
        if (statu == LOSS_GAME)
        {
            // system("color 4F"); //改变背景为红色
            showMine(FALSE);
            setColor(0x04);
            printf("\t    哦吼,踩到雷了!\n");
        }
        setColor(DEFCOLOR);
    
        BOOL rst = FALSE;
        char ch;
        printf("\n\t  是否重新开始游戏(Y/N): ");
        fflush(stdin);
        do
        {
            ch = _getch();
            if (ch == 'Y' || ch == 'y' || ch == Key_ENTER)
            {
                init();
                DrawMine();
                rst = TRUE;
                break;
            }
            else if (ch == 'N' || ch == 'n' || ch == Key_ESC)
            {
                rst = FALSE;
                break;
            }
        } while (TRUE);
    
        return rst;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

      胜利界面
    胜利界面

      
      失败界面
    失败界面


    五、总结

      没有总结
      

      完整代码已上传到GitCode,需要的移步 https://gitcode.net/apull/Mines_Console
      
      

  • 相关阅读:
    【postgres】docker desktop全部署后端MVC + postgres + Adminer可视化数据库
    【Spring】Spring MVC 程序开发
    springboot连接mysql数据库,密码正确却始终报密码错误问题
    第十二章 文件的上传和下载
    热点检测/降级框架Akali的部分原理解析
    knative入门指南
    操作系统学习笔记(V):设备管理
    MongoDB增删改查操作
    C. Bargain(数学贡献法)
    ts-面向对象
  • 原文地址:https://blog.csdn.net/apull/article/details/127719987