Difference between revisions of "Simple Rogue levels in Dart"

From RogueBasin
Jump to navigation Jump to search
(Initial article)
 
(option to not render some rooms)
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
== Dart Implementation of Simple Rogue Levels ==
== Dart Implementation of Simple Rogue Levels ==


The Dart code below generates simple maps.
The Dart code below generates simple maps with rooms and paths, with some rooms optionally disabled.


It will return the map in primitives, either:
The code will return the map in primitives, either:


* A list-of-lists with the individual characters  
* A list-of-lists with the individual characters.
* A string with linebreaks separating the rows
* A string with linebreaks separating the rows.


In both cases, the returned map can be easily parsed and fed into various map engines.
In both cases, the returned map can be easily parsed and fed into various map engines.
Line 13: Line 13:


Instantiate <code>Grid</code> and call either <code>paint()</code> or <code>map()</code>.
Instantiate <code>Grid</code> and call either <code>paint()</code> or <code>map()</code>.
Setting <code>ratioOfDisabledRooms = 0.0</code> will paint all rooms.


<syntaxhighlight lang="dart" line>
<syntaxhighlight lang="dart" line>
Line 20: Line 22:
int rows = 3;
int rows = 3;


Grid grid = Grid(width, height, cols, rows);
// set to 0.0 for not disabled rooms
double ratioOfDisabledRooms = 2 / 9;
 
Grid grid = Grid(
  width,  
  height,  
  cols,  
  rows,
  ratioOfDisabledRooms: ratioOfDisabledRooms);


final String paint = grid.paint();
final String paint = grid.paint();
Line 40: Line 50:


== Preview ==
== Preview ==
Sometimes, paths display small bumps and are not "clean".


<pre>
<pre>
                                                                    ╔══════╗
                                    ╔══════╗
                                                                    ║......║
              ╔═══╗                ║......║
            ╔═════╗                                                ║......║
              ║...║                ║......║
            ║.....║       ╔════╗                                  ║......║
              ║...║                ║......╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ╔══════╗
            ║.....║       ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬......║
              ...╬▒                ......║                  ▒▒▒▒▒╬......║
            ║.....╬▒▒▒▒▒▒▒▒╬....║                                 ╚══════╝
              ║...║▒                ║......║                        ......║
            ║.....║       ║....║
              ...║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬......║                        ║......║
            ║.....║       ╚══╬═╝
              ║...║                ║......║                        ║......║
            ╚════╬╝         
              ╚═══╝                ╚══════╝                       ╚═╬════╝
              ▒▒▒▒▒           ▒
                                                                      ▒
              ▒              ▒▒
                                                                      ▒▒▒▒▒
                           
                                                                    ╔══════╬╗
  ╔══════════╬╗             
                                  ╔═══════════════╗                ║.......║
   ║...........║           ╔═╬══════════╗                      ╔═══════╗
                                  ║...............║                .......║
  ║...........╬▒▒▒▒▒▒▒▒▒▒  ║............║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
                            ▒▒▒▒▒╬...............║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
   ║...........║         ▒  ║............║  ▒                  ║.......║
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ║...............╬▒                .......║
   ╚═╬═════════╝        ▒▒▒╬............╬▒▒▒                  ║.......
                ▒                ║...............║                .......║
    ▒                      ╚═════╬══════╝                      ║.......║
                                ╚═╬═════════════╝                ║.......║
    ▒                            ▒▒▒▒▒▒▒▒▒▒▒                  ╚═══════╝
          ▒▒▒▒▒▒                  ▒▒▒▒▒▒                          ╚═══╬═══╝
    ▒                                      ▒
           ▒                             ▒              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
    ▒▒▒▒▒▒▒▒▒▒▒▒▒                          ▒▒▒▒▒
                                                ╔═══╬══════╗
      ╔══════════╬══════╗          ╔════════════╬═╗
      ╔═══╬╗                                      ║..........║
      ║.................║         ║..............║           ╔════════════╗
      ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒          ║..........║
      ║.................║         ║..............║          ║............║
      ║....║                      ▒    ▒      ▒▒▒▒▒╬..........║
      ║.................╬▒▒▒▒▒▒▒  ║..............╬▒▒▒▒      ║............║
      ║....║                      ▒▒▒▒▒▒▒▒▒▒▒▒▒    ║..........║
      ╚═════════════════╝      ▒▒▒▒╬..............║  ▒       ║............║
      ║....║                                        ╚══════════╝
                                  ╚══════════════╝  ▒▒▒▒▒▒▒▒╬............║
      ║....║
                                                              ╚════════════╝
      ╚════╝
</pre>
 
<pre>
  ╔═══════════════╗            ╔════════╗
   ║...............╬▒▒▒          ........║
  ...............║  ▒▒▒▒▒▒▒▒▒▒▒╬........║                   ╔═══════╗
   ║...............║             ║........╬▒▒▒▒▒▒▒            ║.......║
  ║...............║            ╚════╬═══╝      ▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
  ║...............║                  ▒                        ║.......║
  ║...............║                  ▒                        ║.......║
  ╚═══════════════╝                  ▒                        ╚══╬════╝
                                      ▒                          ▒▒▒
                                      ▒                            ╔╬══════╗
                                      ▒▒                          ║.......
                                  ╔═══╬╗                          ║.......║
        ╔═════════╗              ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒           ║.......║
        ║.........║               ║....║              ▒          ║.......
        ║.........║              ║....║              ▒          ║.......
        ║.........║              ║....║              ▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
        ║.........║              ╚══╬═╝                          ║.......║
        ╚═╬═══════╝                  ▒                            ╚══╬════╝
   ▒▒▒▒▒▒▒▒▒▒                          ▒                              ▒▒▒
╔╬═══╗                                                              ▒
║....║                              ▒                                ▒
....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒                                ▒
....║                             ▒ ▒                                ▒
....║                            ▒ ▒                            ▒▒▒▒▒▒▒▒
....║                             ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
╚════╝
</pre>
</pre>


== Dart Implementation ==
== Dart Implementation ==
The code consists of serval classes, copy the code and inspect each class on its own.


Constructs a <code>Grid</code> with a number of <code>rows</code> and <code>columns</code>.
Constructs a <code>Grid</code> with a number of <code>rows</code> and <code>columns</code>.
Line 80: Line 123:
A <code>Cell</code> holds a <code>Room</code>.
A <code>Cell</code> holds a <code>Room</code>.


A <code>Room</code> has walls and doors. A <code>Room</code> holds a <code>Path</code> connecting it to another <code>Room</code>.
A <code>Cell</code> holds <code>Paths</code> between its <code>Room</code> and other <code>Rooms</code>.
 
Calling <code>Grid.paint()</code> will paint:
 
1. The background of all <code>Cell</code>s.
 
2. All <code>Room</code>s.
 
3. All <code>Path</code>s.  


<code>Grid.paint()</code> calls <code>Cell.paint()</code>, which calls <code>Room.paint()</code>, which calls <code>Path.paint()</code>.


<syntaxhighlight lang="dart" line>
<syntaxhighlight lang="dart" line>
Line 101: Line 151:
   final String _roomDoor;
   final String _roomDoor;
   final String _roomCorridor;
   final String _roomCorridor;
  final String _roomNoRender;
   final int _minRoomInnerDimension;
   final int _minRoomInnerDimension;
  final double _ratioOfDisabledRooms;
  final bool _paintSectionIds;
   late final List<List<Cell>> _grid;
   late final List<List<Cell>> _grid;


Line 110: Line 163:
     int rows, {
     int rows, {
     int minRoomInnerDimension = 3,
     int minRoomInnerDimension = 3,
    double ratioOfDisabledRooms = 2 / 9,
    bool paintSectionIds = false,
     String cellBorder = ' ', // █ mainly for debug, set to blank
     String cellBorder = ' ', // █ mainly for debug, set to blank
     String cellFill = ' ',
     String cellFill = ' ',
Line 121: Line 176:
     String roomDoor = '╬',
     String roomDoor = '╬',
     String roomCorridor = '▒',
     String roomCorridor = '▒',
    String roomNoRender = ' ', // set this to '/' to paint no-render rooms
   })  : _minRoomInnerDimension = minRoomInnerDimension,
   })  : _minRoomInnerDimension = minRoomInnerDimension,
        _ratioOfDisabledRooms = ratioOfDisabledRooms,
        _paintSectionIds = paintSectionIds,
         _cellBorder = cellBorder,
         _cellBorder = cellBorder,
         _cellFill = cellFill,
         _cellFill = cellFill,
Line 132: Line 190:
         _roomFloor = roomFloor,
         _roomFloor = roomFloor,
         _roomDoor = roomDoor,
         _roomDoor = roomDoor,
         _roomCorridor = roomCorridor {
         _roomCorridor = roomCorridor,
        _roomNoRender = roomNoRender {
    CellIdHelper? idHelper = _paintSectionIds ? CellIdHelper() : null;
 
     //
     //
     // Initialize the all cells and their corresponding room inside.
     // Initialize the all cells and their corresponding room inside.
     //
     //
     _grid = Cell(0, 0, _width, _height).rows(rows).map((cellRow) {
     _grid = Cell(0, 0, _width, _height).rows(rows).map((cellRow) {
       final cols = cellRow.cols(columns);
       final cols = cellRow.cols(columns, cellIdHelper: idHelper);
       for (var columnCell in cols) {
       for (var columnCell in cols) {
         columnCell.setRoom(minRoomInnerDimension: _minRoomInnerDimension);
         columnCell.setRoom(minRoomInnerDimension: _minRoomInnerDimension);
Line 144: Line 205:
     }).toList();
     }).toList();


     _calculateNeigbours(); // calculate neighbours in the grid pattern
     _calculateNeigbours(); // calculate cell neighbours in the grid pattern
     _connectNeighboursRandomly(); // which neigbours are connected
     _connectNeighboursRandomly(); // which cell neigbours are connected
    _disallowRoomRenderingRandomly(
        ratioNotRender:
            _ratioOfDisabledRooms); // forbid some cells to render a room
     _connectNeighboursByPaths(); // calculate the paths between neigbours
     _connectNeighboursByPaths(); // calculate the paths between neigbours
   }
   }


   List<Cell> _tableCellsAsList() => _grid.expand((s) => s).toList();
   List<Cell> _gridCellsAsList() => _grid.expand((s) => s).toList();


   /// A [String] representation of the map.
   /// A [String] representation of the map.
Line 159: Line 223:
         _height, (index) => List.generate(_width, (index) => _cellFill));
         _height, (index) => List.generate(_width, (index) => _cellFill));


     for (final cell in _tableCellsAsList()) {
     for (final cell in _gridCellsAsList()) {
       cell.paintBackground(
       cell.paintBackground(
         map,
         map,
Line 167: Line 231:
     }
     }


     for (final cell in _tableCellsAsList()) {
     for (final cell in _gridCellsAsList()) {
       cell.paint(
       cell.paintRoom(
         map,
         map,
         _roomCornerTopLeft,
         _roomCornerTopLeft,
Line 177: Line 241:
         _roomBorderHorizontal,
         _roomBorderHorizontal,
         _roomFloor,
         _roomFloor,
        _roomNoRender,
      );
    }
    for (final cell in _gridCellsAsList()) {
      cell.paintPaths(
        map,
         _roomDoor,
         _roomDoor,
         _roomCorridor,
         _roomCorridor,
Line 210: Line 281:
   /// [Cell]s are connected and form a connected map.
   /// [Cell]s are connected and form a connected map.
   void _connectNeighboursRandomly() {
   void _connectNeighboursRandomly() {
     final List<Cell> cells = _tableCellsAsList();
     final List<Cell> cells = _gridCellsAsList();
     final int segmentLengthExpected = cells.length;
     final int segmentLengthExpected = cells.length;
     final List<int> cellsIndex =
     final List<int> cellsIndex =
Line 233: Line 304:
   }
   }


  /// Will disallow random [Cell]s to render a [Room].
  ///
  /// The [ratioNotRender] defines an AVERAGE accross all [Grid] generations.
  /// It does not gurantee that that a certain numer of [Room]s is
  /// not rendered or rendered. If feeds an internal randomizer.
  ///
  /// Instead, these [Cell]s will join all the incoming [Path]s
  /// of a [Room] and form a junction.
  ///
  /// `2 / 9`  means that `2 / 9` cells will not render.
  void _disallowRoomRenderingRandomly({final double ratioNotRender = 2 / 9}) {
    final List<Cell> cells = _gridCellsAsList();
    final countNegative = (ratioNotRender * cells.length).toInt();
    final countPositive = cells.length - countNegative;
    final random = [
      ...List.generate(countNegative, (_) => false),
      ...List.generate(countPositive, (_) => true)
    ];
    assert(random.length == cells.length);
    for (int i = 0; i < cells.length; i++) {
      random.shuffle();
      cells[i].doRenderRoom = random.first;
    }
  }
  /// Connects live cells.
  ///
  /// Or live cells connected by dead cells.
   void _connectNeighboursByPaths() {
   void _connectNeighboursByPaths() {
     // Checks bilater connections
     // Checks bilater connections
     final List<(Cell, Cell)> roomsAlreadyConnected = [];
     final List<(Cell, Cell)> roomsAlreadyConnected = [];
     final cells = _tableCellsAsList();
     final cells = _gridCellsAsList();


     for (final cellStart in cells) {
     for (final cellStart in cells) {
Line 247: Line 350:
         roomsAlreadyConnected.add((cellStart, cellEnd));
         roomsAlreadyConnected.add((cellStart, cellEnd));


         final roomStart = cellStart.room;
         if (cellStart.isInDeadPath() == true ||
        final roomEnd = cellEnd.room;
            cellEnd.isInDeadPath() == true) {
          continue;
        }


         final Path path = _getRoomDoorsRandom(cellStart, cellEnd);
         final Path path = _getRoomDoorsRandom(cellStart, cellEnd);


         roomStart.paths.add(path);
         cellStart.paths.add(path);
        roomEnd.paths.add(path.cloneSwapNoPaintPath());
       }
       }
     }
     }
Line 263: Line 367:
   /// will be on the right side of its room, and the door of [cellEnd]
   /// will be on the right side of its room, and the door of [cellEnd]
   /// will be o the left side of its room.
   /// will be o the left side of its room.
  ///
  /// In case of `Cell.doRenderRoom==false` the center location
  /// of that [Cell] is assumed to be the single "door" of that [Cell].
   static Path _getRoomDoorsRandom(Cell cellStart, Cell cellEnd) {
   static Path _getRoomDoorsRandom(Cell cellStart, Cell cellEnd) {
     Point<int> startDoor;
     Point<int> startDoor;
Line 269: Line 376:
     Point<int> endDoorConn;
     Point<int> endDoorConn;


     final Room roomStart = cellStart.room;
     (Point<int>, Point<int>) getLeftOrCenter(Cell cell) =>
     final Room roomEnd = cellEnd.room;
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.leftDoorAndConnector();
 
     (Point<int>, Point<int>) getTopOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.topDoorAndConnector();
 
    (Point<int>, Point<int>) getRightOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.rightDoorAndConnector();
 
    (Point<int>, Point<int>) getBottomOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.bottomDoorAndConnector();


     if (cellEnd == cellStart.neighbourLeft) {
     if (cellEnd == cellStart.neighbourLeft) {
       (startDoor, startDoorConn) = roomStart.leftDoorAndConnector();
       (startDoor, startDoorConn) = getLeftOrCenter(cellStart);
       (endDoor, endDoorConn) = roomEnd.rightDoorAndConnector();
       (endDoor, endDoorConn) = getRightOrCenter(cellEnd);
     } else if (cellEnd == cellStart.neighbourTop) {
     } else if (cellEnd == cellStart.neighbourTop) {
       (startDoor, startDoorConn) = roomStart.topDoorAndConnector();
       (startDoor, startDoorConn) = getTopOrCenter(cellStart);
       (endDoor, endDoorConn) = roomEnd.bottomDoorAndConnector();
       (endDoor, endDoorConn) = getBottomOrCenter(cellEnd);
     } else if (cellEnd == cellStart.neighbourRight) {
     } else if (cellEnd == cellStart.neighbourRight) {
       (startDoor, startDoorConn) = roomStart.rightDoorAndConnector();
       (startDoor, startDoorConn) = getRightOrCenter(cellStart);
       (endDoor, endDoorConn) = roomEnd.leftDoorAndConnector();
       (endDoor, endDoorConn) = getLeftOrCenter(cellEnd);
     } else {
     } else {
       //if (cellEnd == cellStart.neighbourBottom) {
       //if (cellEnd == cellStart.neighbourBottom) {
       (startDoor, startDoorConn) = roomStart.bottomDoorAndConnector();
       (startDoor, startDoorConn) = getBottomOrCenter(cellStart);
       (endDoor, endDoorConn) = roomEnd.topDoorAndConnector();
       (endDoor, endDoorConn) = getTopOrCenter(cellEnd);
     }
     }
     return Path(startDoor, endDoor, startDoorConn, endDoorConn);
     return Path(startDoor, endDoor, startDoorConn, endDoorConn);
   }
   }
Line 304: Line 429:
   final Set<Cell> connectedNeighbours = {};
   final Set<Cell> connectedNeighbours = {};
   late Room room;
   late Room room;
  bool doRenderRoom = true;
  final String? cellId;
  final Set<Path> paths = {};


   Cell(
   Cell(int left, int top, int width, int height, [this.cellId])
    int left,
      : super(left, top, width, height);
    int top,
    int width,
    int height,
  ) : super(left, top, width, height);


   void paintBackground(
   void paintBackground(
Line 334: Line 459:
         }
         }
       }
       }
    }
    if (cellId != null) {
      map[top][left] = cellId!;
    }
  }
  void paintPaths(
      List<List<String>> map, String roomDoor, String roomCorridor) {
    for (final Path path in paths) {
      path.paint(map, roomDoor, roomCorridor);
     }
     }
   }
   }


   void paint(
   void paintRoom(
     List<List<String>> map,
     List<List<String>> map,
     String roomCornerTopLeft,
     String roomCornerTopLeft,
Line 346: Line 482:
     String roomBorderHorizontal,
     String roomBorderHorizontal,
     String roomFloor,
     String roomFloor,
     String roomDoor,
     String roomNoRender,
    String roomCorridor,
   ) =>
   ) =>
       room.paint(
       room.paint(
          map,
        map,
          roomCornerTopLeft,
        roomCornerTopLeft,
          roomCornerTopRight,
        roomCornerTopRight,
          roomCornerBottomLeft,
        roomCornerBottomLeft,
          roomCornerBottomRight,
        roomCornerBottomRight,
          roomBorderVertical,
        roomBorderVertical,
          roomBorderHorizontal,
        roomBorderHorizontal,
          roomFloor,
        roomFloor,
          roomDoor,
        roomNoRender,
          roomCorridor);
        doRenderRoom,
      );


   /// The layout is based in collapsed borders. This, a room needs to know
   /// The layout is based in collapsed borders. This, a room needs to know
Line 425: Line 561:


   /// Splits this [Cell] into a number of horizontal [cells].
   /// Splits this [Cell] into a number of horizontal [cells].
   List<Cell> cols(int cells) {
   List<Cell> cols(int cells, {CellIdHelper? cellIdHelper}) {
     int cellWidth = width ~/ cells;
     int cellWidth = width ~/ cells;
     assert(cellWidth <= width);
     assert(cellWidth <= width);
Line 445: Line 581:
       }
       }


       ret.add(Cell(x0, y0, x1 - x0, y1 - y0));
      final cellId = cellIdHelper?.increment;
 
       ret.add(Cell(x0, y0, x1 - x0, y1 - y0, cellId));
     }
     }


Line 452: Line 590:


   /// Splits this [Cell] into a number of vertical [cells].
   /// Splits this [Cell] into a number of vertical [cells].
   List<Cell> rows(int cells) {
   List<Cell> rows(int cells, {CellIdHelper? cellIdHelper}) {
     int cellHeight = height ~/ cells;
     int cellHeight = height ~/ cells;
     assert(cellHeight <= height);
     assert(cellHeight <= height);
Line 472: Line 610:
       }
       }


       ret.add(Cell(x0, y0, x1 - x0, y1 - y0));
      final cellId = cellIdHelper?.increment;
 
       ret.add(Cell(
        x0,
        y0,
        x1 - x0,
        y1 - y0,
        cellId,
      ));
     }
     }


     return ret;
     return ret;
  }
  /// If this [Cell] is `doRenderRoom == false` AND has only one path direction with
  /// a [Cell] where `doRenderRoom == true`.
  bool isInDeadPath() {
    if (doRenderRoom == true) {
      return false;
    }
    final liveDirections = _liveDirections({});
    return liveDirections == 1;
  }
  int _liveDirections(Set<Cell> visited) {
    visited.add(this);
    int directions = 0;
    final neighs = connectedNeighbours; // between 1-4
    final lives = neighs.where((cell) => cell.doRenderRoom == true).toSet();
    if (lives.isNotEmpty) {
      directions = lives.length;
      visited.addAll(lives);
    }
    final deads = neighs.where((cell) => cell.doRenderRoom == false).toSet();
    for (final dead in deads) {
      if (visited.contains(dead) == false) {
        int count = dead._liveDirections(visited);
        if (count > 0) {
          directions += 1;
        }
      }
    }
    return directions;
   }
   }


   /// Returns ONE random neigbour.
   /// Returns ONE random neigbour.
   Cell _getNeighbourRandom() {
   Cell _getNeighbourRandom() {
    final List<Cell> neigbours = _getNeighbours();
    neigbours.shuffle();
    return neigbours.first;
  }
  /// Returns ONE random neigbour.
  List<Cell> _getNeighbours() {
     final List<Cell> neigbours = [];
     final List<Cell> neigbours = [];


Line 495: Line 684:
     }
     }


    neigbours.shuffle();
     return neigbours;
     return neigbours.first;
   }
   }


Line 522: Line 710:
/// A [Room] inside a [Cell].
/// A [Room] inside a [Cell].
class Room extends Rectangle<int> {
class Room extends Rectangle<int> {
  final Set<Path> paths = {};
   Room(
   Room(
     int left,
     int left,
Line 530: Line 716:
     int height,
     int height,
   ) : super(left, top, width, height);
   ) : super(left, top, width, height);
  (Point<int> door, Point<int> connector) centralDoor() {
    final p = Point<int>(left + width ~/ 2, top + height ~/ 2);
    return (p, p);
  }


   (Point<int> door, Point<int> connector) leftDoorAndConnector() {
   (Point<int> door, Point<int> connector) leftDoorAndConnector() {
Line 548: Line 739:
   (Point<int>, Point<int>) bottomDoorAndConnector() {
   (Point<int>, Point<int>) bottomDoorAndConnector() {
     final rnd = _randomWidth;
     final rnd = _randomWidth;
     return (Point<int>(rnd, bottom - 1), Point<int>(rnd, bottom));
     return (Point<int>(rnd, bottom - 1), Point<int>(rnd, bottom));
   }
   }
Line 569: Line 759:
     String roomBorderHorizontal,
     String roomBorderHorizontal,
     String roomFloor,
     String roomFloor,
     String roomDoor,
     String roomNoRender,
     String roomCorridor,
     bool doRenderRoom,
   ) {
   ) {
    if (doRenderRoom == false) {
      roomCornerTopLeft = roomNoRender;
      roomCornerBottomLeft = roomNoRender;
      roomCornerTopRight = roomNoRender;
      roomCornerBottomRight = roomNoRender;
      roomBorderHorizontal = roomNoRender;
      roomBorderVertical = roomNoRender;
      roomFloor = roomNoRender;
    }
     for (int y = top; y < top + height; y++) {
     for (int y = top; y < top + height; y++) {
       for (int x = left; x < left + width; x++) {
       for (int x = left; x < left + width; x++) {
Line 599: Line 799:
         }
         }
       }
       }
    }
    for (final Path path in paths) {
      path.paint(map, roomDoor, roomCorridor);
     }
     }
   }
   }
Line 622: Line 818:
   Point<int> startDoorConnection;
   Point<int> startDoorConnection;
   Point<int> endDoorConnection;
   Point<int> endDoorConnection;
   bool doPaintPath;
   bool alreadyPainted = false; // avoids painting paths twice
   Path(
 
    this.startDoor,
   Path(this.startDoor, this.endDoor, this.startDoorConnection,
    this.endDoor,
      this.endDoorConnection);
    this.startDoorConnection,
    this.endDoorConnection, {
    this.doPaintPath = true,
  });


   Path cloneSwapNoPaintPath() {
   // Path cloneSwapNoPaintPath() {
    return Path(endDoor, startDoor, endDoorConnection, startDoorConnection,
  //  return Path(endDoor, startDoor, endDoorConnection, startDoorConnection);
        doPaintPath: false);
   // }
   }


   /// A [Path] paints the start-ROOM door it belongs to.
   /// A [Path] paints the start-ROOM door it belongs to.
Line 645: Line 836:
     String roomDoor,
     String roomDoor,
     String roomCorridor,
     String roomCorridor,
  ) {
    map[startDoor.y][startDoor.x] = roomDoor;
    map[endDoor.y][endDoor.x] = roomDoor;
    PathPainer(map).paint(roomCorridor, startDoorConnection, endDoorConnection);
  }
  @override
  String toString() {
    return 'conn1: $startDoorConnection conn2: $endDoorConnection';
  }
}
// --------------------------------------------------------------------------------
// Paints paths.
// --------------------------------------------------------------------------------
class PathPainer {
  final List<List<String>> _map;
  PathPainer(this._map);
  /// Will paint a path between [start] and [end].
  paint(
    String draw,
    Point<int> start,
    Point<int> end,
   ) {
   ) {
     paintCorridor(Point<int> point) {
     paintCorridor(Point<int> point) {
       map[point.y][point.x] = roomCorridor;
       _map[point.y][point.x] = draw;
     }
     }


     map[startDoor.y][startDoor.x] = roomDoor;
     paintCorridor(start);
 
    paintCorridor(end);
    if (doPaintPath) {
      paintCorridor(startDoorConnection);
      paintCorridor(endDoorConnection);


      final startX = min(startDoorConnection.x, endDoorConnection.x);
    final startX = min(start.x, end.x);
      final endX = max(startDoorConnection.x, endDoorConnection.x);
    final endX = max(start.x, end.x);


      final startY = min(startDoorConnection.y, endDoorConnection.y);
    final startY = min(start.y, end.y);
      final endY = max(startDoorConnection.y, endDoorConnection.y);
    final endY = max(start.y, end.y);


      if (startX == endX) {
    if (startX == endX) {
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------
        // START and END are on the same ROW (y)
      // START and END are on the same ROW (y)
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------


        for (int y = startY; y < endY; y++) {
      for (int y = startY; y < endY; y++) {
          paintCorridor(Point(startDoorConnection.x, y));
        paintCorridor(Point(start.x, y));
        }
      }
      } else if (startY == endY) {
    } else if (startY == endY) {
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------
        // START and END are on the same COLUMN (x)
      // START and END are on the same COLUMN (x)
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------


        for (int x = startX; x < endX; x++) {
      for (int x = startX; x < endX; x++) {
          paintCorridor(Point(x, startDoorConnection.y));
        paintCorridor(Point(x, start.y));
        }
      }
      } else {
    } else {
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------
        // START and END are not on the same axis
      // START and END are not on the same axis
        // ------------------------------------------------------------------------
      // ------------------------------------------------------------------------


        // This generates a map with path-corners that are not
      // This generates a map with path-corners that are not
        // all at the center of the path.
      // all at the center of the path.
        final rangeX = List.generate((endX - startX).abs(), (index) => index);
      final rangeX = List.generate((endX - startX).abs(), (index) => index);
        final rangeY = List.generate((endY - startY).abs(), (index) => index);
      final rangeY = List.generate((endY - startY).abs(), (index) => index);


        rangeX.shuffle();
      rangeX.shuffle();
        rangeY.shuffle();
      rangeY.shuffle();


        final int deltaXAbsolute = startX + rangeX.first;
      final int deltaXAbsolute = startX + rangeX.first;
        final int deltaYAbsolute = startY + rangeY.first;
      final int deltaYAbsolute = startY + rangeY.first;


        if (startDoorConnection.x < endDoorConnection.x) {
      if (start.x < end.x) {
          for (int x = startX; x <= deltaXAbsolute; x++) {
        for (int x = startX; x <= deltaXAbsolute; x++) {
            paintCorridor(Point(x, startDoorConnection.y));
          paintCorridor(Point(x, start.y));
          }
        }


          for (int x = endX; x >= deltaXAbsolute; x--) {
        for (int x = endX; x >= deltaXAbsolute; x--) {
            paintCorridor(Point(x, endDoorConnection.y));
          paintCorridor(Point(x, end.y));
          }
        }


          for (int y = startY; y < endY; y++) {
        for (int y = startY; y < endY; y++) {
            paintCorridor(Point(deltaXAbsolute, y));
          paintCorridor(Point(deltaXAbsolute, y));
          }
        }
        } else {
      } else {
          for (int y = startY; y <= deltaYAbsolute; y++) {
        for (int y = startY; y <= deltaYAbsolute; y++) {
            paintCorridor(Point(startDoorConnection.x, y));
          paintCorridor(Point(start.x, y));
          }
        }


          for (int y = endY; y >= deltaYAbsolute; y--) {
        for (int y = endY; y >= deltaYAbsolute; y--) {
            paintCorridor(Point(endDoorConnection.x, y));
          paintCorridor(Point(end.x, y));
          }
        }


          for (int x = startX; x < endX; x++) {
        for (int x = startX; x < endX; x++) {
            paintCorridor(Point(x, deltaYAbsolute));
          paintCorridor(Point(x, deltaYAbsolute));
          }
         }
         }
       }
       }
     }
     }
   }
   }
}
// --------------------------------------------------------------------------------
// Helper class to have debug Cell-IDs.
// --------------------------------------------------------------------------------
class CellIdHelper {
  int _id = 0;
  String get increment => String.fromCharCode(_id++ + 65);
}
}
</syntaxhighlight>
</syntaxhighlight>

Latest revision as of 14:24, 15 October 2023

Dart Implementation of Simple Rogue Levels

The Dart code below generates simple maps with rooms and paths, with some rooms optionally disabled.

The code will return the map in primitives, either:

  • A list-of-lists with the individual characters.
  • A string with linebreaks separating the rows.

In both cases, the returned map can be easily parsed and fed into various map engines.

Usage

Instantiate Grid and call either paint() or map().

Setting ratioOfDisabledRooms = 0.0 will paint all rooms.

int width = 80;
int height = 30;
int cols = 3;
int rows = 3;

 // set to 0.0 for not disabled rooms
double ratioOfDisabledRooms = 2 / 9;

Grid grid = Grid(
  width, 
  height, 
  cols, 
  rows, 
  ratioOfDisabledRooms: ratioOfDisabledRooms);

final String paint = grid.paint();
print(paint);

List<List<String>> map = grid.map();
print(map);

The constructor allows overwriting the default ASCII definitions.

Grid grid = Grid(80, 30, 3, 3,
  roomDoor = '/',
  roomCorridor = '#',
  // ...
);

Preview

Sometimes, paths display small bumps and are not "clean".

                                     ╔══════╗
               ╔═══╗                 ║......║
               ║...║                 ║......║
               ║...║                 ║......╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ╔══════╗
               ║...╬▒                ║......║                   ▒▒▒▒▒╬......║
               ║...║▒                ║......║                        ║......║
               ║...║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬......║                        ║......║
               ║...║                 ║......║                        ║......║
               ╚═══╝                 ╚══════╝                        ╚═╬════╝
                                                                       ▒
                                                                       ▒▒▒▒▒
                                                                    ╔══════╬╗
                                  ╔═══════════════╗                 ║.......║
                                  ║...............║                 ║.......║
                             ▒▒▒▒▒╬...............║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ║...............╬▒                ║.......║
                ▒                 ║...............║                 ║.......║
                ▒                 ╚═╬═════════════╝                 ║.......║
           ▒▒▒▒▒▒                   ▒▒▒▒▒▒                          ╚═══╬═══╝
           ▒                             ▒               ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
           ▒                             ▒           ╔═══╬══════╗
       ╔═══╬╗                            ▒           ║..........║
       ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ▒           ║..........║
       ║....║                       ▒    ▒      ▒▒▒▒▒╬..........║
       ║....║                       ▒▒▒▒▒▒▒▒▒▒▒▒▒    ║..........║
       ║....║                                        ╚══════════╝
       ║....║
       ╚════╝
   ╔═══════════════╗             ╔════════╗
   ║...............╬▒▒▒          ║........║
   ║...............║  ▒▒▒▒▒▒▒▒▒▒▒╬........║                    ╔═══════╗
   ║...............║             ║........╬▒▒▒▒▒▒▒             ║.......║
   ║...............║             ╚════╬═══╝      ▒▒▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
   ║...............║                  ▒                        ║.......║
   ║...............║                  ▒                        ║.......║
   ╚═══════════════╝                  ▒                        ╚══╬════╝
                                      ▒                           ▒▒▒
                                      ▒                            ╔╬══════╗
                                      ▒▒                           ║.......║
                                   ╔═══╬╗                          ║.......║
         ╔═════════╗               ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒           ║.......║
         ║.........║               ║....║              ▒           ║.......║
         ║.........║               ║....║              ▒           ║.......║
         ║.........║               ║....║              ▒▒▒▒▒▒▒▒▒▒▒▒╬.......║
         ║.........║               ╚══╬═╝                          ║.......║
         ╚═╬═══════╝                  ▒                            ╚══╬════╝
  ▒▒▒▒▒▒▒▒▒▒                          ▒                               ▒▒▒
 ╔╬═══╗                               ▒                                 ▒
 ║....║                               ▒                                 ▒
 ║....╬▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒                                 ▒
 ║....║                             ▒ ▒                                 ▒
 ║....║                             ▒ ▒                            ▒▒▒▒▒▒▒▒
 ║....║                             ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
 ╚════╝

Dart Implementation

The code consists of serval classes, copy the code and inspect each class on its own.

Constructs a Grid with a number of rows and columns.

The Grid holds a number of Cells.

A Cell holds a Room.

A Cell holds Paths between its Room and other Rooms.

Calling Grid.paint() will paint:

1. The background of all Cells.

2. All Rooms.

3. All Paths.


import 'dart:math';

class Grid {
  final int _width;
  final int _height;
  final String _cellBorder;
  final String _cellFill;
  final String _roomCornerTopLeft;
  final String _roomCornerTopRight;
  final String _roomCornerBottomLeft;
  final String _roomCornerBottomRight;
  final String _roomBorderVertical;
  final String _roomBorderHorizontal;
  final String _roomFloor;
  final String _roomDoor;
  final String _roomCorridor;
  final String _roomNoRender;
  final int _minRoomInnerDimension;
  final double _ratioOfDisabledRooms;
  final bool _paintSectionIds;
  late final List<List<Cell>> _grid;

  Grid(
    this._width,
    this._height,
    int columns,
    int rows, {
    int minRoomInnerDimension = 3,
    double ratioOfDisabledRooms = 2 / 9,
    bool paintSectionIds = false,
    String cellBorder = ' ', // █ mainly for debug, set to blank
    String cellFill = ' ',
    String roomCornerTopLeft = '╔',
    String roomCornerTopRight = '╗',
    String roomCornerBottomLeft = '╚',
    String roomCornerBottomRight = '╝',
    String roomBorderVertical = '═',
    String roomBorderHorizontal = '║',
    String roomFloor = '.',
    String roomDoor = '╬',
    String roomCorridor = '▒',
    String roomNoRender = ' ', // set this to '/' to paint no-render rooms
  })  : _minRoomInnerDimension = minRoomInnerDimension,
        _ratioOfDisabledRooms = ratioOfDisabledRooms,
        _paintSectionIds = paintSectionIds,
        _cellBorder = cellBorder,
        _cellFill = cellFill,
        _roomCornerTopLeft = roomCornerTopLeft,
        _roomCornerTopRight = roomCornerTopRight,
        _roomCornerBottomLeft = roomCornerBottomLeft,
        _roomCornerBottomRight = roomCornerBottomRight,
        _roomBorderVertical = roomBorderVertical,
        _roomBorderHorizontal = roomBorderHorizontal,
        _roomFloor = roomFloor,
        _roomDoor = roomDoor,
        _roomCorridor = roomCorridor,
        _roomNoRender = roomNoRender {
    CellIdHelper? idHelper = _paintSectionIds ? CellIdHelper() : null;

    //
    // Initialize the all cells and their corresponding room inside.
    //
    _grid = Cell(0, 0, _width, _height).rows(rows).map((cellRow) {
      final cols = cellRow.cols(columns, cellIdHelper: idHelper);
      for (var columnCell in cols) {
        columnCell.setRoom(minRoomInnerDimension: _minRoomInnerDimension);
      }
      return cols;
    }).toList();

    _calculateNeigbours(); // calculate cell neighbours in the grid pattern
    _connectNeighboursRandomly(); // which cell neigbours are connected
    _disallowRoomRenderingRandomly(
        ratioNotRender:
            _ratioOfDisabledRooms); // forbid some cells to render a room
    _connectNeighboursByPaths(); // calculate the paths between neigbours
  }

  List<Cell> _gridCellsAsList() => _grid.expand((s) => s).toList();

  /// A [String] representation of the map.
  String paint() => map().map((row) => row.join('')).join('\n');

  /// An array representation of the map.
  List<List<String>> map() {
    final map = List.generate(
        _height, (index) => List.generate(_width, (index) => _cellFill));

    for (final cell in _gridCellsAsList()) {
      cell.paintBackground(
        map,
        _cellFill,
        _cellBorder,
      );
    }

    for (final cell in _gridCellsAsList()) {
      cell.paintRoom(
        map,
        _roomCornerTopLeft,
        _roomCornerTopRight,
        _roomCornerBottomLeft,
        _roomCornerBottomRight,
        _roomBorderVertical,
        _roomBorderHorizontal,
        _roomFloor,
        _roomNoRender,
      );
    }

    for (final cell in _gridCellsAsList()) {
      cell.paintPaths(
        map,
        _roomDoor,
        _roomCorridor,
      );
    }

    return map;
  }

  /// Sets the left, top, right and bottom neighbours within
  /// the [_grid], or null if no neighbour is available.
  void _calculateNeigbours() {
    final int rows = _grid.length;
    final int cols = _grid.first.length;

    for (int y = 0; y < rows; y++) {
      for (int x = 0; x < cols; x++) {
        Cell? left = x < 1 ? null : _grid[y][x - 1];
        Cell? right = x >= cols - 1 ? null : _grid[y][x + 1];

        Cell? top = y < 1 ? null : _grid[y - 1][x];
        Cell? bottom = y >= rows - 1 ? null : _grid[y + 1][x];

        _grid[y][x].neighbourLeft = left;
        _grid[y][x].neighbourTop = top;
        _grid[y][x].neighbourRight = right;
        _grid[y][x].neighbourBottom = bottom;
      }
    }
  }

  /// Will add connections between all [Cell]s until all
  /// [Cell]s are connected and form a connected map.
  void _connectNeighboursRandomly() {
    final List<Cell> cells = _gridCellsAsList();
    final int segmentLengthExpected = cells.length;
    final List<int> cellsIndex =
        List<int>.generate(segmentLengthExpected, (i) => i);

    int segmentLengthTest;

    do {
      // Pick a random [Cell]
      cellsIndex.shuffle();
      final cell = cells[cellsIndex.first];

      // Connect a random neighbour of the [Cell] with the [Cell]
      final neighbour = cell._getNeighbourRandom();
      cell.connectedNeighbours.add(neighbour);
      neighbour.connectedNeighbours.add(cell);

      // Check if the count of all connected cells
      // equals the count of all cells.
      segmentLengthTest = cell._getAllNodesBFS({}).length;
    } while (segmentLengthTest != segmentLengthExpected);
  }

  /// Will disallow random [Cell]s to render a [Room].
  ///
  /// The [ratioNotRender] defines an AVERAGE accross all [Grid] generations.
  /// It does not gurantee that that a certain numer of [Room]s is
  /// not rendered or rendered. If feeds an internal randomizer.
  ///
  /// Instead, these [Cell]s will join all the incoming [Path]s
  /// of a [Room] and form a junction.
  ///
  /// `2 / 9`  means that `2 / 9` cells will not render.
  void _disallowRoomRenderingRandomly({final double ratioNotRender = 2 / 9}) {
    final List<Cell> cells = _gridCellsAsList();

    final countNegative = (ratioNotRender * cells.length).toInt();
    final countPositive = cells.length - countNegative;

    final random = [
      ...List.generate(countNegative, (_) => false),
      ...List.generate(countPositive, (_) => true)
    ];

    assert(random.length == cells.length);

    for (int i = 0; i < cells.length; i++) {
      random.shuffle();
      cells[i].doRenderRoom = random.first;
    }
  }

  /// Connects live cells.
  ///
  /// Or live cells connected by dead cells.
  void _connectNeighboursByPaths() {
    // Checks bilater connections
    final List<(Cell, Cell)> roomsAlreadyConnected = [];
    final cells = _gridCellsAsList();

    for (final cellStart in cells) {
      for (final cellEnd in cellStart.connectedNeighbours) {
        if (roomsAlreadyConnected.contains((cellStart, cellEnd)) ||
            roomsAlreadyConnected.contains((cellEnd, cellStart))) {
          continue;
        }

        roomsAlreadyConnected.add((cellStart, cellEnd));

        if (cellStart.isInDeadPath() == true ||
            cellEnd.isInDeadPath() == true) {
          continue;
        }

        final Path path = _getRoomDoorsRandom(cellStart, cellEnd);

        cellStart.paths.add(path);
      }
    }
  }

  /// Returns doors on matching sides of the rooms contained in the given [Cell]s.
  ///
  /// If this [Cell] is LEFT and [cellEnd] is RIGHT, the door of [Cell]
  /// will be on the right side of its room, and the door of [cellEnd]
  /// will be o the left side of its room.
  ///
  /// In case of `Cell.doRenderRoom==false` the center location
  /// of that [Cell] is assumed to be the single "door" of that [Cell].
  static Path _getRoomDoorsRandom(Cell cellStart, Cell cellEnd) {
    Point<int> startDoor;
    Point<int> endDoor;
    Point<int> startDoorConn;
    Point<int> endDoorConn;

    (Point<int>, Point<int>) getLeftOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.leftDoorAndConnector();

    (Point<int>, Point<int>) getTopOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.topDoorAndConnector();

    (Point<int>, Point<int>) getRightOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.rightDoorAndConnector();

    (Point<int>, Point<int>) getBottomOrCenter(Cell cell) =>
        cell.doRenderRoom == false
            ? cell.room.centralDoor()
            : cell.room.bottomDoorAndConnector();

    if (cellEnd == cellStart.neighbourLeft) {
      (startDoor, startDoorConn) = getLeftOrCenter(cellStart);
      (endDoor, endDoorConn) = getRightOrCenter(cellEnd);
    } else if (cellEnd == cellStart.neighbourTop) {
      (startDoor, startDoorConn) = getTopOrCenter(cellStart);
      (endDoor, endDoorConn) = getBottomOrCenter(cellEnd);
    } else if (cellEnd == cellStart.neighbourRight) {
      (startDoor, startDoorConn) = getRightOrCenter(cellStart);
      (endDoor, endDoorConn) = getLeftOrCenter(cellEnd);
    } else {
      //if (cellEnd == cellStart.neighbourBottom) {
      (startDoor, startDoorConn) = getBottomOrCenter(cellStart);
      (endDoor, endDoorConn) = getTopOrCenter(cellEnd);
    }

    return Path(startDoor, endDoor, startDoorConn, endDoorConn);
  }
}

// --------------------------------------------------------------------------------
// Cell
// --------------------------------------------------------------------------------

/// A map consists of a matrix of [Cell]s.
///
/// A [Cell] has neighbours and is connected to at least one neighbour.
class Cell extends Rectangle<int> {
  Cell? neighbourLeft;
  Cell? neighbourTop;
  Cell? neighbourRight;
  Cell? neighbourBottom;
  final Set<Cell> connectedNeighbours = {};
  late Room room;
  bool doRenderRoom = true;
  final String? cellId;

  final Set<Path> paths = {};

  Cell(int left, int top, int width, int height, [this.cellId])
      : super(left, top, width, height);

  void paintBackground(
    List<List<String>> map,
    String cellFill,
    String cellBorder,
  ) {
    for (int y = top; y < top + height; y++) {
      for (int x = left; x < left + width; x++) {
        if (y == top && connectedNeighbours.contains(neighbourTop) == false) {
          map[y][x] = cellBorder;
        } else if (y == top + height - 1 &&
            connectedNeighbours.contains(neighbourBottom) == false) {
          map[y][x] = cellBorder;
        } else if (x == left &&
            connectedNeighbours.contains(neighbourLeft) == false) {
          map[y][x] = cellBorder;
        } else if (x == left + width - 1 &&
            connectedNeighbours.contains(neighbourRight) == false) {
          map[y][x] = cellBorder;
        } else {
          map[y][x] = cellFill;
        }
      }
    }

    if (cellId != null) {
      map[top][left] = cellId!;
    }
  }

  void paintPaths(
      List<List<String>> map, String roomDoor, String roomCorridor) {
    for (final Path path in paths) {
      path.paint(map, roomDoor, roomCorridor);
    }
  }

  void paintRoom(
    List<List<String>> map,
    String roomCornerTopLeft,
    String roomCornerTopRight,
    String roomCornerBottomLeft,
    String roomCornerBottomRight,
    String roomBorderVertical,
    String roomBorderHorizontal,
    String roomFloor,
    String roomNoRender,
  ) =>
      room.paint(
        map,
        roomCornerTopLeft,
        roomCornerTopRight,
        roomCornerBottomLeft,
        roomCornerBottomRight,
        roomBorderVertical,
        roomBorderHorizontal,
        roomFloor,
        roomNoRender,
        doRenderRoom,
      );

  /// The layout is based in collapsed borders. This, a room needs to know
  /// whether it is placed in the last col or row. As this is needed for
  /// correctly collapsing the last border.
  Cell setRoom({final int minRoomInnerDimension = 3}) {
    final minRoomOuterDimension = minRoomInnerDimension + 2;

    // ----------------------------------------------------------------------------
    // Outer coords of room within a Cell.
    //
    // These room coords sit directly within the walls of the Cell.
    // ----------------------------------------------------------------------------

    final int x0 = left + 1;
    final int y0 = top + 1;
    final int x1 = (x0 + width) - 1;
    final int y1 = (y0 + height) - 1;
    final int widthAvailable = x1 - x0;
    final int heightAvailable = y1 - y0;

    if (widthAvailable < minRoomOuterDimension ||
        heightAvailable < minRoomOuterDimension) {
      throw 'The free cell space is too small $widthAvailable x $heightAvailable to accommodate a walled room of minimum $minRoomOuterDimension x $minRoomOuterDimension. Try a min map size of 20 x 20.';
    }

    // ----------------------------------------------------------------------------
    // Pick random coordinates and random width and random height.
    //
    // Test if the resulting room is valid, accept the room if it is.
    // ----------------------------------------------------------------------------

    final xRange = List.generate(widthAvailable, (index) => index);
    final yRange = List.generate(heightAvailable, (index) => index);

    late int x0Random;
    late int y0Random;
    late int x1Random;
    late int y1Random;
    late int widthRandom;
    late int heightRandom;

    do {
      xRange.shuffle();
      yRange.shuffle();

      x0Random = xRange[0];
      y0Random = yRange[0];
      x1Random = xRange[1];
      y1Random = yRange[1];
      widthRandom = x1Random - x0Random;
      heightRandom = y1Random - y0Random;
    } while (widthRandom < minRoomOuterDimension ||
        heightRandom < minRoomOuterDimension ||
        x0Random + widthRandom > widthAvailable ||
        y0Random + heightRandom > heightAvailable);

    final xStart = x0 + x0Random;
    final yStart = y0 + y0Random;

    room = Room(xStart, yStart, widthRandom, heightRandom);

    return this;
  }

  /// Splits this [Cell] into a number of horizontal [cells].
  List<Cell> cols(int cells, {CellIdHelper? cellIdHelper}) {
    int cellWidth = width ~/ cells;
    assert(cellWidth <= width);

    List<Cell> ret = [];

    for (int cellIndex = 0; cellIndex < cells; cellIndex++) {
      int x0 = left + cellIndex * cellWidth;
      int y0 = top;
      int x1 = x0 + cellWidth;
      int y1 = top + height;

      if (cellIndex == cells - 1) {
        // last col consumes the remaining width
        x1 = width;
      } else {
        // collapsing the cell borders into one
        x1++;
      }

      final cellId = cellIdHelper?.increment;

      ret.add(Cell(x0, y0, x1 - x0, y1 - y0, cellId));
    }

    return ret;
  }

  /// Splits this [Cell] into a number of vertical [cells].
  List<Cell> rows(int cells, {CellIdHelper? cellIdHelper}) {
    int cellHeight = height ~/ cells;
    assert(cellHeight <= height);

    List<Cell> ret = [];

    for (int cellIndex = 0; cellIndex < cells; cellIndex++) {
      int x0 = left;
      int y0 = top + cellIndex * cellHeight;
      int x1 = left + width;
      int y1 = y0 + cellHeight;

      if (cellIndex == cells - 1) {
        // last row consumes the remaining height
        y1 = height;
      } else {
        // collapsing the cell borders into one
        y1++;
      }

      final cellId = cellIdHelper?.increment;

      ret.add(Cell(
        x0,
        y0,
        x1 - x0,
        y1 - y0,
        cellId,
      ));
    }

    return ret;
  }

  /// If this [Cell] is `doRenderRoom == false` AND has only one path direction with
  /// a [Cell] where `doRenderRoom == true`.
  bool isInDeadPath() {
    if (doRenderRoom == true) {
      return false;
    }

    final liveDirections = _liveDirections({});
    return liveDirections == 1;
  }

  int _liveDirections(Set<Cell> visited) {
    visited.add(this);
    int directions = 0;
    final neighs = connectedNeighbours; // between 1-4

    final lives = neighs.where((cell) => cell.doRenderRoom == true).toSet();

    if (lives.isNotEmpty) {
      directions = lives.length;
      visited.addAll(lives);
    }
    final deads = neighs.where((cell) => cell.doRenderRoom == false).toSet();

    for (final dead in deads) {
      if (visited.contains(dead) == false) {
        int count = dead._liveDirections(visited);
        if (count > 0) {
          directions += 1;
        }
      }
    }

    return directions;
  }

  /// Returns ONE random neigbour.
  Cell _getNeighbourRandom() {
    final List<Cell> neigbours = _getNeighbours();
    neigbours.shuffle();
    return neigbours.first;
  }

  /// Returns ONE random neigbour.
  List<Cell> _getNeighbours() {
    final List<Cell> neigbours = [];

    if (neighbourLeft != null) {
      neigbours.add(neighbourLeft!);
    }
    if (neighbourTop != null) {
      neigbours.add(neighbourTop!);
    }
    if (neighbourRight != null) {
      neigbours.add(neighbourRight!);
    }
    if (neighbourBottom != null) {
      neigbours.add(neighbourBottom!);
    }

    return neigbours;
  }

  /// Finds all nodes within a bi-directional structure of nodes.
  Set<Cell> _getAllNodesBFS(Set<Cell> visited) {
    final nodes = {...connectedNeighbours};
    nodes.removeWhere((node) => visited.contains(node));
    visited.addAll(nodes);

    final Set<Cell> collect = {};
    for (var node in nodes) {
      collect.addAll(node._getAllNodesBFS(visited));
    }

    nodes.addAll(collect);

    return nodes;
  }
}

// --------------------------------------------------------------------------------
// ROOM inside a CELL
// --------------------------------------------------------------------------------

/// A [Room] inside a [Cell].
class Room extends Rectangle<int> {
  Room(
    int left,
    int top,
    int width,
    int height,
  ) : super(left, top, width, height);

  (Point<int> door, Point<int> connector) centralDoor() {
    final p = Point<int>(left + width ~/ 2, top + height ~/ 2);
    return (p, p);
  }

  (Point<int> door, Point<int> connector) leftDoorAndConnector() {
    final rnd = _randomHeight;
    return (Point<int>(left, rnd), Point<int>(left - 1, rnd));
  }

  (Point<int>, Point<int>) rightDoorAndConnector() {
    final rnd = _randomHeight;
    return (Point<int>(right - 1, rnd), Point<int>(right, rnd));
  }

  (Point<int>, Point<int>) topDoorAndConnector() {
    final rnd = _randomWidth;
    return (Point<int>(rnd, top), Point<int>(rnd, top - 1));
  }

  (Point<int>, Point<int>) bottomDoorAndConnector() {
    final rnd = _randomWidth;
    return (Point<int>(rnd, bottom - 1), Point<int>(rnd, bottom));
  }

  /// For a room with height = 5, this method will return `top + [2...4]`.
  int get _randomHeight =>
      top + (List.generate(height - 2, (index) => index + 1)..shuffle()).first;

  /// For a room with width = 5, this method will return `left + [2...4]`.
  int get _randomWidth =>
      left + (List.generate(width - 2, (index) => index + 1)..shuffle()).first;

  void paint(
    List<List<String>> map,
    String roomCornerTopLeft,
    String roomCornerTopRight,
    String roomCornerBottomLeft,
    String roomCornerBottomRight,
    String roomBorderVertical,
    String roomBorderHorizontal,
    String roomFloor,
    String roomNoRender,
    bool doRenderRoom,
  ) {
    if (doRenderRoom == false) {
      roomCornerTopLeft = roomNoRender;
      roomCornerBottomLeft = roomNoRender;
      roomCornerTopRight = roomNoRender;
      roomCornerBottomRight = roomNoRender;
      roomBorderHorizontal = roomNoRender;
      roomBorderVertical = roomNoRender;
      roomFloor = roomNoRender;
    }

    for (int y = top; y < top + height; y++) {
      for (int x = left; x < left + width; x++) {
        // CORNERS
        if (y == top && x == left) {
          map[y][x] = roomCornerTopLeft;
        } else if (y == bottom - 1 && x == left) {
          map[y][x] = roomCornerBottomLeft;
        } else if (y == bottom - 1 && x == right - 1) {
          map[y][x] = roomCornerBottomRight;
        } else if (y == top && x == right - 1) {
          map[y][x] = roomCornerTopRight;
        }
        // SIDES
        else if (y == top) {
          map[y][x] = roomBorderVertical;
        } else if (y == top + height - 1) {
          map[y][x] = roomBorderVertical;
        } else if (x == left) {
          map[y][x] = roomBorderHorizontal;
        } else if (x == left + width - 1) {
          map[y][x] = roomBorderHorizontal;
        }
        // FLOOR
        else {
          map[y][x] = roomFloor;
        }
      }
    }
  }
}

// --------------------------------------------------------------------------------
// PATH between two ROOMs
// --------------------------------------------------------------------------------

/// A [Path] between two [Room]s.
///
/// [startDoor] and [endDoor] are doors.
///
/// [startDoorConnection] and [endDoorConnection] are the coordinates
/// right outside [startDoor] and [endDoor].
class Path {
  Point<int> startDoor;
  Point<int> endDoor;
  Point<int> startDoorConnection;
  Point<int> endDoorConnection;
  bool alreadyPainted = false; // avoids painting paths twice

  Path(this.startDoor, this.endDoor, this.startDoorConnection,
      this.endDoorConnection);

  // Path cloneSwapNoPaintPath() {
  //   return Path(endDoor, startDoor, endDoorConnection, startDoorConnection);
  // }

  /// A [Path] paints the start-ROOM door it belongs to.
  ///
  /// A [Path] does not paint the end-ROOM door.
  ///
  /// [doPaintPath] paints the corridors between [startDoor] and [endDoor] door.
  void paint(
    List<List<String>> map,
    String roomDoor,
    String roomCorridor,
  ) {
    map[startDoor.y][startDoor.x] = roomDoor;
    map[endDoor.y][endDoor.x] = roomDoor;

    PathPainer(map).paint(roomCorridor, startDoorConnection, endDoorConnection);
  }

  @override
  String toString() {
    return 'conn1: $startDoorConnection conn2: $endDoorConnection';
  }
}

// --------------------------------------------------------------------------------
// Paints paths.
// --------------------------------------------------------------------------------

class PathPainer {
  final List<List<String>> _map;

  PathPainer(this._map);

  /// Will paint a path between [start] and [end].
  paint(
    String draw,
    Point<int> start,
    Point<int> end,
  ) {
    paintCorridor(Point<int> point) {
      _map[point.y][point.x] = draw;
    }

    paintCorridor(start);
    paintCorridor(end);

    final startX = min(start.x, end.x);
    final endX = max(start.x, end.x);

    final startY = min(start.y, end.y);
    final endY = max(start.y, end.y);

    if (startX == endX) {
      // ------------------------------------------------------------------------
      // START and END are on the same ROW (y)
      // ------------------------------------------------------------------------

      for (int y = startY; y < endY; y++) {
        paintCorridor(Point(start.x, y));
      }
    } else if (startY == endY) {
      // ------------------------------------------------------------------------
      // START and END are on the same COLUMN (x)
      // ------------------------------------------------------------------------

      for (int x = startX; x < endX; x++) {
        paintCorridor(Point(x, start.y));
      }
    } else {
      // ------------------------------------------------------------------------
      // START and END are not on the same axis
      // ------------------------------------------------------------------------

      // This generates a map with path-corners that are not
      // all at the center of the path.
      final rangeX = List.generate((endX - startX).abs(), (index) => index);
      final rangeY = List.generate((endY - startY).abs(), (index) => index);

      rangeX.shuffle();
      rangeY.shuffle();

      final int deltaXAbsolute = startX + rangeX.first;
      final int deltaYAbsolute = startY + rangeY.first;

      if (start.x < end.x) {
        for (int x = startX; x <= deltaXAbsolute; x++) {
          paintCorridor(Point(x, start.y));
        }

        for (int x = endX; x >= deltaXAbsolute; x--) {
          paintCorridor(Point(x, end.y));
        }

        for (int y = startY; y < endY; y++) {
          paintCorridor(Point(deltaXAbsolute, y));
        }
      } else {
        for (int y = startY; y <= deltaYAbsolute; y++) {
          paintCorridor(Point(start.x, y));
        }

        for (int y = endY; y >= deltaYAbsolute; y--) {
          paintCorridor(Point(end.x, y));
        }

        for (int x = startX; x < endX; x++) {
          paintCorridor(Point(x, deltaYAbsolute));
        }
      }
    }
  }
}

// --------------------------------------------------------------------------------
// Helper class to have debug Cell-IDs.
// --------------------------------------------------------------------------------

class CellIdHelper {
  int _id = 0;

  String get increment => String.fromCharCode(_id++ + 65);
}