{"id":32171253,"url":"https://github.com/eskil/scurry","last_synced_at":"2026-02-21T03:31:14.485Z","repository":{"id":139624716,"uuid":"612480424","full_name":"eskil/scurry","owner":"eskil","description":"An A-star 2D polygon map search implementation and library for Elixir","archived":false,"fork":false,"pushed_at":"2025-04-01T13:46:57.000Z","size":13816,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-24T04:51:47.444Z","etag":null,"topics":["a-star","algorithms","elixir","wxwidgets"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eskil.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-11T04:13:00.000Z","updated_at":"2025-09-24T23:51:39.000Z","dependencies_parsed_at":"2025-03-31T15:47:34.698Z","dependency_job_id":"d4bc5a77-ab19-4351-83b9-4a9259eccf78","html_url":"https://github.com/eskil/scurry","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/eskil/scurry","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eskil%2Fscurry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eskil%2Fscurry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eskil%2Fscurry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eskil%2Fscurry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eskil","download_url":"https://codeload.github.com/eskil/scurry/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eskil%2Fscurry/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29672704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T03:11:15.450Z","status":"ssl_error","status_checked_at":"2026-02-21T03:10:34.920Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a-star","algorithms","elixir","wxwidgets"],"created_at":"2025-10-21T17:46:19.494Z","updated_at":"2026-02-21T03:31:14.476Z","avatar_url":"https://github.com/eskil.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"Scurry\n======\n[![GitHub CI](https://github.com/eskil/scurry/actions/workflows/elixir.yml/badge.svg)](https://github.com/eskil/scurry/actions/workflows/elixir.yml)\n[![Coverage Status](https://coveralls.io/repos/github/eskil/scurry/badge.svg?branch=main)](https://coveralls.io/github/eskil/scurry?branch=main)\n[![Last Updated](https://img.shields.io/github/last-commit/eskil/scurry.svg)](https://github.com/eskil/scurry/commits/master)\n[![Hex.pm Version](https://img.shields.io/hexpm/v/scurry.svg?style=flat-square)](https://hex.pm/packages/scurry)\n[![Documentation](https://img.shields.io/badge/hexdocs-quickstart-blue)](https://hexdocs.pm/scurry/quickstart.html)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Hex.pm Download Total](https://img.shields.io/hexpm/dt/scurry.svg?style=flat-square)](https://hex.pm/packages/scurry)\n\nAn\n[A-star 2D polygon map search](https://en.wikipedia.org/wiki/A*_search_algorithm)\nimplementation and set of polygon, geometry and vector utility\nfunctions for Elixir.\n\nPossible use cases cover game path finding or general graph searches.\n\n# Quickstart\n\n* See the [quickstart](Quickstart.md).\n* Or on [hex.pm docs](https://hexdocs.pm/scurry/quickstart.html).\n* And [full API] reference](https://hexdocs.pm/scurry/api-reference.html).\n\n\n# WxWidgets Demo\n\nStart the [WxWidgets](https://www.erlang.org/doc/apps/wx/chapter.html) demo of A-star by running `Scurry.Wx.start()`;\n\n```\n$ mix deps.get\n$ mix compile\n$ iex -S mix\nErlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] [dtrace]\n\nInteractive Elixir (1.18.1) - press Ctrl+C to exit (type h() ENTER for help)\n\nScurry (vx.y.z)\n---------------\n\nAn a-star 2d polygon map search implementation and library for elixir\n\nUsing config from config/config.exs\n\nFor WxWidget demo run Scurry.Wx.start()\n\nGo forth and find... 🐿️\n\niex(1)\u003e Scurry.Wx.start()\n```\n\n![animated gif showing wxwidgets demo](imgs/a-star-sample.gif?raw=true \"A-star demo\")\n\n* The *start point* is the **green crosshair**.\n* The *cursor position* is the **red crosshair** if inside the main polygon, **gray** if outside.\n* Moving the mouse will show a line from start to the cursor.\n  * It'll be **green** if the there's a *line of sight*, meaning no obstacles.\n  * It'll be **gray** if not, and there'll be a **small red crosshair** at\n    the first obstacle intersection, and **small gray crosshair** all subsequent\n    intersections in the line of sight.\n* Holding down left mouse button will show full search graph in\n  **bright orange** and a **thick green path** for the found path.\n* Releasing the left mouse button resets the start to there.\n  * You can place the start outside the main polygon.\n\n\n# How to use\n\nAdding `scurry` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:scurry, \"~\u003e 2.0\"},\n  ]\nend\n```\n\nUpdate your dependencies:\n\n```sh-session\n$ mix deps.get\n```\n\nCommit `mix.lock` file as desired.\n\n# Internals\n\n**Full API documentation than this see [the hexdocs](https://hexdocs.pm/scurry), especially see the [quickstart](Quickstart.md).**\n\n```\nmix docs\nopen doc/index.html\n```\n\n### Vectors\n\nIn `lib/vector.ex` you'll find the basic vector operations (dot,\ncross, length, add/sub) needed. A vector is a tuple of numbers, `{x, y}`.\n\n### Lines\n\nA line is a tuple of vectors, `{{x1, y1}, {x2, y2}}`.\n\n### Polygon\n\nIn `lib/plygon.ex` you'll find the various polygon related functions,\nsuch as line of sight, nearest point, convex etc.\n\nA polygon is a list of vertices (nodes) that are `{x, y}` tuples that\nrepresent screen coordinates.\n\nIn elixir, it looks like\n\n```elixir\npolygon = [{x, y}, {x, y}, ...]\n```\n\n* The screen coordinate `0, 0` is **upper left**\n* The x-axis goes **left-to-right** and y-axis **top-to-bottom**.\n* Polygons **must be** defined **clockwise and not-closed**. *This is important for convex/concave vertex checks.*\n* The polygon will be treated as closed with the last coordinate connected to the first.\n\n### Polygon map\n\nThe wxwidgets demo map is loaded from a [json file](priv/complex.json), and looks like\n\n```json\n{\n  \"polygons\": [\n    \"main\": [\n      [x, y], [x, y], [x,y], ...\n    ],\n    \"hole1\": [\n      [x, y], [x, y], [x,y], ...\n    ],\n    \"hole2\": [\n      [x, y], [x, y], [x,y], ...\n    ]\n  ]\n}\n```\n\nThe demo loads the polygon data, and the polygon named `main` is the\nprimary walking area - as complex as it needs to be.\n\nSubsequent polygons (not named `main`) are added as holes/obstacles\nwithin it - non-walkable areas.\n\nPolygons *must not* be be closed (meaning last `[x, y]` json point being\nequal to the first), this will be handled internally.\n\nThe polygon name isn't used internally, only by the wxwidgets demo to\npick the primary boundary and which are holes.\n\n### Graph\n\nThe A-star graph doesn't need to be a polygon map. It just needs to be\nmap from `node` to a list of `{node, cost}` edges.\n\nA `node` just has to be a term that elixir can use as a map key.\n\nFor the 2D map search, it is a map from a `vertex` (aka vectors) to a list of\n`{vertex, cost}`. This is computed from the polygon map using a set\nof vertices. This set is composed of;\n\n* the main polygon's **concave** shape  (pointing *into* the world)\n* the holes' **convex** shapes (point *out of( the hole, *into* the world)\n\nand `PolygonMap.get_vertices/2` creates this.\n\n```elixir\nvertices = [\n  {x1, y1} = vertex1,\n  {x2, y2} = vertex2,\n  {x3, y3} = vertex3,\n  ...\n]\n```\n\nThis is transformed to a graph (`PolygonMap.create_graph/4`) given the\npolygon, holes, vertices (from above) and cost function.\n\nAssuming a `cost_fun/2` that has type `vertex, vertex :: cost`, the graph looks like;\n\n```elixir\ngraph = %{\n  vertex1 =\u003e [\n    vertex2, cost_fun(vertex1, vertex2),\n    vertex3, cost_fun(vertex1, vertex3),\n    vertex4, cost_fun(vertex1, vertex4),\n  ],\n  # When expressed as \"vertex = {x, y}\" (ie. vectors)\n  {x1, x2} =\u003e [\n    {{x2, y2}, cost_fun({x1, y2}, {x2, y2})},\n    {{x3, y3}, cost_fun({x1, y2}, {x3, y3})},\n    ...\n  ],\n  ...\n}\n```\n\nNote: `create_graph` uses `PolygonMap.is_line_of_sight?/3` to\ndetermine if two vertices should have an edge. This is currently not\nconfigurable or passed as a function. Having that functionality would\nallow for configurably behaviour, such as passing through/flying over\nobstacles.\n\nThe default `cost_fun` and `heur_fun` (see later) is the euclidean\ndistance been the two points. The difference between the two is,\n`cost_fun` is used while computing the graph and `heur_fun` while\ncomputing the path. Typically they will be the same but that is\ndependent on use case.\n\n```elixir\n# Euclidean distance cost function\ncost_fun = fn a, b -\u003e Vector.distance(a, b) end\n```\n\n### A-star\n\nIn the context of A-star, we use the terminology `node` instead of\n`vertex` since we're describing graphs. In the example, each node is\na polygon vertex/vector (ie. `{x, y}`).\n\nA `node` is opaque to the algorithm, it just uses them as\nkeys for it's internal state maps and arguments to `heur_fun`.\nindexes.\n\nThe A-star algorithm main call is `Astar.search/4` and takes.\n\n* `graph` to search. The graph should be constructed as\n\n```elixir\ngraph = %{\n  node1 =\u003e [\n    node2, cost_fun(node1, node2),\n    node3, cost_fun(node1, node3),\n    node4, cost_fun(node1, node4),\n  ],\n  # When doing 2d-map, nodes are vectors, {x, y}\n  {x1, x2} =\u003e [\n    {{x2, y2}, cost_fun({x1, y2}, {x2, y2})},\n    {{x3, y3}, cost_fun({x1, y2}, {x3, y3})},\n    ...\n  ],\n  ...\n}\n```\n\n* `start` and `stop`, the nodes to find a path between. When doing 2d-map pathfinding, these are typically added to create a temporay graph to search, using `PolygonMap.extend/5`\n\n* `heur_fun` function `node, node :: cost` computes the heuristic\n  cost. The default is the euclidian distance.\n\n```elixir\n# Euclidean distance heuristic function\nfn a, b -\u003e Vector.distance(a, b) end\n```\n\nThe state it maintains and returns\n\n* `shortest_path_tree`, a map of edges, `node_a =\u003e node_b`,\n  where `node_b` is the \"previous\" node from `node_a` that is\n  the shortest path.\n\n* `queue` priority queue / list `[node, node, ...]` sorted on\n  the cost (see `f_cost` below) of the path from `start` to node to\n  `stop`.\n\n* `frontier` map of `node =\u003e node (prev)` that have been reached\n  and edges yet to try and have been added to the `queue`. It's a map,\n  so when we visit a node, we can add how we reached it to\n  `shortest_path_tree`.\n\n* `g_cost`, map `node =\u003e cost` with the minimal current cost from\n  the `start` to `node`. Each iteration compare the current\n  node's `g_cost` against the value in the map. If it's less, we've\n  found a shorter path to this node and update the `g_cost` map.\n\n* `f_cost`, map `node =\u003e cost` with the \"total cost\" of path from\n  `start`, via node, to `stop`. This means the computed minimal\n  cost from `start` to node (`g_cost`) plus the heuristic cost via\n  `heur_fun`. This is used to reorder `queue`.\n\n`shortest_path_tree` is the most relevant field, and can be converted\nto a path using `Astar.path/1`.\n\nWithin `astar.ex`, there's two steps; search \u0026 getting the\npath. `search` returns the full state, and `path` could be\nextended to return the cost along the path if needed. It can fetch\nthis from `g_cost.`\n\n# Maintainers corner\n\n## New release\n\nTo make a new release.\n\nPreconditions\n* build test etc\n* Logged into hex via `mix hex...`, check using `mix hex.user whoami`\n\nSteps\n1. Increase version in `mix.exs`\n1. `mix hex.publish`\n\n## Todo\n\nSpeed up graph extension by caching cachable `cost_fun` results in a state.\n\nThat might mean making a genserver version that has the state supports\nextend/search using calls/casts.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feskil%2Fscurry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feskil%2Fscurry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feskil%2Fscurry/lists"}