Simple Rogue levels in Dart

From RogueBasin
Revision as of 09:22, 11 October 2023 by Erik (talk | contribs) (separated into classes)
Jump to navigation Jump to search

Dart Implementation of Simple Rogue Levels

The Dart code below generates simple maps.

It 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().

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

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

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

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

Dart Implementation

Constructs a Grid with a number of rows and columns.

The Grid holds a number of Cells.

A Cell holds a Room.

A Room has walls and doors. A Room holds a Path connecting it to another Room.

Grid.paint() calls Cell.paint(), which calls Room.paint(), which calls Path.paint().

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 int _minRoomInnerDimension;
  late final List<List<Cell>> _grid;

  Grid(
    this._width,
    this._height,
    int columns,
    int rows, {
    int minRoomInnerDimension = 3,
    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 = '▒',
  })  : _minRoomInnerDimension = minRoomInnerDimension,
        _cellBorder = cellBorder,
        _cellFill = cellFill,
        _roomCornerTopLeft = roomCornerTopLeft,
        _roomCornerTopRight = roomCornerTopRight,
        _roomCornerBottomLeft = roomCornerBottomLeft,
        _roomCornerBottomRight = roomCornerBottomRight,
        _roomBorderVertical = roomBorderVertical,
        _roomBorderHorizontal = roomBorderHorizontal,
        _roomFloor = roomFloor,
        _roomDoor = roomDoor,
        _roomCorridor = roomCorridor {
    //
    // Initialize the all cells and their corresponding room inside.
    //
    _grid = Cell(0, 0, _width, _height).rows(rows).map((cellRow) {
      final cols = cellRow.cols(columns);
      for (var columnCell in cols) {
        columnCell.setRoom(minRoomInnerDimension: _minRoomInnerDimension);
      }
      return cols;
    }).toList();

    _calculateNeigbours(); // calculate neighbours in the grid pattern
    _connectNeighboursRandomly(); // which neigbours are connected
    _connectNeighboursByPaths(); // calculate the paths between neigbours
  }

  List<Cell> _tableCellsAsList() => _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 _tableCellsAsList()) {
      cell.paintBackground(
        map,
        _cellFill,
        _cellBorder,
      );
    }

    for (final cell in _tableCellsAsList()) {
      cell.paint(
        map,
        _roomCornerTopLeft,
        _roomCornerTopRight,
        _roomCornerBottomLeft,
        _roomCornerBottomRight,
        _roomBorderVertical,
        _roomBorderHorizontal,
        _roomFloor,
        _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 = _tableCellsAsList();
    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);
  }

  void _connectNeighboursByPaths() {
    // Checks bilater connections
    final List<(Cell, Cell)> roomsAlreadyConnected = [];
    final cells = _tableCellsAsList();

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

        final roomStart = cellStart.room;
        final roomEnd = cellEnd.room;

        final Path path = _getRoomDoorsRandom(cellStart, cellEnd);

        roomStart.paths.add(path);
        roomEnd.paths.add(path.cloneSwapNoPaintPath());
      }
    }
  }

  /// 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.
  static Path _getRoomDoorsRandom(Cell cellStart, Cell cellEnd) {
    Point<int> startDoor;
    Point<int> endDoor;
    Point<int> startDoorConn;
    Point<int> endDoorConn;

    final Room roomStart = cellStart.room;
    final Room roomEnd = cellEnd.room;

    if (cellEnd == cellStart.neighbourLeft) {
      (startDoor, startDoorConn) = roomStart.leftDoorAndConnector();
      (endDoor, endDoorConn) = roomEnd.rightDoorAndConnector();
    } else if (cellEnd == cellStart.neighbourTop) {
      (startDoor, startDoorConn) = roomStart.topDoorAndConnector();
      (endDoor, endDoorConn) = roomEnd.bottomDoorAndConnector();
    } else if (cellEnd == cellStart.neighbourRight) {
      (startDoor, startDoorConn) = roomStart.rightDoorAndConnector();
      (endDoor, endDoorConn) = roomEnd.leftDoorAndConnector();
    } else {
      //if (cellEnd == cellStart.neighbourBottom) {
      (startDoor, startDoorConn) = roomStart.bottomDoorAndConnector();
      (endDoor, endDoorConn) = roomEnd.topDoorAndConnector();
    }
    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;

  Cell(
    int left,
    int top,
    int width,
    int height,
  ) : 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;
        }
      }
    }
  }

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

  /// 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) {
    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++;
      }

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

    return ret;
  }

  /// Splits this [Cell] into a number of vertical [cells].
  List<Cell> rows(int cells) {
    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++;
      }

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

    return ret;
  }

  /// Returns ONE random neigbour.
  Cell _getNeighbourRandom() {
    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!);
    }

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

  /// 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> {
  final Set<Path> paths = {};

  Room(
    int left,
    int top,
    int width,
    int height,
  ) : super(left, top, width, height);

  (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 roomDoor,
    String roomCorridor,
  ) {
    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;
        }
      }
    }

    for (final Path path in paths) {
      path.paint(map, roomDoor, roomCorridor);
    }
  }
}
// --------------------------------------------------------------------------------
// 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 doPaintPath;
  Path(
    this.startDoor,
    this.endDoor,
    this.startDoorConnection,
    this.endDoorConnection, {
    this.doPaintPath = true,
  });

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

  /// 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,
  ) {
    paintCorridor(Point<int> point) {
      map[point.y][point.x] = roomCorridor;
    }

    map[startDoor.y][startDoor.x] = roomDoor;

    if (doPaintPath) {
      paintCorridor(startDoorConnection);
      paintCorridor(endDoorConnection);

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

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

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

        for (int y = startY; y < endY; y++) {
          paintCorridor(Point(startDoorConnection.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, startDoorConnection.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 (startDoorConnection.x < endDoorConnection.x) {
          for (int x = startX; x <= deltaXAbsolute; x++) {
            paintCorridor(Point(x, startDoorConnection.y));
          }

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

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

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

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