Packages

A generic searching/pathfinding algorithm library for the Gleam programming language

Current section

Files

Jump to
gwg_pathfinding src gwg pathfinding.gleam
Raw

src/gwg/pathfinding.gleam

import gleam/bool
import gleam/dict.{type Dict}
import gleam/float
import gleam/list
import gleam/order.{Eq}
import gleam/pair
import gleam/result
import gleam/set.{type Set}
/// A function that takes the current state and its G score and returns a list
/// of possible [`Action`s](#Action).
///
pub type Successor(action, state) =
fn(state, Float) -> List(Action(action, state))
/// A transition to a `state` by an `action` with a `weight`.
///
/// The weight could mean a cost, the distance or terrain difficulty, ...
///
pub type Action(action, state) {
Action(action: action, state: state, weight: Float)
}
type Astarstate(state, action) {
Astarstate(from_state: state, action: action, g_score: Float, f_score: Float)
}
type Flowfieldstate(action) {
Flowfieldstate(action: action, weight: Float, g_score: Float)
}
/// Performs an A* search to find the optimal path from an initial `state`.
///
/// This function returns a the sequence of actions to reach the goal when the
/// `heuristic_fun` returns 0.0 (or less), signaling the goal has been
/// reached.
///
/// If the goal can not be reach, it will returns the sequence of actions to
/// the "closest" state reached.
///
/// ## Examples
///
/// ```gleam
/// let actions = [
/// Vec2(-1, 0),
/// Vec2(1, 0),
/// Vec2(0, -1),
/// Vec2(0, 1),
/// Vec2(-1, -1),
/// Vec2(-1, 1),
/// Vec2(1, -1),
/// Vec2(1, 1),
/// ]
///
/// let world0 =
/// vec2i_dict.from_string(
/// ""
/// <> "###########\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "# ## ####\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "## ##### ##\n"
/// <> "# # # #\n"
/// <> "# #\n"
/// <> "###########\n",
/// )
///
/// let world1 =
/// vec2i_dict.from_string(
/// ""
/// <> "###########\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "# ##x####\n"
/// <> "# # #\n"
/// <> "# # #\n"
/// <> "## ##### ##\n"
/// <> "# # # #\n"
/// <> "# #\n"
/// <> "###########\n",
/// )
///
/// let init = Vec2(2, 2)
/// let target = Vec2(7, 2)
///
/// fn successor(
/// state: Vec2i,
/// g_score: Float,
/// world: Dict(Vec2i, String),
/// ) -> List(Action(Vec2i, Vec2i)) {
/// actions
/// |> list.filter_map(fn(action) {
/// let weight = action |> vec2i.length
/// use <- bool.guard(g_score +. weight >. 32.0, Error(Nil))
///
/// let state = state |> vec2i.add(action)
/// use <- bool.guard(world |> dict.has_key(state), Error(Nil))
/// Ok(Action(action:, state:, weight:))
/// })
/// }
///
/// fn heuristic(state: Vec2i) -> Float {
/// vec2i.distance(state, target)
/// }
///
/// astar(
/// init:,
/// successor: fn(state, g_score) {
/// successor(state, g_score, world0)
/// },
/// heuristic:,
/// )
/// // Ok:
/// // ###########
/// // # # #
/// // # @ # * #
/// // # ↓ # ↗ #
/// // # ↓ ##↑####
/// // # ↓ # ↖ #
/// // # ↓ # ↖ #
/// // ##↘#####↑##
/// // # ↘# #↗ #
/// // # →→↗ #
/// // ###########
///
/// astar(
/// init:,
/// successor: fn(state, g_score) {
/// successor(state, g_score, world1)
/// },
/// heuristic:,
/// )
/// // Error:
/// // ###########
/// // # # #
/// // # @ # * #
/// // # ↓ # #
/// // # ↓ ##x####
/// // # ↓ # #
/// // # ↓ # ↖ #
/// // ##↘#####↑##
/// // # ↘# #↗ #
/// // # →→↗ #
/// // ###########
/// ```
///
pub fn astar(
init state: state,
successor successor_fun: Successor(action, state),
heuristic heuristic_fun: fn(state) -> Float,
) -> Result(List(action), List(action)) {
do_astar(
state,
successor_fun,
heuristic_fun,
set.from_list([state]),
dict.new(),
)
}
fn do_astar(
init_state: state,
successor_fun: Successor(action, state),
heuristic_fun: fn(state) -> Float,
queue: Set(state),
flowfield: Dict(state, Astarstate(state, action)),
) -> Result(List(action), List(action)) {
case lowest_f_score_state(queue, flowfield) {
Ok(state) -> {
let #(g_score, f_score) = case flowfield |> dict.get(state) {
Ok(Astarstate(g_score:, f_score:, ..)) -> #(g_score, f_score)
Error(Nil) -> #(0.0, 1.0)
}
use <- bool.lazy_guard(f_score -. g_score <=. 0.0, fn() {
Ok(state |> reconstruct_actions(flowfield |> dict.delete(init_state)))
})
let queue = queue |> set.delete(state)
let actions =
successor_fun(state, g_score)
|> astar_actions_folder(state, g_score, heuristic_fun, flowfield)
let queue = actions |> dict.keys |> set.from_list |> set.union(queue)
let flowfield = flowfield |> dict.merge(actions)
do_astar(init_state, successor_fun, heuristic_fun, queue, flowfield)
}
Error(Nil) -> {
closest_state(flowfield)
|> result.unwrap(init_state)
|> reconstruct_actions(flowfield |> dict.delete(init_state))
|> Error
}
}
}
fn lowest_f_score_state(
queue: Set(state),
flowfield: Dict(state, Astarstate(state, action)),
) -> Result(state, Nil) {
queue
|> set.to_list
|> list.max(fn(a, b) {
case dict.get(flowfield, a), dict.get(flowfield, b) {
Ok(a), Ok(b) -> float.compare(b.f_score, a.f_score)
Ok(a), Error(Nil) -> float.compare(0.0, a.f_score)
Error(Nil), Ok(b) -> float.compare(b.f_score, 0.0)
Error(Nil), Error(Nil) -> Eq
}
})
}
fn astar_actions_folder(
actions: List(Action(action, state)),
from_state: state,
from_g_score: Float,
heuristic_fun: fn(state) -> Float,
flowfield: Dict(state, Astarstate(state, action)),
) -> Dict(state, Astarstate(state, action)) {
actions
|> list.fold(dict.new(), fn(acc, action) {
let g_score = from_g_score +. action.weight
case flowfield |> dict.merge(acc) |> dict.get(action.state) {
Ok(old) if old.g_score <=. g_score -> acc
_ -> {
let f_score = g_score +. heuristic_fun(action.state)
Astarstate(from_state:, action: action.action, g_score:, f_score:)
|> dict.insert(acc, action.state, _)
}
}
})
}
fn closest_state(
flowfield: Dict(state, Astarstate(state, action)),
) -> Result(state, Nil) {
flowfield
|> dict.to_list
|> list.max(fn(a, b) {
let #(_, a) = a
let #(_, b) = b
float.compare(b.f_score -. b.g_score, a.f_score -. a.g_score)
})
|> result.map(pair.first)
}
fn reconstruct_actions(
state: state,
flowfield: Dict(state, Astarstate(state, action)),
) -> List(action) {
do_reconstruct_actions(state, flowfield, [])
}
fn do_reconstruct_actions(
state: state,
flowfield: Dict(state, Astarstate(state, action)),
actions: List(action),
) -> List(action) {
case flowfield |> dict.get(state) {
Ok(Astarstate(from_state:, action:, ..)) ->
do_reconstruct_actions(from_state, flowfield, [action, ..actions])
Error(Nil) -> actions
}
}
/// Generates a flowfield (also known as vector field or Dijkstra map).
///
/// ## Examples
///
/// ```gleam
/// let actions = [
/// Vec2(-1, 0),
/// Vec2(1, 0),
/// Vec2(0, -1),
/// Vec2(0, 1),
/// Vec2(-1, -1),
/// Vec2(-1, 1),
/// Vec2(1, -1),
/// Vec2(1, 1),
/// ]
///
/// let world =
/// vec2i_dict.from_string(
/// ""
/// <> " # \n"
/// <> " ### ### \n"
/// <> " \n"
/// <> "### ##### #\n"
/// <> "# # # # #\n"
/// <> "# # # # # #\n"
/// <> "# # ### # #\n"
/// <> "# # # #\n"
/// <> "# ####### #\n"
/// <> "# #\n"
/// <> "###########\n",
/// )
///
/// let target = Vec2(5, 5)
///
/// fn successor(state: Vec2i, g_score: Float) -> List(Action(Vec2i, Vec2i)) {
/// actions
/// |> list.filter_map(fn(action) {
/// let weight = action |> vec2i.length
/// use <- bool.guard(g_score +. weight >. 32.0, Error(Nil))
///
/// let state = state |> vec2i.subtract(action)
/// use <- bool.guard(vec2i.distance(state, target) >. 16.0, Error(Nil))
/// use <- bool.guard(world |> dict.has_key(state), Error(Nil))
/// Ok(Action(action:, state:, weight:))
/// })
/// }
///
/// astar(init:, successor:)
/// // ↓↙→↘↓#↙←←←←
/// // ↘###↓↙←###↙
/// // →→↘↓↙←←←←←←
/// // ###↓#####↖#
/// // # #↓#↓↙←#↑#
/// // # #↓#*#↖#↑#
/// // # #↘###↑#↑#
/// // # #→→→↗↑#↑#
/// // # #######↑#
/// // #→→→→→→→↗↑#
/// // ###########
/// ```
///
pub fn flowfield(
init state: state,
successor successor_fun: Successor(action, state),
) -> Dict(state, action) {
do_flowfield(state, successor_fun, [state], dict.new())
}
fn do_flowfield(
start_state: state,
successor_fun: Successor(action, state),
queue: List(state),
flowfield: Dict(state, Flowfieldstate(action)),
) -> Dict(state, action) {
case queue {
[state, ..queue] -> {
let g_score = case flowfield |> dict.get(state) {
Ok(Flowfieldstate(g_score:, ..)) -> g_score
Error(Nil) -> 0.0
}
let actions =
successor_fun(state, g_score)
|> flowfield_actions_folder(g_score, flowfield)
let queue =
actions
|> dict.drop(flowfield |> dict.keys)
|> dict.keys
|> list.append(queue, _)
let flowfield = flowfield |> dict.merge(actions)
do_flowfield(start_state, successor_fun, queue, flowfield)
}
_ ->
flowfield
|> dict.delete(start_state)
|> dict.map_values(fn(_key, value) { value.action })
}
}
fn flowfield_actions_folder(
actions: List(Action(action, state)),
from_g_score: Float,
flowfield: Dict(state, Flowfieldstate(action)),
) -> Dict(state, Flowfieldstate(action)) {
actions
|> list.fold(dict.new(), fn(acc, action) {
let g_score = from_g_score +. action.weight
case flowfield |> dict.merge(acc) |> dict.get(action.state) {
Ok(old)
if old.g_score <. g_score
|| { old.g_score <=. g_score && old.weight <=. action.weight }
-> acc
_ ->
Flowfieldstate(action: action.action, weight: action.weight, g_score:)
|> dict.insert(acc, action.state, _)
}
})
}