├── lib
├── a_star.dart
└── src
│ ├── state.dart
│ ├── algorithm.dart
│ └── node.dart
├── index.html
├── .gitignore
├── pubspec.yaml
├── .github
└── workflows
│ └── main.yml
├── CHANGELOG.md
├── example
└── example.dart
├── LICENSE
├── test
└── grid_test.dart
├── analysis_options.yaml
└── README.md
/lib/a_star.dart:
--------------------------------------------------------------------------------
1 | export "src/algorithm.dart";
2 | export "src/node.dart";
3 | export "src/state.dart";
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Temporarily removed from the repo for maintenance
2 | benchmark
3 | test/a_star_test.dart
4 |
5 | .children
6 | .project
7 | .DS_Store
8 | packages
9 | .packages
10 | !deploy/packages
11 | pubspec.lock
12 | .dart_tool
13 | /.idea/
14 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: a_star
2 | description: A comprehensive A* algorithm. Suitable for anything from physical grids to abstract search spaces.
3 | homepage: https://github.com/Levi-Lesches/dart-a-star
4 | version: 3.0.1
5 |
6 | environment:
7 | sdk: ^3.0.0
8 |
9 | dependencies:
10 | collection: ^1.15.0
11 | meta: ^1.14.0
12 |
13 | dev_dependencies:
14 | test: ^1.20.1
15 | very_good_analysis: ^6.0.0
16 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | name: 🏗️ Build & Test
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: ⚙️ Set up Dart
18 | uses: dart-lang/setup-dart@v1
19 |
20 | - name: Install dependencies
21 | run: dart pub get
22 |
23 | - name: Verify formatting
24 | run: dart format --output=none --set-exit-if-changed .
25 |
26 | - name: Analyze project source
27 | run: dart analyze .
28 |
29 | - name: Run tests
30 | run: dart test
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGES
2 |
3 | ## 3.0.1
4 |
5 | * Formatting and Pub page updates
6 | * Simplified example and updated README
7 | * Re-licensed under the BSD 3-Clause (from Apache 2.0)
8 |
9 | ## 3.0.0
10 | * **Breaking**: Moved `AStarNode.depth` to `AStarState.depth`. This allows you to customize your depth values, which is useful in cases where different actions have different costs
11 |
12 | ## 2.0.0
13 | * Updated to Dart 3.0
14 | * Added a more generic API, which allows for an infinite or non-physical grid
15 | * **Breaking**: Removed the legacy API
16 |
17 | ## 1.0.0
18 | * Updated for Dart 2.15+ null safety
19 | * dart:html web/* example still exists, but is not tested and may not work.
20 | * Move from hand-rolled set of lints to Dart team package:lint recommended ones.
21 |
22 | ## 0.4.0
23 | * Updated for compatibility with 2019 Dart (2.8+).
24 |
25 | ## 0.2.0
26 |
27 | * Fix bug with rounding and actually find the shortest path.
28 | https://github.com/sethladd/dart-a-star/pull/1
--------------------------------------------------------------------------------
/example/example.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: avoid_print
2 |
3 | import "package:a_star/a_star.dart";
4 |
5 | class CoordinatesState extends AStarState {
6 | static const goal = 100;
7 |
8 | final int x;
9 | final int y;
10 |
11 | const CoordinatesState(this.x, this.y, {super.depth = 0});
12 |
13 | @override
14 | Iterable expand() => [
15 | CoordinatesState(x, y + 1, depth: depth + 1), // down
16 | CoordinatesState(x, y - 1, depth: depth + 1), // up
17 | CoordinatesState(x + 1, y, depth: depth + 1), // right
18 | CoordinatesState(x - 1, y, depth: depth + 1), // left
19 | ];
20 |
21 | @override
22 | double heuristic() => ((goal - x).abs() + (goal - y).abs()).toDouble();
23 |
24 | @override
25 | String hash() => "($x, $y)";
26 |
27 | @override
28 | bool isGoal() => x == goal && y == goal;
29 | }
30 |
31 | void main() {
32 | const start = CoordinatesState(0, 0);
33 | final result = aStar(start);
34 | if (result == null) { print("No path"); return; }
35 |
36 | final path = result.reconstructPath();
37 | for (final step in path) {
38 | print("Walk to $step");
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Levi Lesches
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/lib/src/state.dart:
--------------------------------------------------------------------------------
1 | /// A state used in an A* algorithm.
2 | ///
3 | /// This class mainly exists to require three important functions:
4 | /// - [hash]: Compute a short and unique hash to represent this state
5 | /// - [heuristic]: Calculates the estimated distance to the goal state
6 | /// - [expand]: Gets all possible neighbor states reachable from this one
7 | abstract class AStarState> {
8 | /// How far down the tree this state is.
9 | final num depth;
10 |
11 | /// A constructor for the state.
12 | const AStarState({required this.depth});
13 |
14 | /// The heuristic (estimated cost) of this state. See https://en.wikipedia.org/wiki/Heuristic_(computer_science).
15 | double heuristic();
16 |
17 | /// Determines a unique hash to represent this state.
18 | ///
19 | /// This hash is used in [Set]s, [Map]s (as [hashCode]) and during debugging.
20 | String hash();
21 |
22 | /// Gets all possible states reachable from this state.
23 | ///
24 | /// It is okay to return cyclic paths from this function. In a scenario with reversible actions,
25 | /// it is possible to go from, for example, `State A` to `State B`, and from `State B` to
26 | /// `State A`. It is okay to return both states when needed, as the cycle will be detected
27 | /// by comparing each state's [hash] values.
28 | Iterable expand();
29 |
30 | /// Whether this state is the goal state.
31 | bool isGoal();
32 |
33 | @override
34 | String toString() => hash();
35 | }
36 |
--------------------------------------------------------------------------------
/lib/src/algorithm.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: avoid_print
2 |
3 | import "package:collection/collection.dart";
4 | import "node.dart";
5 | import "state.dart";
6 |
7 | /// Runs the A* algorithm on the given state, returning the first goal state, or null.
8 | ///
9 | /// If [verbose] is true, this will print debug info about which states were expanded during the
10 | /// search. Once the algorithm reaches [limit] states (1,000 by default), it returns null to
11 | /// indicate failure.
12 | ///
13 | /// To replay the path from the [state] to the goal state, use [AStarNode.reconstructPath].
14 | AStarNode? aStar>(
15 | T state, {
16 | bool verbose = false,
17 | int limit = 1000,
18 | }) {
19 | final startNode = AStarNode(state);
20 | final opened = >{startNode};
21 | final closed = >{};
22 | final open = PriorityQueue>()..add(startNode);
23 | var count = 0;
24 |
25 | while (open.isNotEmpty) {
26 | final node = open.removeFirst();
27 | if (verbose) {
28 | print(
29 | "[$count] Exploring: ${node.hash} (${node.depth} + ${node.heuristic} = ${node.cost})",
30 | );
31 | }
32 | if (node.state.isGoal()) return node;
33 | opened.remove(node);
34 | closed.add(node);
35 | if (count++ >= limit) {
36 | if (verbose) print("ABORT: Hit A* limit of $limit nodes");
37 | return null;
38 | }
39 | for (final newNode in node.expand()) {
40 | if (closed.contains(newNode) || opened.contains(newNode)) continue;
41 | if (verbose) {
42 | print("[$count] Got: ${newNode.hash} (cost = ${newNode.heuristic})");
43 | }
44 | open.add(newNode);
45 | opened.add(newNode);
46 | }
47 | }
48 | return null;
49 | }
50 |
--------------------------------------------------------------------------------
/test/grid_test.dart:
--------------------------------------------------------------------------------
1 | import "dart:math";
2 | import "package:test/test.dart";
3 | import "package:a_star/a_star.dart";
4 |
5 | class CoordinatesState extends AStarState {
6 | final int x;
7 | final int y;
8 | final int goalX;
9 | final int goalY;
10 | final String? direction;
11 | CoordinatesState(
12 | this.x,
13 | this.y,
14 | this.goalX,
15 | this.goalY, {
16 | this.direction,
17 | super.depth = 0,
18 | });
19 |
20 | Iterable getNeighbors() => [
21 | CoordinatesState(x, y + 1, goalX, goalY, direction: "up"),
22 | CoordinatesState(x, y - 1, goalX, goalY, direction: "down"),
23 | CoordinatesState(x + 1, y, goalX, goalY, direction: "right"),
24 | CoordinatesState(x - 1, y, goalX, goalY, direction: "left"),
25 | ];
26 |
27 | bool get isValid => true;
28 |
29 | @override
30 | Iterable expand() => [
31 | for (final neighbor in getNeighbors())
32 | if (neighbor.isValid) neighbor,
33 | ];
34 |
35 | @override
36 | double heuristic() => sqrt(pow(goalX - x, 2) + pow(goalY - y, 2));
37 |
38 | @override
39 | bool isGoal() => x == goalX && y == goalY;
40 |
41 | @override
42 | String hash() => "($x, $y)";
43 | }
44 |
45 | void main() => test("Simple path should be exactly 21 spaces", () {
46 | final start = CoordinatesState(0, 0, 10, 10);
47 | final result = aStar(start);
48 | expect(result, isNotNull);
49 | if (result == null) return;
50 | final path = result.reconstructPath();
51 | expect(path, isNotEmpty);
52 | expect(path, hasLength(21));
53 | final origin = path.first;
54 | final destination = path.last;
55 | expect(origin.x, start.x);
56 | expect(origin.y, start.y);
57 | expect(destination.x, start.goalX);
58 | expect(destination.y, start.goalY);
59 | });
60 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints. See the following for docs:
3 | # https://dart.dev/guides/language/analysis-options
4 | #
5 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
6 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
7 | # invoked from the command line by running `flutter analyze`.
8 | include: package:very_good_analysis/analysis_options.yaml # has more lints
9 |
10 | analyzer:
11 | language:
12 | # Strict casts isn't helpful with null safety. It only notifies you on `dynamic`,
13 | # which happens all the time in JSON.
14 | #
15 | # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md
16 | strict-casts: false
17 |
18 | # Don't let any types be inferred as `dynamic`.
19 | #
20 | # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md
21 | strict-inference: true
22 |
23 | # Don't let Dart infer the wrong type on the left side of an assignment.
24 | #
25 | # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md
26 | strict-raw-types: true
27 |
28 | exclude:
29 | - benchmark/*.dart
30 |
31 | linter:
32 | rules:
33 | # Rules NOT in package:very_good_analysis
34 | prefer_double_quotes: true
35 | prefer_expression_function_bodies: true
36 |
37 | # Rules to be disabled from package:very_good_analysis
38 | prefer_single_quotes: false # prefer_double_quotes
39 | lines_longer_than_80_chars: false # lines should be at most 100 chars
40 | sort_pub_dependencies: false # Sort dependencies by function
41 | use_key_in_widget_constructors: false # not in Flutter apps
42 | directives_ordering: false # sort dart, then flutter, then package imports
43 | always_use_package_imports: false # not when importing sibling files
44 | sort_constructors_first: false # final properties, then constructor
45 | avoid_dynamic_calls: false # this lint takes over errors in the IDE
46 | one_member_abstracts: false # abstract classes are good for interfaces
47 | cascade_invocations: false # this often looks uglier
48 |
49 | # Temporarily disabled until we are ready to document
50 | # public_member_api_docs: false
51 |
--------------------------------------------------------------------------------
/lib/src/node.dart:
--------------------------------------------------------------------------------
1 | import "dart:collection";
2 | import "package:meta/meta.dart";
3 |
4 | import "state.dart";
5 |
6 | /// A node in an A* search.
7 | ///
8 | /// Each node is a wrapper around some corresponding [AStarState], [T]. This node caches some
9 | /// calculations about its [state], like [hash] and [heuristic], while also storing tree data,
10 | /// like [depth] and [parent].
11 | ///
12 | /// Nodes can be used in [Set]s and [Map]s as they override [hashCode] and [==] in terms of [hash].
13 | /// Nodes can also be compared by [cost] and implement [Comparable] to do so.
14 | ///
15 | /// When given a node as a result of running A*, use [reconstructPath] to get the path of states
16 | /// that led to this node. This is especially useful in puzzles or games where the path to a
17 | /// solution is significant, but can be ignored in cases where only the solution itself is needed.
18 | @immutable
19 | class AStarNode> implements Comparable> {
20 | /// The cached result of calling [AStarState.hash] on [state].
21 | final String hash;
22 |
23 | /// The cached result of calling [AStarState.heuristic] on [state].
24 | final double heuristic;
25 |
26 | /// The depth of this node in the A* tree. The root has depth 0.
27 | num get depth => state.depth;
28 |
29 | /// The underlying [AStarState] this node represents.
30 | final T state;
31 |
32 | /// This node's parent. If this node is the root, [parent] will be null.
33 | final AStarNode? parent;
34 |
35 | /// Creates an A* node based on an [AStarState].
36 | AStarNode(this.state, {this.parent})
37 | : hash = state.hash(),
38 | heuristic = state.heuristic();
39 |
40 | /// The total cost of this node.
41 | ///
42 | /// In f(x) = g(x) + h(x):
43 | /// - f(x) is the [cost]
44 | /// - g(x) is the [depth]
45 | /// - h(x) is the [heuristic].
46 | ///
47 | /// Working on cached values allows this cost to be compared cheaply.
48 | double get cost => depth + heuristic;
49 |
50 | /// Returns the list of [AStarState]s that led to this node during A* search.
51 | Iterable reconstructPath() {
52 | final path = Queue();
53 | path.addFirst(this.state);
54 | var current = parent;
55 | while (current != null) {
56 | path.addFirst(current.state);
57 | current = current.parent;
58 | }
59 | return path;
60 | }
61 |
62 | /// Expands this node into all its child nodes by calling [AStarState.expand].
63 | Iterable> expand() sync* {
64 | for (final newState in state.expand()) {
65 | yield AStarNode(newState, parent: this);
66 | }
67 | }
68 |
69 | @override
70 | int get hashCode => hash.hashCode;
71 |
72 | @override
73 | bool operator ==(Object other) => other is AStarNode && hash == other.hash;
74 |
75 | @override
76 | int compareTo(AStarNode other) => cost.compareTo(other.cost);
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A* path finding with Dart
2 |
3 | A simple but generally applicable [A* algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) implemented in Dart.
4 |
5 | A* is an efficient family of algorithms to find the shortest path from one to
6 | another. This package implements the simplest version, but [other variants](https://en.wikipedia.org/wiki/A*_search_algorithm#Variants)
7 | are available, like [IDA*](https://en.wikipedia.org/wiki/Iterative_deepening_A*).
8 |
9 | While A* typically represents paths on a physical grid, it can also be used when problems are
10 | represented in an abstract space. Puzzles are a good example, as each configuration represents
11 | one state, and the transitions between states are all the legal moves. See for example
12 | [the 15/8-puzzle](https://www.geeksforgeeks.org/8-puzzle-problem-using-branch-and-bound).
13 |
14 | To use, override the `AStarState` class with a state that describes your problem, and override the following members:
15 |
16 | - `double heuristic()`, which estimates how "close" the state is to the goal
17 | - `String hash()`, which generates a unique hash for the state
18 | - `Iterable expand()`, which generates all neighbor states
19 | - `bool isGoal()`, which determines if the state is the end state
20 |
21 | Here is an example of a simple state that represents going from `(0, 0)` to `(100, 100)` on a grid.
22 |
23 | ```dart
24 | class CoordinatesState extends AStarState {
25 | static const goal = 100;
26 |
27 | final int x;
28 | final int y;
29 |
30 | const CoordinatesState(this.x, this.y, {super.depth = 0});
31 |
32 | @override
33 | Iterable expand() => [
34 | CoordinatesState(x, y + 1, depth: depth + 1), // down
35 | CoordinatesState(x, y - 1, depth: depth + 1), // up
36 | CoordinatesState(x + 1, y, depth: depth + 1), // right
37 | CoordinatesState(x - 1, y, depth: depth + 1), // left
38 | ];
39 |
40 | @override
41 | double heuristic() => ((goal - x).abs() + (goal - y).abs()).toDouble();
42 |
43 | @override
44 | String hash() => "($x, $y)";
45 |
46 | @override
47 | bool isGoal() => x == goal && y == goal;
48 | }
49 | ```
50 |
51 | To get your result, pass a starting state to `aStar()`. You can use
52 | `reconstructPath()` on the result to walk back through the search tree and get
53 | the whole path, which is especially helpful for puzzles:
54 |
55 | ```dart
56 | void main() {
57 | const start = CoordinatesState(0, 0);
58 | final result = aStar(start);
59 | if (result == null) { print("No path"); return; }
60 |
61 | final path = result.reconstructPath();
62 | for (final step in path) {
63 | print("Walk to $step");
64 | }
65 | }
66 | ```
67 |
68 | This package was originally developed by [Seth Ladd](https://github.com/sethladd) and [Eric Seidel](https://github.com/eseidel). See an (old) running example from them [here](https://levi-lesches.github.io/dart-a-star).
69 |
70 | # Contributors
71 |
72 | * https://github.com/sethladd
73 | * https://github.com/PedersenThomas
74 | * https://github.com/filiph
75 | * https://github.com/eseidel
76 |
--------------------------------------------------------------------------------