Flutter-随机迷宫生成和解迷宫小游戏

此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏。本人是应届毕业生,希望能与大家一起讨论和学习~

注:由于这是本人第一次写博客,难免排版或用词上有所欠缺,请大家多多包涵。
注:如需转载文章,请注明出处,谢谢。

一、项目介绍:

1.概述
项目名:方块迷宫
作者:沫小亮。
编程框架与语言:Flutter&Dart
开发环境:Android Studio 3.6.2
学习参考:慕课网-看得见的算法
项目完整源码地址:(待更新)
游戏截图:
在这里插入图片描述在这里插入图片描述

2.迷宫生成原理
1.采用图的遍历进行迷宫生成,其本质就是生成一棵树,树中每个节点只能访问一次,且每个节点之间没有环路(迷宫的正确路径只有一条)。
2.初始化:设置起点和终点位置,并给所有行坐标为奇数且列坐标为奇数的位置设置为路。其余位置设置为墙。(坐标从0…开始算)

(如下图,蓝色位置为墙,橙色位置为路,橙色线条为可能即将打通的路,此图来源于慕课网-看得见的算法)
在这里插入图片描述
3.在遍历过程中,不断遍历每个位置,同时遍历过的位置设为已访问位置,结合迷宫生成算法(见迷宫特点第6点)让相邻某个墙变成路,使之路径联通。直至所有位置都遍历完成则迷宫生成结束(每个节点只能遍历一次)。

(如下图,蓝色位置为墙,橙色位置为路,橙色线条为可能即将打通的路,此图来源于慕课网-看得见的算法)
在这里插入图片描述

3.迷宫特点(可根据需求自行扩展)
1.迷宫只有一个起点、一个终点,且起点和终点的位置固定。
2.迷宫的正确路径只有一条。
3.迷宫的正确路径是连续的。
4.迷宫地图是正方形,且方块行数和列数都为奇数。
5.迷宫中每个方块占用一个单元格。
6.迷宫生成算法:图的深度优先遍历和广度优先遍历相结合 + 随机队列(入队和出队随机在队头或队尾)+ 随机方向遍历顺序(提高迷宫的随机性)。
7.迷宫自动求解算法:图的深度优先遍历(递归方法)。

4.玩法介绍(可根据需求自行扩展)
1.游戏共设置有10个关卡,到达终点可以进入下一关,随着关卡数的增加,迷宫地图大小(方块数)增加,但限定时间也会增加。
2.点击方向键可对玩家角色的位置进行控制。
2.每个关卡都有限定时间,超过限定时间仍未到达终点则闯关失败,可从本关继续挑战。
3.每个关卡都可以使用一次提示功能,可展示2秒的正确路径,便于小白玩家入门。
4. 颜色对应:
蓝灰色方块->墙(不可经过)
蓝色方块->玩家角色(可控制移动)
白色方块->路(可经过)
深橘色->终点(通关)
橙色->正确路径(提示功能)

二、项目源码(主要部分):

pubspec.yaml //flutter配置清单

dependencies:
  flutter:
    sdk: flutter
  //toast库
  fluttertoast: ^3.1.3
  //Cupertino主题图标集
  cupertino_icons: ^0.1.2

在这里插入图片描述

  • maze_game_model.dart //迷宫游戏数据层
class MazeGameModel {
  int _rowSum; //迷宫行数
  int _columnSum; //迷宫列数
  int _startX, _startY; //迷宫入口坐标([startX,startY])
  int _endX, _endY; //迷宫出口坐标([endX,endY])
  static final int MAP_ROAD = 1; //1代表路
  static final int MAP_WALL = 0; //0代表墙
  List<List<int>> mazeMap; //迷宫地形(1代表路,0代表墙)
  List<List<bool>> visited; //是否已经访问过
  List<List<bool>> path; //是否是正确解的路径
  List<List<int>> direction = [
    [-1, 0],
    [0, 1],
    [1, 0],
    [0, -1]
  ]; //迷宫遍历的方向顺序(迷宫趋势)
  int spendStepSum = 0; //求解的总步数
  int successStepLength = 0; //正确路径长度
  int playerX, playerY; //当前玩家坐标

  MazeGameModel(int rowSum, int columnSum) {
    if (rowSum % 2 == 0 || columnSum % 2 == 0) {
      throw "model_this->迷宫行数和列数不能为偶数";
    }
    this._rowSum = rowSum;
    this._columnSum = columnSum;
    mazeMap = new List<List<int>>();
    visited = new List<List<bool>>();
    path = new List<List<bool>>();

    //初始化迷宫起点与终点坐标
    _startX = 1;
    _startY = 0;
    _endX = rowSum - 2;
    _endY = columnSum - 1;

    //初始化玩家坐标
    playerX = _startX;
    playerY = _startY;

    //初始化迷宫遍历的方向(上、左、右、下)顺序(迷宫趋势)
    //随机遍历顺序,提高迷宫生成的随机性(共12种可能性)
    for (int i = 0; i < direction.length; i++) {
      int random = Random().nextInt(direction.length);
      List<int> temp = direction[random];
      direction[random] = direction[i];
      direction[i] = temp;
    }

    //初始化迷宫地图
    for (int i = 0; i < rowSum; i++) {
      List<int> mazeMapList = new List();
      List<bool> visitedList = new List();
      List<bool> pathList = new List();

      for (int j = 0; j < columnSum; j++) {
        //行和列都为基数则设置为路,否则设置为墙
        if (i % 2 == 1 && j % 2 == 1) {
          mazeMapList.add(1); //设置为路
        } else {
          mazeMapList.add(0); //设置为墙
        }
        visitedList.add(false);
        pathList.add(false);
      }
      mazeMap.add(mazeMapList);
      visited.add(visitedList);
      path.add(pathList);
    }
    //初始化迷宫起点与终点位置
    mazeMap[_startX][_startY] = 1;
    mazeMap[_endX][_endY] = 1;
  }

  //返回迷宫行数
  int getRowSum() {
    return _rowSum;
  }

  //返回迷宫列数
  int getColumnSum() {
    return _columnSum;
  }

  //返回迷宫入口X坐标
  int getStartX() {
    return _startX;
  }

  //返回迷宫入口Y坐标
  int getStartY() {
    return _startY;
  }

  //返回迷宫出口X坐标
  int getEndX() {
    return _endX;
  }

  //返回迷宫出口Y坐标
  int getEndY() {
    return _endY;
  }

  //判断[i][j]是否在迷宫地图内
  bool isInArea(int i, int j) {
    return i >= 0 && i < _rowSum && j >= 0 && j < _columnSum;
  }
}
  • position.dart //位置类(实体类)
    注:x对应二维数组中的行下标,y对应二维数组中的列下标(往后也是)
class Position extends LinkedListEntry<Position>{
  int _x, _y;             //X对应二维数组中的行下标,y对应二维数组中的列下标
  Position _prePosition;  //存储上一个位置
  
  Position(int x, int y,  { Position prePosition = null } ) {
    this._x = x;
    this._y = y;
    this._prePosition = prePosition;
  }

  //返回X坐标()
  int getX() {
    return _x;
  }

  //返回Y坐标()
  int getY() {
    return _y;
  }

  //返回上一个位置
  Position getPrePosition() {
    return _prePosition;
  }
}
  • random_queue.dart //随机队列
    入队:头部或尾部(各50%的概率)
    出队:头部或尾部(各50%的概率)
    底层数据结构:LinkedList
class RandomQueue {
  LinkedList<Position> _queue;

  RandomQueue(){
    _queue = new LinkedList();
  }

  //往随机队列里添加一个元素
  void addRandom(Position position) {
    if (Random().nextInt(100) < 50) {
     //从头部添加
      _queue.addFirst(position);
    }
    //从尾部添加 
    else {
      _queue.add(position);
    }
  }
  
  //返回随机队列中的一个元素
  Position removeRandom() {
    if (_queue.length == 0) {
      throw "数组元素为空";
    }
    if (Random().nextInt(100) < 50) {
      //从头部移除
      Position position = _queue.first;
      _queue.remove(position);
      return position;
    } else {
      //从尾部移除
      Position position = _queue.last;
      _queue.remove(position);
      return position;
    }
  }

  //返回随机队列元素数量
  int getSize() {
    return _queue.length;
  }

  //判断随机队列是否为空
  bool isEmpty() {
    return _queue.length == 0;
  }
}
  • main.dart //迷宫游戏视图层和控制层

1. APP全局设置


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    if (Platform.isAndroid) {
      // 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。
      SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
      SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
    }
    return MaterialApp(
      title: '方块迷宫',     //应用名
      theme: ThemeData(
        primarySwatch: Colors.blue, //主题色
      ),
      debugShowCheckedModeBanner: false,  //不显示debug标志
      home: MyHomePage(),   //主页面
    );
  }
}

2.界面初始化

 class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int gameWidth, gameHeight;     //游戏地图宽度和高度
  double itemWidth, itemHeight;  //每个小方块的宽度和高度
  int level = 1;                 //当前关卡数(共10关)
  int rowSum = 15;               //游戏地图行数
  int columnSum = 15;            //游戏地图列数
  int surplusTime;               //游戏剩余时间
  bool isTip = false;            //是否使用提示功能
  Timer timer;                   //计时器
  MazeGameModel _model;          //迷宫游戏数据层

  //初始化状态
  @override
  void initState() {
    super.initState();
    _model = new MazeGameModel(rowSum, columnSum);

    //新建一个事件循环队列,确保不堵塞主线程
    new Future(() {
      //生成一个迷宫
      _doGenerator(_model.getStartX(), _model.getStartY() + 1);
    });

    //设置倒计时
    _setSurplusTime(level);
  }

3.界面整体结构

 @override
  Widget build(BuildContext context) {
    //获取手机屏幕宽度,并让屏幕高度等于屏幕宽度(确保形成正方形迷宫区域)
    //结果向下取整,避免出现实际地图宽度大于手机屏幕宽度的情况
    gameHeight = gameWidth = MediaQuery.of(context).size.width.floor();
    //每一个小方块的宽度和长度(屏幕宽度/列数)
    itemHeight = itemWidth = (gameWidth / columnSum);
    return Scaffold(
      appBar: PreferredSize(
          //设置标题栏高度
          preferredSize: Size.fromHeight(40),
          //标题栏区域
          child: _appBarWidget()),
      body: ListView(
        children: <Widget>[
          //游戏地图区域
          _gameMapWidget(),
          //游戏提示与操作栏区域
          _gameTipWidget(),
          //游戏方向控制区域
          _gameControlWidget(),
        ],
      ),
    );
  }

4.游戏地图区域

注:由于游戏提示与操作栏区域、游戏方向键控制区域不是本文章要讲的重点,故不详细介绍,有兴趣的朋友可以到完整项目源码地址中查看。

 //游戏地图区域
  Widget _gameMapWidget(){
    return Container(
        width: gameHeight.toDouble(),
        height: gameHeight.toDouble(),
        color: Colors.white,
        child: Center(
          //可堆叠布局(配合Positioned绝对布局使用)
          child: Stack(
            //按行遍历
            children: List.generate(_model.mazeMap.length, (i) {
              return Stack(
                //按列遍历
                  children: List.generate(_model.mazeMap[i].length, (j) {
                    //绝对布局
                    return Positioned(
                        //每个方块的位置
                        left: j * itemWidth.toDouble(),
                        top: i * itemHeight.toDouble(),
                        //每个方块的大小和颜色
                        child: Container(
                            width: itemWidth.toDouble(),
                            height: itemHeight.toDouble(),
                            //位于顶层的颜色应放在前面进行判断,避免被其他颜色覆盖
                            //墙->蓝灰色
                            //路->白色
                            //玩家角色->蓝色
                            //迷宫终点-> 深橘色
                            //迷宫正确路径->橙色
                            color: _model.mazeMap[i][j] == 0
                                ? Colors.blueGrey
                                : (_model.playerX == i && _model.playerY == j)
                                ? Colors.blue
                                : (_model.getEndX() == i && _model.getEndY() == j)
                                ? Colors.deepOrange
                                : _model.path[i][j] ? Colors.orange : Colors.white));
                  }));
            }),
          ),
        ));
  }

5.生成迷宫

//开始生成迷宫地图
  void _doGenerator(int x, int y) {
    RandomQueue queue = new RandomQueue();
    //设置起点
    Position start = new Position(x, y);
    //入队
    queue.addRandom(start);
    _model.visited[start.getX()][start.getY()] = true;
    while (queue.getSize() != 0) {
      //出队
      Position curPosition = queue.removeRandom();
      //对上、下、左、右四个方向进行遍历,并获得一个新位置
      for (int i = 0; i < 4; i++) {
        int newX = curPosition.getX() + _model.direction[i][0] * 2;
        int newY = curPosition.getY() + _model.direction[i][1] * 2;
        //如果新位置在地图范围内且该位置没有被访问过
        if (_model.isInArea(newX, newY) && !_model.visited[newX][newY]) {
          //入队
          queue.addRandom(new Position(newX, newY, prePosition: curPosition));
          //设置该位置为已访问
          _model.visited[newX][newY] = true;
          //设置该位置为路
          _setModelWithRoad(curPosition.getX() + _model.direction[i][0], curPosition.getY() + _model.direction[i][1]);
        }
      }
    }
  }

6.自动解迷宫(提示功能)

//自动解迷宫(提示功能)
  //从起点位置开始(使用递归的方式)求解迷宫,如果求解成功则返回true,否则返回false
  bool _doSolver(int x, int y) {
    if (!_model.isInArea(x, y)) {
      throw "坐标越界";
    }
    //设置已访问
    _model.visited[x][y] = true;
    //设置该位置为正确路径
    _setModelWithPath(x, y, true);

    //如果该位置为终点位置,则返回true
    if (x == _model.getEndX() && y == _model.getEndY()) {
      return true;
    }
    //对四个方向进行遍历,并获得一个新位置
    for (int i = 0; i < 4; i++) {
      int newX = x + _model.direction[i][0];
      int newY = y + _model.direction[i][1];
      //如果该位置在地图范围内,且该位置为路,且该位置没有被访问过,则继续从该点开始递归求解
      if (_model.isInArea(newX, newY) &&
          _model.mazeMap[newX][newY] == MazeGameModel.MAP_ROAD &&
          !_model.visited[newX][newY]) {
        if (_doSolver(newX, newY)) {
          return true;
        }
      }
    }
    
    //如果该位置不是正确的路径,则将该位置设置为非正确路径所途径的位置
    _setModelWithPath(x, y, false);
    return false;
  }

7.控制玩家角色移动

  • 移动到新位置
//控制玩家角色移动
  void _doPlayerMove(String direction) {
    switch (direction) {
      case "上":
      //如果待移动的目标位置在迷宫地图内,且该位置是路,则进行移动
        if (_model.isInArea(_model.playerX - 1, _model.playerY) && _model.mazeMap[_model.playerX - 1][_model.playerY] == 1) {
          setState(() {
            _model.playerX--;
          });
        }
        break;
//省略其他三个方向的代码
  • 玩家到达终点位置
//如果玩家角色到达终点位置
if (_model.playerX == _model.getEndX() && _model.playerY == _model.getEndY()) {
      isTip = false;     //刷新可提示次数
      timer.cancel();    //取消倒计时
      //如果当前关是第10关
      if (level == 10) {
        showDialog(
            barrierDismissible: false,
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                content: Text("骚年,你已成功挑战10关,我看你骨骼惊奇,适合玩迷宫(狗头"),
                actions: <Widget>[
                  new FlatButton(
                    child: new Text('继续挑战第10关(新地图)', style: TextStyle(fontSize: 16)),
                    onPressed: () {
                      setState(() {
                        _model.playerX = _model.getStartX();
                        _model.playerY = _model.getStartY();
                      });
                      //重新初始化数据
                      _model = new MazeGameModel(rowSum, columnSum);
                      //生成迷宫和设置倒计时
                      _doGenerator(_model.getStartX(), _model.getStartY() + 1);
                      _setSurplusTime(level);
                      Navigator.of(context).pop();
                    },
                  )
                ],
              );
            });
      }
      //如果当前关不是第10关
      else {
        showDialog(
            barrierDismissible: false,
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                content: Text("恭喜闯关成功"),
                actions: <Widget>[
                  new FlatButton(
                    child: new Text('挑战下一关', style: TextStyle(fontSize: 16)),
                    onPressed: () {
                      setState(() {
                        //关卡数+1,玩家角色回到起点
                        level++;
                        _model.playerX = _model.getStartX();
                        _model.playerY = _model.getStartY();
                      });
                      //重新初始化数据
                      _model = new MazeGameModel(rowSum = rowSum + 4, columnSum = columnSum + 4);
                      //生成迷宫和设置倒计时
                      _doGenerator(_model.getStartX(), _model.getStartY() + 1);
                      _setSurplusTime(level);
                      Navigator.of(context).pop();
                    },
                  )
                ],
              );
            });
      }
    }

注:其他与控制逻辑相关的方法不在此文中详细介绍,有兴趣的朋友可以到完整项目源码地址中浏览。

发布了1 篇原创文章 · 获赞 6 · 访问量 167

猜你喜欢

转载自blog.csdn.net/moxiaoliang123456/article/details/105538221