Scurry

GitHub CICoverage StatusLast UpdatedHex.pm VersionDocumentationLicense: MITHex.pm Download Total

An A-star 2D polygon map search implementation and set of polygon, geometry and vector utility functions for Elixir.

Possible use cases cover game path finding or general graph searches.

Quickstart

WxWidgets Demo

Start the WxWidgets demo of A-star by running Scurry.Wx.start();

$ mix deps.get
$ mix compile
$ iex -S mix
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] [dtrace]

Interactive Elixir (1.18.1) - press Ctrl+C to exit (type h() ENTER for help)

Scurry (vx.y.z)
---------------

An a-star 2d polygon map search implementation and library for elixir

Using config from config/config.exs

For WxWidget demo run Scurry.Wx.start()

Go forth and find... 🐿️

iex(1)> Scurry.Wx.start()

animated gif showing wxwidgets demo

How to use

Adding scurry to your list of dependencies in mix.exs:

def deps do
  [
    {:scurry, "~> 2.0"},
  ]
end

Update your dependencies:

$ mix deps.get

Commit mix.lock file as desired.

Internals

Full API documentation than this see the hexdocs, especially see the quickstart.

mix docs
open doc/index.html

Vectors

In lib/vector.ex you'll find the basic vector operations (dot, cross, length, add/sub) needed. A vector is a tuple of numbers, {x, y}.

Lines

A line is a tuple of vectors, {{x1, y1}, {x2, y2}}.

Polygon

In lib/plygon.ex you'll find the various polygon related functions, such as line of sight, nearest point, convex etc.

A polygon is a list of vertices (nodes) that are {x, y} tuples that represent screen coordinates.

In elixir, it looks like

polygon = [{x, y}, {x, y}, ...]

Polygon map

The wxwidgets demo map is loaded from a json file, and looks like

{
  "polygons": [
    "main": [
      [x, y], [x, y], [x,y], ...
    ],
    "hole1": [
      [x, y], [x, y], [x,y], ...
    ],
    "hole2": [
      [x, y], [x, y], [x,y], ...
    ]
  ]
}

The demo loads the polygon data, and the polygon named main is the primary walking area - as complex as it needs to be.

Subsequent polygons (not named main) are added as holes/obstacles within it - non-walkable areas.

Polygons must not be be closed (meaning last [x, y] json point being equal to the first), this will be handled internally.

The polygon name isn't used internally, only by the wxwidgets demo to pick the primary boundary and which are holes.

Graph

The A-star graph doesn't need to be a polygon map. It just needs to be map from node to a list of {node, cost} edges.

A node just has to be a term that elixir can use as a map key.

For the 2D map search, it is a map from a vertex (aka vectors) to a list of {vertex, cost}. This is computed from the polygon map using a set of vertices. This set is composed of;

and PolygonMap.get_vertices/2 creates this.

vertices = [
  {x1, y1} = vertex1,
  {x2, y2} = vertex2,
  {x3, y3} = vertex3,
  ...
]

This is transformed to a graph (PolygonMap.create_graph/4) given the polygon, holes, vertices (from above) and cost function.

Assuming a cost_fun/2 that has type vertex, vertex :: cost, the graph looks like;

graph = %{
  vertex1 => [
    vertex2, cost_fun(vertex1, vertex2),
    vertex3, cost_fun(vertex1, vertex3),
    vertex4, cost_fun(vertex1, vertex4),
  ],
  # When expressed as "vertex = {x, y}" (ie. vectors)
  {x1, x2} => [
    {{x2, y2}, cost_fun({x1, y2}, {x2, y2})},
    {{x3, y3}, cost_fun({x1, y2}, {x3, y3})},
    ...
  ],
  ...
}

Note: create_graph uses PolygonMap.is_line_of_sight?/3 to determine if two vertices should have an edge. This is currently not configurable or passed as a function. Having that functionality would allow for configurably behaviour, such as passing through/flying over obstacles.

The default cost_fun and heur_fun (see later) is the euclidean distance been the two points. The difference between the two is, cost_fun is used while computing the graph and heur_fun while computing the path. Typically they will be the same but that is dependent on use case.

# Euclidean distance cost function
cost_fun = fn a, b -> Vector.distance(a, b) end

A-star

In the context of A-star, we use the terminology node instead of vertex since we're describing graphs. In the example, each node is a polygon vertex/vector (ie. {x, y}).

A node is opaque to the algorithm, it just uses them as keys for it's internal state maps and arguments to heur_fun. indexes.

The A-star algorithm main call is Astar.search/4 and takes.

graph = %{
  node1 => [
    node2, cost_fun(node1, node2),
    node3, cost_fun(node1, node3),
    node4, cost_fun(node1, node4),
  ],
  # When doing 2d-map, nodes are vectors, {x, y}
  {x1, x2} => [
    {{x2, y2}, cost_fun({x1, y2}, {x2, y2})},
    {{x3, y3}, cost_fun({x1, y2}, {x3, y3})},
    ...
  ],
  ...
}
# Euclidean distance heuristic function
fn a, b -> Vector.distance(a, b) end

The state it maintains and returns

shortest_path_tree is the most relevant field, and can be converted to a path using Astar.path/1.

Within astar.ex, there's two steps; search & getting the path. search returns the full state, and path could be extended to return the cost along the path if needed. It can fetch this from g_cost.

Maintainers corner

New release

To make a new release.

Preconditions

Steps

  1. Increase version in mix.exs
  2. mix hex.publish

Todo

Speed up graph extension by caching cachable cost_fun results in a state.

That might mean making a genserver version that has the state supports extend/search using calls/casts.