├── 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 | --------------------------------------------------------------------------------