数据结构课设C++_迷宫游戏

前言

该课设内容于初学C++时所做,代码指令仍有很大提升余地!

本内容包含:

4大建迷宫算法DFS、(Random)Prim、十字分割法、(Random)Kruskal

1种寻路算法A*寻路

easyx画迷宫(作者现学现做的,如有不足还多包含)

结果展示

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

实现内容概括

  1. 可以在设置页面调整迷宫的大小,建立迷宫的算法
  2. 能够粗略显式建立迷宫过程
  3. 可以在迷宫页面控制节点走迷宫,并设有出路
  4. 可以在迷宫页面实时寻路,给与提示

代码几大部分

  1. 迷宫类
  2. 迷宫节点类
  3. easyx画迷宫
  4. 4大建迷宫算法
  5. A*寻路算法

迷宫类

class maze {
    public:
    //...
    //...
    //...
    maze(short cols, short rows, int mk)
    {
        setlocale(LC_ALL, "");
        _rows = rows + 1;
        _cols = cols * 2+2;
        _mk = (enum mazeKind)mk;
        _capacity = rows * cols;
        _vv.resize(_rows);//初始化二维数组(迷宫)
        for (size_t i = 0; i < _rows; i++)
        {
            _vv[i].resize(_cols / 2, 0x11111);
        }
        _vv[1][1] = 0x11010;
    }
    private:
    enum mazeKind {
        NORMAL = 1,
        EVIL,
        CARTON
    };

    private://成员变量
    short _cols = 0;//迷宫横宽
    short _rows = 0;//迷宫竖长
    enum mazeKind _mk = NORMAL;//迷宫风格
    int _capacity = 0;//迷宫大小
    int _occupation = 0;//计算建立迷宫时已用的空间
    public:vector<vector<int>> _vv;//用二维数组保存迷宫,这里为了方便,设为了公有成员
};

思路:

  1. 对于迷宫的建立,要选择一种存储方式来保存迷宫节点
  2. 要如何表示迷宫的墙和路径

方法:

  1. 用二维数组vecotr<vector>来保存迷宫
  2. 该二维数组的每个节点都存储多个比特位的值,而迷宫的墙就用这几个比特位来标志迷宫某点的上下左右是否是通路(0表示不通,1表示相通)

迷宫类节点

这一部分主要用于DFS算法以及在迷宫中控制节点移动

class mazeNode {
    public:
    //方向,用来表明下一步走向
    enum DIRECTION {
        UP = 1,
        DOWN,
        LEFT,
        RIGHT
    };
    //...
    //...
    //...
    public://成员变量
    enum DIRECTION _dir;
    int _x=1;//x坐标(_vv的第二个[])
    int _y=1;//y坐标(_vv的第一个[])
    int _prority = 0;
};

easyx画迷宫

//显示创建过程的画线函数
void drawSingleline(int cols, int rows, vector<vector<int>>& vv, int dir);
//直接绘制迷宫图
int drawMazeline(int cols, int rows, vector<vector<int>>& vv);
//游戏制作者
void producer();
//游戏选项
void option(short& cols, short& rows, int& alg, int& kind, bool& visible);
//游戏菜单
void helpInfo(short& cols, short& rows, int& alg, int& kind, bool& visible);
//EasyX用来判断鼠标区域
bool inArea(int mx, int my, int x, int y, int w, int h);
//EasyX用来快速建立文本框
bool button(int x, int y, int w, int h, const char* text, int status);
//命令行定位光标
void setpos(short x, short y)
{
    COORD pos = { x,y };
    HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(houtput, pos);
}
//用图形化界面启动迷宫
void GameStart_EasyX();
//用图形化界面运行迷宫
void RunGame_Easyx(short& cols, short& rows, int& alg, int& kind, bool& visible);

这一部分用于将迷宫类的信息画在图形化界面上,以及控制其交互

4大建迷宫算法

DFS算法建迷宫

原理:

通过不断的入栈出栈方式,将所有的节点遍历完,就建成了迷宫。

从任意一点出发,随机往一个没走过的方向走,然后将这两条路之间的墙打通,并且入栈。一直循环到遍历完所有结点。如果其中发现无路可走,则退栈,退到有路可走为止。遍历完所有节点后,就能保证任意两点能够互通

缺陷:

主路过于明显,且岔路少,基本上一条路就能走到头

具体示意:

在这里插入图片描述

核心代码:

void maze::creatmaze_DFS()
{
    mazeNode mzd;
    stack<mazeNode> st;
    bool stispop = false;

    while (!isFull())
    {
        int flag = 0;
        int dir = rand() % 4 + 1;
        switch (dir) 
        {
            case mazeNode::UP:
                if (mzd.up(*this))
                {
                    flag = 1;
                    st.push(mzd);
                }
                break;
            case mazeNode::DOWN:
                if (mzd.down(*this))
                {
                    flag = 1;
                    st.push(mzd);
                }
                break;
            case mazeNode::LEFT:
                if (mzd.left(*this))
                {
                    flag = 1;
                    st.push(mzd);
                }
                break;
            case mazeNode::RIGHT:
                if (mzd.right(*this))
                {
                    flag = 1;
                    st.push(mzd);
                }
                break;
        }
        if (flag == 0)
        {
            if (mzd.left(*this))
            {
                flag = 1;
                st.push(mzd);
            }
            else if (mzd.up(*this))
            {
                flag = 1;
                st.push(mzd);
            }
            else if (mzd.down(*this))
            {
                flag = 1;
                st.push(mzd);
            }
            else if (mzd.right(*this))
            {	
                flag = 1;
                st.push(mzd);
            }
            else if (flag==0&& !isFull()&&!st.empty())
            {
                st.pop();
                mzd = st.top();
            }
        }
    }
    _vv[_rows - 1][_cols / 2 - 1] &= 0x11101;
}	

(Random)Prim算法建迷宫

原理:

通过随机访问所有可访问的边缘节点的方式,将所有节点访问完,就建成了迷宫。

从一随机点开始,将该点周围所有没有遍历过的点放入待选列表。然后在待选列表中随机选择一点,该坐标就是动点下一步要前往的点。直到所有的点都遍历完,就完成了迷宫的建立。

待选列表:将下一步走通向的所有坐标放入该表,动点移动的时候,就在这些坐标中随机选择一个点的坐标移动

已选列表:若动点已经走过某个坐标,则将该坐标放入已选列表中(进入已选列表中的值不能再次被选择,这个表只是个虚拟概念,可以不创建)

迷宫特点:

这类迷宫比较好看,岔路较多,主路不明显,所以常用该算法建立大型迷宫

具体示意:

在这里插入图片描述

核心代码:

void maze::creatmaze_Prim()
{
    vector<pair<int, int> > v_wait;
    mazeNode mzd;
    mzd._x = 1;
    mzd._y = 1;
    v_wait.push_back(make_pair(1, 1));
    int select = 0;
    int dir = 1;
    while (!v_wait.empty())
    {
        int flag = 0;
        select = rand() % v_wait.size();
        dir = rand() % 4 + 1;
        mzd._x = v_wait[select].first;
        mzd._y = v_wait[select].second;

        if (mzd.inList(*this, mzd._x, mzd._y - 1))//(_vv[mzd._y][mzd._x] & 0x100000)等于0时,说明没有进过列表
        {
            v_wait.push_back(make_pair(mzd._x, mzd._y - 1));
        }
        if (mzd.inList(*this, mzd._x, mzd._y + 1))
        {
            v_wait.push_back(make_pair(mzd._x, mzd._y + 1));
        }
        if (mzd.inList(*this, mzd._x - 1, mzd._y))
        {
            v_wait.push_back(make_pair(mzd._x - 1, mzd._y));
        }
        if (mzd.inList(*this, mzd._x + 1, mzd._y))
        {
            v_wait.push_back(make_pair(mzd._x + 1, mzd._y));
        }

        switch (dir)
        {
            case mazeNode::UP:
                if (mzd._y - 1 > 0 && (_vv[mzd._y - 1][mzd._x] & 0x100000) && !(_vv[mzd._y - 1][mzd._x] & 0000001))
                {
                    flag = 1;
                    _vv[mzd._y][mzd._x] &= 0x101110;
                    _vv[mzd._y - 1][mzd._x] &= 0x110111;
                }
                break;
            case mazeNode::DOWN:
                if (mzd._y + 1 < getheight() && (_vv[mzd._y + 1][mzd._x] & 0x100000) && !(_vv[mzd._y + 1][mzd._x] & 0x000001))
                {
                    flag = 1;
                    _vv[mzd._y][mzd._x] &= 0x110110;
                    _vv[mzd._y + 1][mzd._x] &= 0x101111;
                }
                break;
            case mazeNode::LEFT:
                if (mzd._x - 1 > 0 && (_vv[mzd._y][mzd._x - 1] & 0x100000) && !(_vv[mzd._y][mzd._x - 1] & 0x000001))
                {
                    flag = 1;
                    _vv[mzd._y][mzd._x] &= 0x111010;
                    _vv[mzd._y][mzd._x - 1] &= 0x111101;
                }
                break;
            case mazeNode::RIGHT:
                if (mzd._x + 1 < getwidth() && (_vv[mzd._y][mzd._x + 1] & 0x100000) && !(_vv[mzd._y][mzd._x + 1] & 0x000001))
                {
                    flag = 1;
                    _vv[mzd._y][mzd._x] &= 0x111100;
                    _vv[mzd._y][mzd._x + 1] &= 0x111011;
                }
                break;
        }
        if (flag == 0)
        {
            if (mzd._x - 1 > 0 && (_vv[mzd._y][mzd._x - 1] & 0x100000) && !(_vv[mzd._y][mzd._x - 1] & 0x000001))
            {
                _vv[mzd._y][mzd._x] &= 0x111010;
                _vv[mzd._y][mzd._x - 1] &= 0x111101;
            }
            else if (mzd._y - 1 > 0 && (_vv[mzd._y - 1][mzd._x] & 0x100000) && !(_vv[mzd._y - 1][mzd._x] & 0x000001))
            {
                flag = 1;
                _vv[mzd._y][mzd._x] &= 0x101110;
                _vv[mzd._y - 1][mzd._x] &= 0x110111;
            }
            else if (mzd._y + 1 < getheight() && (_vv[mzd._y + 1][mzd._x] & 0x100000) && !(_vv[mzd._y + 1][mzd._x] & 0x000001))
            {
                flag = 1;
                _vv[mzd._y][mzd._x] &= 0x110110;
                _vv[mzd._y + 1][mzd._x] &= 0x101111;
            }
            else if (mzd._x + 1 < getwidth() && (_vv[mzd._y][mzd._x + 1] & 0x100000) && !(_vv[mzd._y][mzd._x + 1] & 0x000001))
            {
                flag = 1;
                _vv[mzd._y][mzd._x] &= 0x111100;
                _vv[mzd._y][mzd._x + 1] &= 0x111011;
            }
        }
        v_wait.erase(v_wait.begin() + select);
    }
    _vv[_rows - 1][_cols / 2 - 1] &= 0x11101;
} 

Recursion(十字分割、递归分割)算法建迷宫

原理:

通过大事化小,小事化了的方式,将建立迷宫的算法简化成递归打墙的方式(寻找两个相邻节点,打掉之间的墙)。递归到底,就建成了迷宫。

将大迷宫每次用2条线从中间分成4份,然后这2条线被分成了4条边,随机选择3条边,在这3条边上分别任意选择一个墙面打通,使墙面的两端相连(这样就能保证整个迷宫的连通性),然后对其分割的4部分进行递归,即可完成迷宫创建。

缺陷:

该算法建立的迷宫大多路径很直,视觉上看起来不够复杂

具体示意:

在这里插入图片描述

核心代码:

void maze::recursion(int col_start,int col_end, int row_start,int row_end)
{
    //drawEmptymap();//画空白迷宫
    //setpos(0, 15); 
    int rodecol = 0;
    int roderow = 0;
    int tmp = 0;//用来记录两次的随即值是否相同,相同的话就再次随机
    int chose = rand() % 2;//用来选择那条线(col/row)会有两个通路

    if (col_start>=col_end && row_start>=row_end)//只有一格时要返回
    {
        return;
    }
    else if (row_start == row_end)//对于“长方形”(行/列相等,另外一个只有一格宽度)划分区域来说,要单独处理
    {
        int cur = 0;
        while(col_start + cur < col_end)
        {
            _vv[row_start][col_start + cur] &= 0x11101;
            _vv[row_start][col_start + cur + 1] &= 0x11011;
            cur++;
        }
        return;
    }
    else if(col_start == col_end)
    {
        int cur = 0;
        while (row_start + cur < row_end)
        {
            _vv[row_start + cur][col_start] &= 0x10111;
            _vv[row_start + cur + 1][col_start] &= 0x01111;
            cur++;
        }
        return;
    }

    if (chose == 0)
    {
        roderow = rand() % ((row_start + row_end) / 2 - row_start + 1) + row_start;//选取row分割线以上的随机一段
        _vv[roderow][(col_start + col_end) / 2] &= 0x11101;
        _vv[roderow][(col_start + col_end) / 2 + 1] &= 0x11011;
        tmp = rand() % ((row_start + row_end) / 2 - row_start + 1) + 1;
        roderow = (row_end + row_start) / 2 + tmp;//选取row分割线以下的随机一段
        while (roderow > row_end)//防止访问越界,对于不对称的分割线来说,可能会越界
        {
            roderow--;
        }
        _vv[roderow][(col_start + col_end) / 2] &= 0x11101;
        _vv[roderow][(col_start + col_end) / 2 + 1] &= 0x11011;

        rodecol = rand() % (col_end - col_start + 1) + col_start;//给col分割一下,保证每次都有三个被分割
        _vv[(row_end + row_start) / 2][rodecol] &= 0x10111;
        _vv[(row_end + row_start) / 2 + 1][rodecol] &= 0x01111;
    }
    else
    {
        rodecol = rand() % ((col_end + col_start) / 2 - col_start + 1) + col_start;//选取col分割线以左的随机一段
        _vv[(row_end + row_start) / 2][rodecol] &= 0x10111;
        _vv[(row_end + row_start) / 2 + 1][rodecol] &= 0x01111;
        tmp = rand() % ((col_end + col_start) / 2 - col_start + 1) + 1;
        rodecol = (col_end + col_start) / 2 + tmp;//选取col分割线以右的随机一段
        while (rodecol > col_end)
        {
            rodecol--;
        }
        _vv[(row_end + row_start) / 2][rodecol] &= 0x10111;
        _vv[(row_end + row_start) / 2 + 1][rodecol] &= 0x01111;

        roderow = rand() % (row_end - row_start + 1) + row_start;//给row分割一下,保证每次都有三个被分割
        _vv[roderow][(col_start + col_end) / 2] &= 0x11101;
        _vv[roderow][(col_start + col_end) / 2 + 1] &= 0x11011;
    }
    recursion(col_start, (col_end + col_start) / 2, row_start, (row_end + row_start) / 2);//递归左上
    recursion((col_start + col_end) / 2 + 1, col_end, row_start, (row_end + row_start) / 2);//递归右上
    recursion(col_start, (col_end + col_start) / 2, (row_start + row_end) / 2 + 1, row_end);//递归左下
    recursion((col_start + col_end) / 2 + 1, col_end, (row_start + row_end) / 2 + 1, row_end);//递归右下
}

(Random)Kruskal算法建迷宫

原理:

前面3个算法本质都是通过遍历节点来建立迷宫,而该算法则是通过遍历墙来建立迷宫。该算法是难度比较高的算法,因为需要一个并查集来记录节点的集合关系。

将迷宫中的每个网格都看作是一个集合,开始的时候集合的父结点是自己,然后随机选择一面墙,判断该墙两侧是否属于同一个集合,如果不属于同一个集合,就把他们之间的墙打通,然后把他们放入同一个集合。如果属于同一个集合,那么就什么也不做。当所有墙都遍历到后,迷宫建立完成。

迷宫特点:

这类迷宫与Prim类似,比较好看,岔路较多,主路不明显,所以也常用该算法建立大型迷宫

具体示意:

在这里插入图片描述

每遍历到一面墙,就会合并墙两侧的节点,当所有的节点都合并成了一个集合后,迷宫就建好了

核心代码:

void maze::creatmaze_Kruskal()
{
    /*初始化存放墙的数组*/
    vector<wall> v_wall;
    for (int j = 1; j <= getheight() - 1; j++)
    {
        for (int i = 1; i < getwidth() - 1; i++)
        {
            v_wall.push_back(wall(i, j, i + 1, j));
        }
    }
    for (int i = 1; i <= getwidth() - 1; i++)
    {
        for (int j = 1; j < getheight() - 1; j++) 
        {
            v_wall.push_back(wall(i, j, i, j + 1));
        }
    }

    //用来加快找墙速度.通过一个数组来存放对应的w_wall的每个墙的下标,这样在后续就可以加快找到剩下还没遍历到的墙速度
    vector<int> ptmp;
    for (int i = 0; i < v_wall.size(); i++)
    {
        ptmp.push_back(i);
    }

    /*初始化实现并查集的数组, 每一对都对应自己的坐标, 自己是自己的父集*/
    /*当改变所属集合时,只需要把自己pair的数据修改为对应父集的坐标就行*/
    vector<vector<pair<int, int>>> v_set;
    v_set.resize(getheight());
    for (size_t i = 0; i < _rows; i++)
    {
        v_set[i].resize(_cols / 2);
    }
    for (int i = 1; i < v_set.size(); i++)
    {
        for (int j = 1; j < v_set[i].size(); j++)
        {
            v_set[i][j].first = i;
            v_set[i][j].second = j;
        }
    }

    size_t wallNumber = v_wall.size();//用来记录墙的数量
    int chosenWall = 0;//用来表明遍历了多少墙
    int select = 0;//用来选择选的墙的两侧是 上下 还是 左右 关系

    //用作后面的并查集合并集合
    int first = 0;
    int second = 0;
    int x1 = 0;
    int x2 = 0;

    //当墙遍历完时,就结束,迷宫建好
    while (chosenWall != wallNumber)
    {
        //找到还未被遍历的墙
        int number = 0;
        number = rand() % ptmp.size();
        select = ptmp[number];
        //判断所选的墙是 上下 还是 左右 关系
        if (v_wall[select].block1().first == v_wall[select].block2().first)
        {
            /*判断所选墙的两侧是否属于同一个集合*/
            /*如果属于同一个集合,就不打通;不属于,就打通墙壁*/
            if (!(v_set[v_wall[select].block1().second][v_wall[select].block1().first].first == 
                  v_set[v_wall[select].block2().second][v_wall[select].block2().first].first &&
                  v_set[v_wall[select].block1().second][v_wall[select].block1().first].second ==
                  v_set[v_wall[select].block2().second][v_wall[select].block2().first].second))
            {
                _vv[v_wall[select].block1().second][v_wall[select].block1().first] &= 0x10111;
                _vv[v_wall[select].block2().second][v_wall[select].block2().first] &= 0x01111;
                //保存值,防止随着值的改变,指向的坐标也变了
                x1 = v_set[v_wall[select].block2().second][v_wall[select].block2().first].first;
                x2 = v_set[v_wall[select].block2().second][v_wall[select].block2().first].second;
                //将这个集合的父集变为一个子集,再使他指向他的新父集
                v_set[x1][x2].first = v_set[v_wall[select].block1().second][v_wall[select].block1().first].first;
                v_set[x1][x2].second = v_set[v_wall[select].block1().second][v_wall[select].block1().first].second;
                //将这个集合指向他的新父集
                v_set[v_wall[select].block2().second][v_wall[select].block2().first].first = v_set[v_wall[select].block1().second][v_wall[select].block1().first].first;
                v_set[v_wall[select].block2().second][v_wall[select].block2().first].second = v_set[v_wall[select].block1().second][v_wall[select].block1().first].second;
            }
            v_wall[select].occupated();
            ptmp.erase(ptmp.begin() + number);
            chosenWall++;
        }
        else
        {
            if (!(v_set[v_wall[select].block1().second][v_wall[select].block1().first].first ==
                  v_set[v_wall[select].block2().second][v_wall[select].block2().first].first &&
                  v_set[v_wall[select].block1().second][v_wall[select].block1().first].second ==
                  v_set[v_wall[select].block2().second][v_wall[select].block2().first].second))
            {
                _vv[v_wall[select].block1().second][v_wall[select].block1().first] &= 0x11101;
                _vv[v_wall[select].block2().second][v_wall[select].block2().first] &= 0x11011;

                x1 = v_set[v_wall[select].block2().second][v_wall[select].block2().first].first;
                x2 = v_set[v_wall[select].block2().second][v_wall[select].block2().first].second;

                v_set[x1][x2].first = v_set[v_wall[select].block1().second][v_wall[select].block1().first].first;
                v_set[x1][x2].second = v_set[v_wall[select].block1().second][v_wall[select].block1().first].second;

                v_set[v_wall[select].block2().second][v_wall[select].block2().first].first = v_set[v_wall[select].block1().second][v_wall[select].block1().first].first;
                v_set[v_wall[select].block2().second][v_wall[select].block2().first].second = v_set[v_wall[select].block1().second][v_wall[select].block1().first].second;
            }
            v_wall[select].occupated();
            ptmp.erase(ptmp.begin() + number);
            chosenWall++;
        }
        /*更改所有集合的关系*/
        /*此次上面的循环没有任何父集变为别的父集的子集,那么下面循环就相当于遍历一遍*/
        /*如果有父集变为子集,那么下面循环就会更改原来这个父集的所有子集,使他们指向新的父集*/
        for (int i = 1; i < v_set.size(); i++)
        {
            for (int j = 1; j < v_set[i].size(); j++)
            {
                first = v_set[v_set[i][j].first][v_set[i][j].second].first;
                second= v_set[v_set[i][j].first][v_set[i][j].second].second;
                v_set[i][j].first = first;
                v_set[i][j].second = second;
            }
        }
    }
    _vv[_rows - 1][_cols / 2 - 1] &= 0x11101;
} 

A*寻路算法

原理:

A*寻路算法是在BFS的基础上,增加了通过判断各点的代价选择较短路径实现的。这个代价的计算,是通过曼哈顿距离进行计算的。

计算当前点的四周的代价,选取代价最小(或之一)的点,移动至该点(保证中间没有墙隔开),并且将该点坐标入栈,若该点为死路,就退栈,退到非死路点为止。然后继续重复操作,直到到达终点。

曼哈段距离:

在这里插入图片描述

曼哈顿距离:两个点在标准坐标系上的绝对轴距总和

所以上图的曼哈顿距离=4

而这个4就是从起点到终点的代价

具体实现:

在这里插入图片描述

核心代码:

void maze::FindPath(int x, int y,int piexl, stack<pair<int, int>>& st)
{
    vector<vector<int>> vv;
    vv.resize(_rows);
    for (size_t i = 0; i < _rows; i++)
    {
        //0表示没有遍历过,1表示遍历过,2表示退栈过
        vv[i].resize(_cols / 2, 0);
    }

    //stack<pair<int,int>> st;
    mazeNode mzd(x,y);
    st.push(make_pair(mzd._y, mzd._x));
    vv[mzd._y][mzd._x] = 1;

    pair<int, int> p;

    while (mzd._x != _cols / 2 - 1 || mzd._y != _rows - 1)
    {
        p=mzd.getPrority(*this,vv);
        if (p.first == -1)
        {
            vv[mzd._y][mzd._x] = 2;
            st.pop();
        }
        else 
        {
            st.push(p);
        }
        mzd._x = st.top().second;
        mzd._y = st.top().first;
    }
}

代码自取

如果对您有所帮助,请点赞支持一下博主!!
gitee仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值