Simple Rogue levels in Dart

From RogueBasin
Jump to navigation Jump to search

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);
}