README.md

Scurry
======
[![GitHub CI](https://github.com/eskil/scurry/actions/workflows/elixir.yml/badge.svg)](https://github.com/eskil/scurry/actions/workflows/elixir.yml)
[![Coverage Status](https://coveralls.io/repos/github/eskil/scurry/badge.svg?branch=main)](https://coveralls.io/github/eskil/scurry?branch=main)
[![Last Updated](https://img.shields.io/github/last-commit/eskil/scurry.svg)](https://github.com/eskil/scurry/commits/master)


An
[A-star 2D polygon map search](https://en.wikipedia.org/wiki/A*_search_algorithm)
implementation and set of polygon, geometry and vector utility
functions for Elixir.

## Wx Demo

Start a 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.14.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Scurry.Wx.start()
```

![animated gif showing demo](imgs/a-star-sample.gif?raw=true "A-star demo")

* The start point is a *green crosshair*.
* The cursor position is a *red crosshair* if inside the main polution, *gray* if outside.
* Moving the mouse will show a line from start to the cursor.
  * It'll be *green* if the there's a line of sight.
  * It'll be *gray* if not, and there'll be a *small red crosshair* at
    the first blockage, and *small gray crosshair* all subsequent
    blocks.
* Holding down left mouse button will show full search graph in
  *bright orange* and a *thick green path* for the found path.
* Releasing the left mouse button resets the start to there.
  * You can place the start outside the main polygon.


## How to use

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `scurry` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:scurry, git: "https://github.com/eskil/scurry.git"},
  ]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/scurry>.

See the [quickstart](Quickstart.md).

## Internals

There's better API documentation in the code (not yet published on hexdocs)

```
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

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

The screen coordinate `0, 0` is upper left the x-axis goes
left-to-right and y-axis top-to-bottom. Polygons must be defined
clockwise and not-closed. This is important for convex/concave vertex
checks.

### Polygon map

The wxwidgets demo map is loaded from a [json file](priv/complex.json), and looks like

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

The `main` polygon is the primary walking area - as complex as it
needs to be.

Subsequent polygons (not named `main`) are holes within it.

Polygons don't need to be closed (last `[x, y]` equals the first),
this will be handled internally.

The polygon name isn't used internally, only to decide which polygon
is 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. 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 `vertice` to a list of
`{vertice, cost}`. This is computed from the polygon map using a set
of vertices. This set is composed of;

* the main polygon's *concave*  (pointing into the world)
* the holes' *convex* (point out of the hole, into the world)

and `PolygonMap.get_vertices/2` creates this.

```elixir
vertices = [
  {x1, y1}=vertice1,
  {x2, y2}=vertice2,
  {x3, y3}=vertice3,
  ...
]
```

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

Assuming a `cost_fun` that has type `vertice, vertice :: cost`, the graph looks like;

```elixir
graph = %{
  vertice1 => [
    vertice2, cost_fun(vertice1, vertice2),
    vertice3, cost_fun(vertice1, vertice3),
    vertice4, cost_fun(vertice1, vertice4),
  ],
  # When expressed as "vertice = {x, y}"
  {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.

The default `cost_fun` and `heur_fun` is the euclidean distance been
the two points.

```elixir
cost_fun = fn a, b -> Vector.distance(a, b) end
```

### A-star

In the context of A-star, we use `node` instead of `vertice` since we're
describing graphs - not strictly polygons. In the example, each node
is a polygon vertice (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` to search. The graph should be constructed as

```elixir
graph = %{
  node1 => [
    node2, cost_fun(node1, node2),
    node3, cost_fun(node1, node3),
    node4, cost_fun(node1, node4),
  ],
  # When expressed as "node = {x, y}"
  {x1, x2} => [
    {{x2, y2}, cost_fun({x1, y2}, {x2, y2})},
    {{x3, y3}, cost_fun({x1, y2}, {x3, y3})},
    ...
  ],
  ...
}
```

* `start` and `stop`, the nodes to find a path between.

* `heur_fun` function `node, node :: cost` computes the heuristic
  cost. The default is the euclidian distance.

```elixir
fn a, b -> Vector.distance(a, b) end
```

The state it maintains and returns

* `shortest_path_tree`, a map of edges, `node_a => node_b`,
  where `node_b` is the "previous" node from `node_a` that is
  the shortest path.

* `queue` priority queue / list `[node, node, ...]` sorted on
  the cost (see `f_cost` below) of the path from `start` to node to
  `stop`.

* `frontier` map of `node => node (prev)` that have been reached
  and edges yet to try and have been added to the `queue`. It's a map,
  so when we visit a node, we can add how we reached it to
  `shortest_path_tree`.

* `g_cost`, map `node => cost` with the minimal current cost from
  the `start` to `node`. Each iteration compare the current
  node's `g_cost` against the value in the map. If it's less, we've
  found a shorter path to this node and update the `g_cost` map.

* `f_cost`, map `node => cost` with the "total cost" of path from
  `start`, via node, to `stop`. This means the computed minimal
  cost from `start` to node (`g_cost`) plus the heuristic cost via
  `heur_fun`. This is used to reorder `queue`.

`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.`