用c程序实现扫雷小游戏

2024-02-29 21:22:06 浏览数 (1)

一、扫雷UI和玩家交互

定义class Field,用来表明扫雷的面板。以及一系列member func来操作该面板。

声明如下,具体实现见第2部分

代码语言:cpp复制
class Field
{
  public:
    Field();
    void mineTheField();  // 埋雷
    void markAdjMineCells();  // 统计每个Cells的周围的雷的个数
    void startSweep(int, int);  // 执行扫雷
    void startSweep(int, int, POSOFCELL, DIR_X, DIR_Y);  // 执行扫雷
    void drawField();  // 展示UI画面
    void checkVictoryAndFlagMines();  // 检查是否胜利并展示所有雷
    void getMove();  // 根据用户按键进行响应

  private:
    int l;  // 长度
    int b;  // 宽度
    int m;  // 雷的个数
    int x;  // 当前光标x
    int y;  // 当前光标y
    int flags;
    int hiddenCells;

    bool firstSweep;  // 首次扫雷

    std::vector<std::pair<int, int>> mines; //store the location of mines

    GRID cells;  // using GRID = std::vector<std::vector<Cell>>; 这里GRID网格存储所有单元格的二维数组
};

展示玩家游戏状态:

代码语言:cpp复制
void dispVictoryOrDefeat()  // 在屏幕打印出游戏状态
{
    writeBuf << endl;
    COLOUR col = gameState == VICTORY ? green_fg : red_fg;

    writeBuf << col << R"(  __   _____  _   _ )" << endl;
    writeBuf << col << R"(    / / _ | | | |)" << endl;
    writeBuf << col << R"(    V / (_) | |_| |)" << endl;
    writeBuf << col << R"(    |_| ___/ ___/ )" << endl;

    writeBuf << endl;

    if (gameState == VICTORY)
    {

        writeBuf << col << R"(  __      _____ _  _ _ )" << endl;
        writeBuf << col << R"(       / /_ _| | | |)" << endl;
        writeBuf << col << R"(    // / | || .` |_|)" << endl;
        writeBuf << col << R"(    _/_/ |___|_|_(_))" << endl;
    }

    else
    {
        writeBuf << col << R"(   _    ___  ___ ___ _ )" << endl;
        writeBuf << col << R"(  | |  / _ / __| __| |)" << endl;
        writeBuf << col << R"(  | |_| (_) __  _||_|)" << endl;
        writeBuf << col << R"(  |_______/|___/___(_))" << endl;
    }
}

那么,我们的主体main函数这样实现

代码语言:cpp复制

int main()
{
    system("clear"); // 清除屏幕

    Field field;  // 初始化扫雷盘

    while (true)
    {
        system("clear");  // 玩家每进行一步按键后,清屏

        field.drawField();  // 根据当前状态画UI

        if (gameState != RUNNING)
            dispVictoryOrDefeat();  // 游戏结束,显示玩家游戏结果

        writeBuf.disp();  // 打印拼命
        writeBuf.clear();  // 清空写入Buf

        if (gameState == RUNNING)
            field.getMove();  // 根据玩家按键,更新状态
        else
            break;
    }

    std::cout << endl
              << reset;

    return 0;
}

二、Filed实现

2.1 埋雷实现:

代码语言:cpp复制
void Field::mineTheField()
{
    std::random_device rd;
    std::mt19937 rng(rd());  // 伪随机产生数
    std::uniform_int_distribution<int> x_uni(0, l - 1);  // 符合自然规律的正态分布埋雷坐标
    std::uniform_int_distribution<int> y_uni(0, b - 1);
    auto m_copy = m;  // 总共要埋得雷
    while (m_copy)  // 开始埋雷
    {
        auto i = x_uni(rng);  // 
        auto j = y_uni(rng);
        if ((i >= x - 1 && i <= x   1) &&  // x和y分别是长度和宽度的中位数。
            (j >= y - 1 && j <= y   1))
            continue;  // 如果随机到坐标是盘的中心点,那么跳过
        if (cells[i][j].state != MINE)  // 当前区域没有埋过雷
        {
            cells[i][j].setMine();  // 以(x,y)坐标的网格开始埋雷
            mines.push_back(std::make_pair(i, j));  // 储存埋雷位置
            --m_copy;  // 成功埋雷,剩余埋雷数-1
        }
    }
}

2.2 统计周边雷的个数

代码语言:cpp复制
void Field::markAdjMineCells()
{
    for (auto mine : mines)  // 遍历所有雷
    {
        int x_pos = mine.first, y_pos = mine.second;  // 雷的坐标

        for (int i = x_pos - 1; i < x_pos   2;   i)  // 雷的x坐标范围[x_pos-1, x_pos 1]
        {
            if (i < 0 || i > l - 1)  // 无效区域,不统计超出扫雷盘x坐标的无效区间
                continue;

            for (int j = y_pos - 1; j < y_pos   2;   j)  // 雷的y坐标范围[y_pos-1, y_pos 1]
            {
                if (j < 0 || j > b - 1) // 无效区域,不统计超出扫雷盘y坐标的无效区间
                    continue;
                if (cells[i][j].state == MINE)  // 如果当前位置就是雷,那么不记录周边雷的个数
                    continue;
                int mineCount = 0;  // 正式开始统计

                for (int c = i - 1; c < i   2;   c)  // 以当前位置(i,j)为中心,检查周围的8个点的区域
                {
                    if (c < 0 || c > l - 1)  // 无效区域
                        continue;
                    for (int d = j - 1; d < j   2;   d)
                    {
                        if (d < 0 || d > b - 1)  // 无效区域
                            continue;
                        if (cells[c][d].state == MINE)  // 周围存在1个雷,进行加1操作
                              mineCount;
                    }
                }
                if (mineCount)  // 如果有雷,进行标注
                    cells[i][j].markAdjMine(mineCount);
            }
        }
    }
}

2.3 开始扫雷

代码语言:cpp复制
void Field::startSweep(int x, int y)
{
    if (gameState != RUNNING)
        return;
    if (cells[x][y].flagged)  // 玩家标记的雷区
        return;
    if (!cells[x][y].hidden && !QUICKCLEAR)  // 当前区域已经扫过
        return;

    switch (cells[x][y].state)
    {
    case EMPTY:  // 没有扫过雷区
        --hiddenCells;
        cells[x][y].reveal();  // 展示当前网格状态
        checkVictoryAndFlagMines();  // 检查是否踩到雷
        break;

    case ADJ_TO_MINE:  // 是个周边雷的数字
        if (cells[x][y].hidden)  //  展示
        {
            --hiddenCells;
            cells[x][y].reveal();
            checkVictoryAndFlagMines();
            return;
        }

        else if (QUICKCLEAR)
        {
            bool isValid = checkValidityOfQuickClear();
            checkVictoryAndFlagMines();
            if(isValid)
                break;
            else
                return;
        }

    case MINE:  // 踩到雷
        gameState = DEFEAT;  // 游戏失败
        for (auto mine : mines)  //所有雷展示出来
            cells[mine.first][mine.second].reveal();
        return;
    }

    // 四周8个位置扫雷
    startSweep(x - 1, y - 1, CORNER, LEFT      , UP        );
    startSweep(x    , y - 1, EDGE  , NULL_DIR_X, UP        );
    startSweep(x   1, y - 1, CORNER, RIGHT     , UP        );
    startSweep(x - 1, y    , EDGE  , LEFT      , NULL_DIR_Y);
    startSweep(x   1, y    , EDGE  , RIGHT     , NULL_DIR_Y);
    startSweep(x - 1, y   1, CORNER, LEFT      , DOWN      );
    startSweep(x    , y   1, EDGE  , NULL_DIR_X, DOWN      );
    startSweep(x   1, y   1, CORNER, RIGHT     , DOWN      );
    checkVictoryAndFlagMines();
}

开始扫雷

代码语言:cpp复制
void Field::startSweep(int x, int y, POSOFCELL pos, DIR_X x_dir, DIR_Y y_dir)
{
    checkVictoryAndFlagMines();
    if (x < 0 || x > l - 1 || y < 0 || y > b - 1)  // 超出扫雷面板
        return;
    if (gameState != RUNNING)
        return;
    if (cells[x][y].flagged)  // 当前位置已经被玩家标注
        return;
    if (!cells[x][y].hidden)  // 当前位置已经扫过
        return;

    switch (cells[x][y].state)
    {
    case EMPTY:  // 空白区域
        cells[x][y].reveal();  // 显示空白
        --hiddenCells;
        if (pos == CORNER)  // 如果该网格位置是角落,那么需要下一次递归是2个边和3个角
        {
            startSweep(x   x_dir, y   y_dir, CORNER, x_dir          , y_dir          );
            startSweep(x   x_dir, y        , EDGE  , x_dir          , NULL_DIR_Y     );
            startSweep(x        , y   y_dir, EDGE  , NULL_DIR_X     , y_dir          );
            startSweep(x   x_dir, y - y_dir, CORNER, x_dir          , (DIR_Y)(-y_dir));
            startSweep(x - x_dir, y   y_dir, CORNER, (DIR_X)(-x_dir), y_dir          );
        }

        else  // 如果该网格位置是边缘
        {
            startSweep(x   x_dir, y   y_dir, EDGE , x_dir, y_dir);  // 扫雷当前位置

            if (y_dir == NULL_DIR_Y)  // 
            {
                startSweep(x   x_dir, y - 1, CORNER, x_dir     , UP  );
                startSweep(x        , y - 1, EDGE  , NULL_DIR_X, UP  );
                startSweep(x        , y   1, EDGE  , NULL_DIR_X, DOWN);
                startSweep(x   x_dir, y   1, CORNER, x_dir     , DOWN);
            }
            else
            {
                startSweep(x - 1, y   y_dir, CORNER, LEFT , y_dir     );
                startSweep(x - 1, y        , EDGE  , LEFT , NULL_DIR_Y);
                startSweep(x   1, y        , EDGE  , RIGHT, NULL_DIR_Y);
                startSweep(x   1, y   y_dir, CORNER, RIGHT, y_dir     );
            }
        }

        break;

    case ADJ_TO_MINE:  // 位置是周边雷的数字,展示出来
        cells[x][y].reveal();
        --hiddenCells;
        break;
    }
}

2.5 画UI

代码语言:cpp复制
void Field::drawField()
{
    writeBuf << reset;
    for (int s = 0; s <= l * 4;   s)  // 用5个位置画一个格子Cell
        writeBuf << " ";  // 先画第1行空白行
    writeBuf << endl;

    writeBuf << reset;
    writeBuf << "    ";  // 第一列1的左边空白区域
    if (cells[0][0].hidden)   // 如果当前区域未扫过,用比较粗的线  
        writeBuf << "┏";
    else
        writeBuf << reset << "┌";

    for (int i = 0; i < l - 1;   i)  // 先画第1行轮廓
    {
        if (cells[i][0].hidden)
        {
            writeBuf << "━━━";
            if (cells[i   1][0].hidden)
                writeBuf << "┳";
            else
                writeBuf << "┱";
        }
        else
        {
            writeBuf << "───";
            if (cells[i   1][0].hidden)
                writeBuf << "┲";
            else
                writeBuf << "┬";
        }
    }

    if (cells[l - 1][0].hidden)
        writeBuf << "━━━┓";
    else
        writeBuf << "───┐";
    writeBuf << endl;

    writeBuf << reset;
    for (int j = 0; j < b;   j)
    {
        if (cells[0][j].hidden)
            writeBuf << "    ┃";
        else
            writeBuf << reset << "    │";

        for (int i = 0; i < l;   i)
        {
            if (cells[i][j].state != MINE || cells[i][j].hidden)
                writeBuf << " ";
            else
                writeBuf << " "; //implement different whitespace char here

            if (i == x && j == y)
            {
                if ((cells[i][j].hidden || cells[i][j].state == EMPTY) &&
                    (!cells[i][j].flagged))
                    writeBuf << blue_bg << " ";

                else
                    writeBuf << blue_bg << cells[i][j].sym;
            }
            else
                writeBuf << cells[i][j].sym;

            if (cells[i][j].hidden || (i != l - 1 && cells[i   1][j].hidden))
                writeBuf << reset << " ┃";
            else
                writeBuf << reset << " │";
        }

        if (j != b - 1)
        {
            writeBuf << endl;
            writeBuf << reset;
            writeBuf << "    ";

            if (cells[0][j].hidden)
            {
                if (cells[0][j   1].hidden)
                    writeBuf << "┣";
                else
                    writeBuf << "┡";
            }

            else
            {
                if (cells[0][j   1].hidden)
                    writeBuf << "┢";
                else
                    writeBuf << "├";
            }

            for (int k = 0; k < l - 1;   k)
            {
                if (cells[k][j].hidden)
                {
                    writeBuf << "━━━";

                    if (cells[k   1][j].hidden && !cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╇";
                    }

                    else if (!cells[k   1][j].hidden && cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╉";
                    }

                    else if (!cells[k   1][j].hidden && !cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╃";
                    }

                    else
                    {
                        writeBuf << "╋";
                    }
                }
                else
                {
                    if (cells[k][j   1].hidden)
                        writeBuf << "━━━";
                    else
                        writeBuf << "───";

                    if (cells[k   1][j].hidden && !cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╄";
                    }

                    else if (!cells[k   1][j].hidden && cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╅";
                    }

                    else if (!cells[k   1][j].hidden && !cells[k][j   1].hidden && cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╆";
                    }

                    else if (cells[k   1][j].hidden && !cells[k][j   1].hidden && cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╊";
                    }

                    else if (!cells[k   1][j].hidden && cells[k][j   1].hidden && cells[k   1][j   1].hidden)
                    {
                        writeBuf << "╈";
                    }

                    else if (!cells[k   1][j].hidden && !cells[k][j   1].hidden && !cells[k   1][j   1].hidden)
                    {
                        writeBuf << "┼";
                    }

                    else
                    {
                        writeBuf << "╋";
                    }
                }
            }

            if (cells[l - 1][j].hidden)
            {
                writeBuf << "━━━";
                if (cells[l - 1][j   1].hidden)
                    writeBuf << "┫";
                else
                    writeBuf << "┩";
            }

            else
            {
                if (cells[l - 1][j   1].hidden)
                    writeBuf << "━━━┪";
                else
                    writeBuf << "───┤";
            }
        }

        writeBuf << endl;
        writeBuf << reset;
    }

    writeBuf << "    ";
    if (cells[0][b - 1].hidden)
        writeBuf << "┗";
    else
        writeBuf << "└";

    for (int i = 0; i < l - 1;   i)
    {
        if (cells[i][b - 1].hidden)
        {
            writeBuf << "━━━";
            if (cells[i   1][b - 1].hidden)
                writeBuf << "┻";
            else
                writeBuf << "┹";
        }
        else
        {
            writeBuf << "───";
            if (cells[i   1][b - 1].hidden)
                writeBuf << "┺";
            else
                writeBuf << "┴";
        }
    }

    if (cells[l - 1][b - 1].hidden)
        writeBuf << "━━━┛";
    else
        writeBuf << "───┘";

    writeBuf << endl;

    writeBuf.goToLine(0);  // 开始绘画
}

2.6 检查是否扫完雷区

代码语言:cpp复制

void Field::checkVictoryAndFlagMines()
{
    if (hiddenCells == m && gameState != DEFEAT)
    {
        gameState = VICTORY;
        //flag all mines that weren't flagged
        for (auto mine : mines)  // 把所有雷的都展示出来
        {
            if (!cells[mine.first][mine.second].flagged)
            {
                cells[mine.first][mine.second].toggleflag();
            }
        }
    }
}

2.7 根据玩家按键进行下一步动作

玩家有效的操作按键包括:

  • ↑, ←, ↓, → 或者H, J, K, L
  • 扫雷 字母S或者回车键
  • 标记雷区 字母F或者空格键
代码语言:cpp复制
void Field::getMove()
{
    KEY k = getKey();
    switch (k)
    {
    case K_K:
    case K_k:
    case K_UP:  // 玩家按键是向上键
        if (y)
            --y;
        return;

    case K_J:
    case K_j:
    case K_DOWN:  // 玩家按键是向下键
        if (y != b - 1)
              y;
        return;

    case K_H:
    case K_h:
    case K_LEFT:  // 玩家按键是向左键
        if (x)
            --x;
        return;

    case K_L:
    case K_l:
    case K_RIGHT:  // 玩家按键是向右键
        if (x != l - 1)
              x;
        return;

    case K_SPACE:  // 玩家按键是空格,标记雷区
    case K_F:
        if (cells[x][y].flagged)
        {
              flags;
              flagDisp;
            cells[x][y].toggleflag();
        }
        else if (flags && cells[x][y].hidden)
        {
            --flags;
            --flagDisp;
            cells[x][y].toggleflag();
        }
        return;

    case K_ENTER:  // 玩家按键是Enter键,执行扫雷
    case K_S:
        if (firstSweep)
        {
            mineTheField();
            markAdjMineCells();
            firstSweep = false;
        }
        startSweep(x, y);
        return;
    }
}

0 人点赞