Camera View, Rotate, Trim and Pad a Map in Dart

From RogueBasin
Jump to navigation Jump to search

Dart Implementation of Map Transformations

The code below is versatile in that it is map engine agnostic.

It will trim, pad, copy and rotate about any list-of-lists.

Usage

Camera View

List<List<String>> map = [ ... ];

int x = player.x;
int y = player.y;

int cameraWidth = 40;
int cameraheight = 20;

List<List<String>> camera = 
    camera<String>(map, x, y, cameraWidth, cameraHeight);

Rotate Right

final map = '''
##########
###....###
###....###
###....###
###....###
###....###
######.###
######▒▒▒#''';

List<List<String>> mapList =
    map.trim().split('\n').map((row) => row.split('')).toList();

List<List<String>> result = Transform.rotateRight<String>(mapList);

String mapString = result.map((e) => e.join('')).join('\n');

print(mapString);

// ########
// ########
// ########
// ##.....#
// ##.....#
// ##.....#
// ▒......#
// ▒#######
// ▒#######
// ########

Pad

final map = '''
##########
###....###
###....###
###....###
###....###
###....###
######.###
######▒▒▒#''';

List<List<String>> mapList =
    map.trim().split('\n').map((row) => row.split('')).toList();

String getWall() => '#';

List<List<String>> result = Transform.pad<String>(mapList, 2, getWall);

String mapString = result.map((e) => e.join('')).join('\n');

print(mapString);

// ##############
// ##############
// ##############
// #####....#####
// #####....#####
// #####....#####
// #####....#####
// #####....#####
// ########.#####
// ########▒▒▒###
// ##############
// ##############

Trim

final map = '''
##############
##############
##############
#####....#####
#####....#####
#####....#####
#####....#####
#####....#####
########.#####
########▒▒▒###
##############
##############''';

List<List<String>> mapList =
    map.trim().split('\n').map((row) => row.split('')).toList();

bool isOuterWall(dynamic str) => str == '#';

List<List<String>> result = Transform.trim<String>(mapListPadded, isOuterWall);

String mapString = result.map((e) => e.join('')).join('\n');

print(mapString);

// ########
// #....###
// #....###
// #....###
// #....###
// #....###
// ####.###
// ####▒▒▒#
// ########

Copy

final map = '''
##########
###....###
###....###
###....###
###....###
###....###
######.###
######▒▒▒#''';

List<List<String>> mapList =
    map.trim().split('\n').map((row) => row.split('')).toList();

List<List<String>> result = Transform.copy<String>(mapList, 0, 0, 5, 5);

String mapString = result.map((e) => e.join('')).join('\n');

print(mapString);

// ######
// ###...
// ###...
// ###...
// ###...
// ###...

Dart Implementation

import 'dart:math';

/// Allows for rotating, padding and trimming a list-of-lists.
import 'dart:math';

/// Allows for rotating, padding, trimming (and more) a list-of-lists (or map).
class Transform {
  // -----------------------------------------------------------------------
  // ROTATE
  // -----------------------------------------------------------------------

  /// Rotates to the right.
  static List<List<T>> rotateRight<T>(List<List<T>> target) {
    List<List<T>> rotatedList = [];
    final width = target.first.length;
    final height = target.length - 1;

    for (int column = 0; column < width; column++) {
      final List<T> lineNew = [];

      for (int row = height; row >= 0; row--) {
        lineNew.add(target[row][column]);
      }

      rotatedList.add(lineNew);
    }

    return rotatedList;
  }

  // -----------------------------------------------------------------------
  // PAD
  // -----------------------------------------------------------------------

  /// Pads with WALL tiles all around.
  ///
  /// The wall tiles are returned by [getWall].
  static List<List<T>> pad<T>(
      List<List<T>> mapShape, int padding, T Function() getWall) {
    List<List<T>> padded = _padTop(mapShape, padding, getWall);

    padded = rotateRight(padded);
    padded = _padTop(padded, padding, getWall);
    padded = rotateRight(padded);
    padded = _padTop(padded, padding, getWall);
    padded = rotateRight(padded);
    padded = _padTop(padded, padding, getWall);
    padded = rotateRight(padded);

    return padded;
  }

  /// Pads a WALL at the top
  static List<List<T>> _padTop<T>(
      List<List<T>> other, int padding, T Function() getWall) {
    final width = other.first.length;

    final List<List<T>> mapNew = [];
    mapNew.addAll(List<List<T>>.generate(padding, (index) {
      return List<T>.generate(width, (index) => getWall.call());
    }));
    mapNew.addAll(other);

    return mapNew;
  }

  // -----------------------------------------------------------------------
  // TRIM
  // -----------------------------------------------------------------------

  /// Trims all outer WALLS down to a single outer wall.
  static List<List<T>> trim<T>(
      List<List<T>> mapShape, bool Function(T e) isOuterWall) {
    List<List<T>> trimmed = _trimTopInternal(mapShape, isOuterWall);
    trimmed = rotateRight(trimmed);
    trimmed = _trimTopInternal(trimmed, isOuterWall);
    trimmed = rotateRight(trimmed);
    trimmed = _trimTopInternal(trimmed, isOuterWall);
    trimmed = rotateRight(trimmed);
    trimmed = _trimTopInternal(trimmed, isOuterWall);
    trimmed = rotateRight(trimmed);

    return trimmed;
  }

  /// Trims WALL tiles from the top of the other elements, leaves ony one WALL.
  static List<List<T>> _trimTopInternal<T>(
      List<List<T>> other, bool Function(T e) isOuterWall) {
    final List<List<T>> rows = [];

    int trimTop = 0;
    for (int row = 0; row < other.length; row++) {
      final List<T> line = other.elementAt(row);

      final isOnlyWallsInternal =
          line.where((T e) => isOuterWall.call(e)).length == line.length;

      if (isOnlyWallsInternal == false) {
        break;
      }
      trimTop = row;
    }

    rows.addAll(other.getRange(trimTop, other.length));
    return rows;
  }

// -----------------------------------------------------------------------
// COPY
// -----------------------------------------------------------------------

  /// Copies the given coordinates.
  ///
  /// [x1] and [y1] are inclusive.
  static List<List<T>> copy<T>(
      List<List<T>> target, int x0, int y0, int x1, int y1) {
    assert(x0 >= 0 && x1 < target.first.length);
    assert(y0 >= 0 && y1 < target.length);

    final List<List<T>> copy = [];

    for (int y = y0; y <= y1; y++) {
      final List<T> copyRow = [];
      for (int x = x0; x <= x1; x++) {
        copyRow.add(target[y][x]);
      }
      copy.add(copyRow);
    }

    return copy;
  }

  /// The method returns a camera-section of a map [target].
  ///
  /// The returned area contains [worldX]/[worldY] (for example the player).
  ///
  /// [cameraWidth] and [cameraHeight] are defining the size of the returned area.
  /// In case the map [target] is smaller than the camera, [cameraWidth] or [cameraHeight]
  /// are set to the respective dimensions of [target] internally.
  ///
  /// ### Usage
  ///
  /// ```
  /// List<List<String>> map = [ ... ];
  ///
  /// int x = player.x;
  /// int y = player.y;
  ///
  /// int cameraWidth = 40;
  /// int cameraheight = 20;
  ///
  /// List<List<T>> camera = camera(map, x, y, cameraWidth, cameraHeight);
  /// ```
  static List<List<T>> camera<T>(List<List<T>> target, int worldX, int worldY,
      int cameraWidth, int cameraHeight) {
    final targetWidth = target.first.length;
    final targetHeight = target.length;

    final camTopBorder = 0;
    final camLeftBorder = 0;
    final camRightBorder = targetWidth - 1;
    final camBottomBorder = targetHeight - 1;

    // ---------------------------------------------------------------------
    // Ensure that camera is smaller than or equal to map size
    // ---------------------------------------------------------------------

    cameraWidth = min(cameraWidth, targetWidth);
    cameraHeight = min(cameraHeight, targetHeight);

    // ---------------------------------------------------------------------
    // Camera coordinates
    // ---------------------------------------------------------------------

    int cameraX0 = worldX - cameraWidth ~/ 2;
    int cameraY0 = worldY - cameraHeight ~/ 2;

    // ---------------------------------------------------------------------
    // Camera coordinates are forbidden to exceed map extends
    // ---------------------------------------------------------------------

    cameraX0 = max(cameraX0, camLeftBorder);
    cameraY0 = max(cameraY0, camTopBorder);

    // Same code as below but do dense and harder to understand.
    // cameraX0 = min(cameraX0, camRightBorder - (cameraWidth - 1));
    // cameraY0 = min(cameraY0, camBottomBorder - (cameraHeight - 1));

    if (cameraX0 > camRightBorder - cameraWidth) {
      cameraX0 = camRightBorder - (cameraWidth - 1);
    }
    int cameraX1 = cameraX0 + (cameraWidth - 1);

    if (cameraY0 > camBottomBorder - cameraHeight) {
      cameraY0 = camBottomBorder - (cameraHeight - 1);
    }
    int cameraY1 = cameraY0 + (cameraHeight - 1);

    return copy<T>(target, cameraX0, cameraY0, cameraX1, cameraY1);
  }

  // -----------------------------------------------------------------------
  // CENTER TILE
  // -----------------------------------------------------------------------

  /// Returns the center tile of shapes with uneven `width` and `height`.
  ///
  /// Undefined for other shapes.
  ///
  /// Will throw if width or height are 1.
  static (Point<int> position, T tile) getCenterTile<T>(
      int x, int y, List<List<T>> target) {
    final int targetHeight = target.length;
    final int targetWidth = target.first.length;

    if (targetHeight == 1 || targetWidth == 1) {
      throw Exception('Not implemented');
    }

    final y0 = (targetHeight / 2).ceil() - 1;
    final x0 = (targetWidth / 2).ceil() - 1;
    final tile = target[y0][x0];
    return (Point<int>(x + x0, y + y0), tile);
  }

  // -----------------------------------------------------------------------
  // FILTER
  // -----------------------------------------------------------------------

  /// Filters [target] by applying [doFilter].
  ///
  /// Returns a list of objects. Each object holding (a) a Point coordinate and
  /// (b) the filtered element at this coordinate.
  ///
  /// ```
  /// bool myFilter(M tile) => ... true or false ...;
  ///
  /// List<List<T>> target = [ ... ];
  ///
  /// final List<(Point<int>, M)> filtered =
  ///   Transform.filter(target, myFilter);
  /// ```

  static List<(Point<int> position, T tile)> filter<T>(
      List<List<T>> target, bool Function(T tile) doFilter) {
    final List<(Point<int> position, T tile)> ret = [];

    final int height = target.length;
    final int width = target.first.length;

    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        if (doFilter.call(target[y][x])) {
          ret.add((Point<int>(x, y), target[y][x]));
        }
      }
    }
    return ret;
  }
}