0

I have a two-dimensional array that I use to represent a character's location in a game, which I will refer to as a grid. The grid is a 5x5 size grid. Not all of the tiles/locations on the grid are viable locations upon which the character may traverse. In the example image, you can see that three of the locations are water instead of land, indicating locations that the character cannot access.

enter image description here

In traversing the grid, only using the four cardinal directions (up,down,left,right) I want the character to find the shortest viable path to reach a destination. For example, if it starts at [1,2] and needs to reach location [3,2], I would like for it to move: up -> right -> right -> down.

Here is what I currently have in place for identifying viable solutions; there is something not quite right with it, but I can't figure out where my logic is going awry:

DartPad Example for interactive functional code.

import 'dart:collection';

class Point {
  int x;
  int y;
  Point({required this.y,required this.x});
}

void main() {
  print('Finding a viable solution for the shortest path...');
  
  var grid = [
    ['','','','',''],
    ['','','','',''],
    ['','','','',''], // Character is at position grid[2][1]
    ['','','','',''], // Destination is position grid[2][3]
    ['','','','',''],
  ];
  
  var characterPosition = Point(y: 2, x: 1);
  var characterDestination = Point(y: 2, x: 3);
  
  PathFinder pathFinder = PathFinder(grid, characterPosition, characterDestination);
  if (pathFinder.solutionExists) {
    print('...Solution exists!');
    for (Point point in pathFinder.solution) {
      print('(${point.y},${point.x})');
    }
  }
  else {
    print('...No solution exists.');
  }
}

class PathFinder {
  late Queue<Point> solution;
  late bool solutionExists;

  PathFinder(grid, characterPosition, characterDestination) {

    // Applying BFS on matrix cells.
    Queue<Point> queue = Queue<Point>();
    queue.add(new Point(y: characterPosition.y, x: characterPosition.x));

    List<List<bool>> visited = List.generate(
      grid.length,
      (index) => List.generate(
        grid[0].length,
        (index) => false,
        growable: false,
      ),
      growable: false,
    );

    visited[characterPosition.y][characterPosition.x] =
        true;

    while (queue.isNotEmpty) {
      Point point = queue.removeLast();

      // Destination found
      if (point.x == characterDestination.x &&
          point.y == characterDestination.y) {
        this.solutionExists = true;
        this.solution = queue;
        return;
      }

      // moving up
      if (_isValid(x: point.x, y: point.y - 1, grid: grid, visited: visited)) {
        queue.add(new Point(y: point.y - 1, x: point.x));
        visited[point.y - 1][point.x] = true;
      }

      // moving down
      if (_isValid(x: point.x, y: point.y + 1, grid: grid, visited: visited)) {
        queue.add(new Point(y: point.y + 1, x: point.x));
        visited[point.y + 1][point.x] = true;
      }

      // moving left
      if (_isValid(x: point.x - 1, y: point.y, grid: grid, visited: visited)) {
        queue.add(new Point(y: point.y, x: point.x - 1));
        visited[point.y][point.x - 1] = true;
      }

      // moving right
      if (_isValid(x: point.x + 1, y: point.y, grid: grid, visited: visited)) {
        queue.add(new Point(y: point.y, x: point.x + 1));
        visited[point.y][point.x + 1] = true;
      }
    }

    this.solutionExists = false;
    this.solution = queue;
    return;
  }

  bool _isValid({required int x, required int y, required List<List<String>> grid, required List<List<bool>> visited}) {
    if (x >= 0 &&
        y >= 0 &&
        x < grid[0].length &&
        y < grid.length &&
        grid[y][x] != '' &&
        visited[y][x] == false) {
      return true;
    }
    return false;
  }
}

If you run this (using DartPad), you can see that the output is:

Finding a viable solution for the shortest path...
...Solution exists!
(1,1)
(3,3)
(1,4)

This output isn't quite what I had expected, meaning I've clearly messed something up. I'm having trouble figuring out what exactly I should do to properly implement this path finding algorithm, so any insight, suggestions, or help would be greatly appreciated! Thank you in advance!!

Josh Kautz
  • 459
  • 3
  • 5
  • 31
  • 1
    I'd look up a path finding algorithm like Dijkstra's or A*. If you decide not to use a preexisting Dart library that does all the work for you, there are plenty of [resources](https://www.redblobgames.com/pathfinding/a-star/introduction.html) available online that will help you implement this. – EvilTak Jun 14 '21 at 16:56
  • Hint: The solution is not the queue. `this.solution = queue;`. – Sani Huttunen Jun 14 '21 at 17:06

1 Answers1

1

The logic is "Breadth First Search". List cameFrom sets each node element to the node element where it came from. Picture: enter image description here

Now reconstruct the entire path, from destination to start node.

Dart solution on DartPad.

Dart is not my first language, so feel free to change the approach...

import 'dart:collection';

// *****************************************************************************
// ***** class Point
class Point {
  int x;
  int y;
  Point({required this.y, required this.x});
}

// *****************************************************************************
// ***** class PathFinder
class PathFinder {
  List<List<String>> grid;
  int gridWidth = 0;
  int gridHeight = 0;

  late Queue<Point> solution;
  late bool solutionExists;

  // ---------------------------------------------------------------------------
  // ----- constructor
  PathFinder(this.grid, characterPosition, characterDestination) {
    // integers for easier manipulation
    Queue<int> frontier = Queue<int>();
    List<int> cameFrom = List.generate(
      this.grid[0].length * this.grid.length,
      (int index) => -1,
      growable: false,
    );

    this.gridWidth = this.grid[0].length;
    this.gridHeight = this.grid.length;

    frontier.add(this._convert2dto1d(characterPosition));

    // +++++ Breadth First Search Logic begin
    while (frontier.isNotEmpty) {
      int current = frontier.removeFirst();
      Queue<Point> neighbors = this._neighbors(_convert1dto2d(current));

      for (Point node in neighbors) {
        int node1d = this._convert2dto1d(node);

        if (cameFrom[node1d] == -1) {
          frontier.add(node1d);
          cameFrom[node1d] = current;
        }
      }
    }
    // +++++ Breadth First Search Logic end

    this.solutionExists = true;

    // +++++ copy solution to 2d Queue<Point> begin
    this.solution = Queue<Point>();
    Point endNode = characterDestination;
    this.solution.add(endNode);
    while (
        endNode.x != characterPosition.x || endNode.y != characterPosition.y) {
      int endNode1d = this._convert2dto1d(endNode);

      if (cameFrom[endNode1d] == -1) {
        this.solutionExists = false;
        break;
      }

      endNode = this._convert1dto2d(cameFrom[endNode1d]);
      this.solution.addFirst(endNode);
    }
    // +++++ copy solution to 2d Queue<Point> end

    return;
  }

  // ---------------------------------------------------------------------------
  // ----- _convert2dto1d
  // ----- Convert point to integer for easier manipulation
  int _convert2dto1d(Point node) {
    return node.y * this.gridWidth + node.x;
  }

  // ---------------------------------------------------------------------------
  // ----- _convert1dto2d
  // ----- Convert integer back to point
  Point _convert1dto2d(int node1d) {
    if (this.gridWidth == 0) {
      return new Point(y: -1, x: -1);
    }

    int y = node1d ~/ this.gridWidth;
    int x = node1d - y * this.gridWidth;

    return new Point(y: y, x: x);
  }

  // ---------------------------------------------------------------------------
  // ----- _neighbors
  Queue<Point> _neighbors(Point node) {
    Queue<Point> neighbors = Queue<Point>();

    for (int dx = -1; dx <= 1; dx += 2) {
      Point neighborNode = new Point(y: node.y, x: node.x + dx);
      if (_isValidNode(neighborNode)) {
        neighbors.add(neighborNode);
      }
    }
    for (int dy = -1; dy <= 1; dy += 2) {
      Point neighborNode = new Point(y: node.y + dy, x: node.x);
      if (_isValidNode(neighborNode)) {
        neighbors.add(neighborNode);
      }
    }

    return neighbors;
  }

  // ---------------------------------------------------------------------------
  // ----- _isValidNode
  bool _isValidNode(Point node) {
    if (node.x >= 0 &&
        node.y >= 0 &&
        node.x < this.gridWidth &&
        node.y < this.gridHeight &&
        this.grid[node.y][node.x] != '') {
      return true;
    }
    return false;
  }
}

// *****************************************************************************
// ***** main
void main() {
  print('Finding a viable solution for the shortest path...');

  var grid = [
    ['', '', '', '', ''],
    ['', '', '', '', ''],
    ['', '', '', '', ''], // Character is at position grid[2][1]
    ['', '', '', '', ''], // Destination is position grid[2][3]
    ['', '', '', '', ''],
  ];

  var characterPosition = Point(y: 2, x: 1);
  var characterDestination = Point(y: 2, x: 3);

  PathFinder pathFinder =
      PathFinder(grid, characterPosition, characterDestination);
  if (pathFinder.solutionExists) {
    print('...Solution exists!');
    for (Point point in pathFinder.solution) {
      print('(${point.y},${point.x})');
    }
  } else {
    print('...No solution exists.');
  }
}
Boris Traljić
  • 956
  • 1
  • 10
  • 16
  • This is fantastic @Boris Traljić - furthermore, this makes a lot of sense and I feel like I understand your solution on a more conceptual level. Thank you for helping me, I really appreciate it! – Josh Kautz Jun 17 '21 at 13:56