semita 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ name: release
2
+
3
+ # A published GitHub release, tagged `v` and semita's version, builds every wheel and the sdist and uploads them to
4
+ # PyPI. A pull request builds and checks the same files without uploading them.
5
+ on:
6
+ pull_request:
7
+ release:
8
+ types: [published]
9
+
10
+ # Every job reads the repository at most; `publish` adds the identity PyPI trusts.
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ sdist:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v5
19
+ with:
20
+ # The version is the tag, which setuptools-scm reads from git.
21
+ fetch-depth: 0
22
+ persist-credentials: false
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v7
26
+
27
+ - name: Build
28
+ run: uv build --sdist
29
+
30
+ - uses: actions/upload-artifact@v6
31
+ with:
32
+ name: sdist
33
+ path: dist/*.tar.gz
34
+
35
+ wheels:
36
+ # Every wheel is built from the sdist, so it carries the sdist's version and needs no git history.
37
+ needs: sdist
38
+ name: wheels on ${{ matrix.os }}
39
+ runs-on: ${{ matrix.os }}
40
+ strategy:
41
+ fail-fast: false
42
+ matrix:
43
+ # Each wheel is built and tested on its own architecture: Linux x86_64 and aarch64, Windows AMD64, and macOS
44
+ # arm64 and x86_64. pyproject.toml's [tool.cibuildwheel] says what is built and how it is tested.
45
+ os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest, macos-15-intel]
46
+
47
+ steps:
48
+ - uses: actions/download-artifact@v7
49
+ with:
50
+ name: sdist
51
+ path: sdist
52
+
53
+ - name: Find the sdist
54
+ id: sdist
55
+ shell: bash
56
+ run: echo "path=$(ls sdist/semita-*.tar.gz)" >> "$GITHUB_OUTPUT"
57
+
58
+ - name: Build and test
59
+ uses: pypa/cibuildwheel@v4.2.1
60
+ with:
61
+ package-dir: ${{ steps.sdist.outputs.path }}
62
+
63
+ - uses: actions/upload-artifact@v6
64
+ with:
65
+ name: wheels-${{ matrix.os }}
66
+ path: wheelhouse/*.whl
67
+
68
+ check:
69
+ # What would be uploaded, all of it, and for a release the tag's version. Each wheel has already been audited for
70
+ # the stable ABI alone, by the audit-command in pyproject.toml.
71
+ needs: [wheels, sdist]
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/download-artifact@v7
75
+ with:
76
+ path: dist
77
+ merge-multiple: true
78
+
79
+ - name: List the files
80
+ run: ls -l dist
81
+
82
+ - name: The tag is v and a version
83
+ if: github.event_name == 'release'
84
+ run: |
85
+ tag="$GITHUB_REF_NAME"
86
+ if [[ "$tag" != v* ]]; then
87
+ echo "A release's tag is v and its version, such as v0.2.0, not $tag"
88
+ exit 1
89
+ fi
90
+ if [ ! -f "dist/semita-${tag#v}.tar.gz" ]; then
91
+ echo "setuptools-scm did not read $tag as semita's version, and built $(ls dist)"
92
+ exit 1
93
+ fi
94
+
95
+ publish:
96
+ if: github.event_name == 'release'
97
+ needs: check
98
+ runs-on: ubuntu-latest
99
+ # PyPI trusts this workflow from this environment alone, so no token is stored anywhere.
100
+ environment:
101
+ name: pypi
102
+ url: https://pypi.org/project/semita/
103
+ permissions:
104
+ id-token: write
105
+ steps:
106
+ - uses: actions/download-artifact@v7
107
+ with:
108
+ path: dist
109
+ merge-multiple: true
110
+
111
+ - name: Upload to PyPI
112
+ uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,62 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest, macos-latest]
15
+ python-version: ["3.12", "3.13"]
16
+ env:
17
+ # The C compiler's warnings fail the build here.
18
+ SEMITA_WARNINGS_AS_ERRORS: "1"
19
+
20
+ steps:
21
+ - uses: actions/checkout@v5
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v7
25
+ with:
26
+ enable-cache: true
27
+
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ - name: Install, compiling the extension
32
+ run: uv sync --python ${{ matrix.python-version }} --extra dev
33
+
34
+ - name: Lint
35
+ run: uv run ruff check .
36
+
37
+ - name: Format
38
+ run: uv run ruff format --check .
39
+
40
+ - name: Type check
41
+ run: uv run pyright
42
+
43
+ - name: Test
44
+ run: uv run pytest -v
45
+
46
+ wheel:
47
+ # One wheel on the stable ABI serves every Python from 3.12; this checks the one built here loads on the newest.
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v5
51
+
52
+ - name: Install uv
53
+ uses: astral-sh/setup-uv@v7
54
+
55
+ - name: Build
56
+ run: SEMITA_WARNINGS_AS_ERRORS=1 uv build
57
+
58
+ - name: Load the wheel on Python 3.14
59
+ run: |
60
+ wheel=$(ls dist/semita-*-cp312-abi3-*.whl)
61
+ uv run --isolated --no-project --python 3.14 --with "$wheel" python -c \
62
+ "import numpy, semita; assert semita.find_path(numpy.ones((3, 3), dtype=bool), (0, 0), (2, 2)) is not None"
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.so
4
+ *.pyd
5
+ *.egg-info/
6
+ # Written by setuptools-scm at build time.
7
+ src/semita/_version.py
8
+ build/
9
+ dist/
10
+ .venv/
11
+ .pytest_cache/
12
+ .ruff_cache/
semita-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Nusspickel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include csrc/*.c csrc/*.h
2
+ # The release's wheels are built and tested from the sdist.
3
+ graft tests
4
+ global-exclude *.py[cod]
semita-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: semita
3
+ Version: 0.1.0
4
+ Summary: Shortest paths and distances on a grid, in C, for Python
5
+ Author-email: Max Nusspickel <max.nusspickel@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/maxnus/semita
8
+ Keywords: pathfinding,jump point search,dijkstra,grid
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: C
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=2.1
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.3; extra == "dev"
21
+ Requires-Dist: scipy>=1.14; extra == "dev"
22
+ Requires-Dist: ruff>=0.8; extra == "dev"
23
+ Requires-Dist: pyright>=1.1.390; extra == "dev"
24
+ Requires-Dist: setuptools>=77; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # semita
28
+
29
+ Shortest and cheapest paths and distances on a grid, in C, for Python: jump point search for one path, A* for one
30
+ where tiles cost differently, and Dijkstra for the cheapest paths from a point to every tile. *Semita* is Latin for a
31
+ footpath.
32
+
33
+ Written for [NachOS](https://github.com/maxnus/NachOS), a StarCraft II bot library, though nothing in it is about
34
+ StarCraft.
35
+
36
+ ```python
37
+ import numpy
38
+
39
+ import semita
40
+
41
+ passable = numpy.ones((5, 5), dtype=bool)
42
+ passable[2, :4] = False # a wall, open at its far end
43
+
44
+ path = semita.find_path(passable, (0, 0), (4, 0))
45
+ assert round(path.distance, 2) == 10.83 # round the wall's end
46
+ assert path.tiles[:3].tolist() == [[0, 0], [1, 1], [1, 2]]
47
+
48
+ assert semita.find_path(passable, (0, 0), (2, 1)) is None # the end is blocked
49
+
50
+ paths = semita.cheapest_paths_from(passable, (0, 0)) # to every tile, in one pass
51
+ assert paths.distances[4, 0] == path.distance
52
+ assert paths.distances[2, 1] == float("inf")
53
+ assert paths.path_to((4, 0)).cost == path.cost
54
+
55
+ # Costs: each value is the cost of moving a unit of distance across its tile, 1 on normal ground and more where it
56
+ # is best avoided; 0 blocks, as False does.
57
+ costs = numpy.ones((7, 7))
58
+ costs[2:5, 2:5] = 10.0 # a patch to keep out of
59
+ around = semita.find_path(costs, (3, 0), (3, 6))
60
+ assert round(around.cost, 2) == round(around.distance, 2) == 7.66 # round the patch, on ground costing 1
61
+ assert semita.find_path(numpy.ones((7, 7), dtype=bool), (3, 0), (3, 6)).distance == 6.0 # straight, on bools
62
+ ```
63
+
64
+ ## The rules
65
+
66
+ - A grid is a 2-D numpy array of bools or real numbers, or anything numpy reads as one, indexed `[i, j]`. Its values
67
+ are the tiles' costs: the cost of moving one unit of distance across each, 1 for normal ground, less where movement
68
+ is faster, more where it is slower or best avoided.
69
+ - 0 and `inf` block a tile. A grid of bools is the simplest: `True` costs 1 and `False` blocks. Otherwise a cost is
70
+ positive; NaN and negatives raise `ValueError`.
71
+ - A step goes to one of the eight neighboring tiles, 1 long straight and √2 diagonally, and only between passable
72
+ tiles. A diagonal step needs both tiles beside it passable too, so no path cuts a corner. A step costs its length
73
+ times the mean of the costs of the two tiles it joins, so a path costs the same both ways.
74
+ - The start is a path to itself, costing nothing, even on a blocked tile, but no step leaves or enters a blocked one.
75
+ - A start or an end outside the grid raises `IndexError`; a negative index counts as outside.
76
+ - `find_path` returns a `Path`, its `distance`, its `cost` and its `tiles` from the start to the end, or `None` if
77
+ there is no path. Of several equally cheap paths it returns one, but which is not part of the contract.
78
+ - `cheapest_paths_from` returns the cheapest path from the start to every tile, found in one pass, as
79
+ `CheapestPaths`: their `costs` and their `distances`, two read-only arrays of the grid's shape, `inf` where no path
80
+ reaches, and `path_to(tile)`, a tile's path as a `Path`, or `None` where none reaches. Where several paths to a tile
81
+ cost the same, it holds one of them. On a grid of bools the distances are the costs.
82
+ - A grid whose passable tiles all cost the same, a grid of bools included, is searched by jump point search; any other
83
+ by A*, which visits more tiles, the more so the cheaper the cheapest tile is, since its estimate of what is left is
84
+ the straight distance times that cost.
85
+ - A grid of bools, or a C-contiguous one of float64, is read as it is; any other is converted on each call, so one
86
+ searched often is best kept as one of those.
87
+
88
+ Each search allocates its own memory and releases the GIL, so searches run in parallel on threads. A grid must not be
89
+ written to while a search reads it.
90
+
91
+ ## Building
92
+
93
+ semita is a C extension on Python's stable ABI, so one wheel serves every Python from 3.12 on a platform. Installing
94
+ from source compiles it with the platform's own compiler: MSVC on Windows, gcc or clang elsewhere.
95
+
96
+ ```bash
97
+ uv sync --extra dev
98
+ uv run pytest
99
+ ```
100
+
101
+ The tests check every search against scipy's Dijkstra over the same steps.
102
+
103
+ ## Releasing
104
+
105
+ Publish a GitHub release tagged `v` and the new version, such as `v0.2.0`. The version is the tag, which
106
+ setuptools-scm reads from git; no file holds it.
107
+
108
+ The `release` workflow then builds the sdist, builds from it a wheel for Linux x86_64 and aarch64, Windows AMD64 and
109
+ macOS arm64 and x86_64, tests and audits each for the stable ABI alone, checks that the tag is a version,
110
+ and uploads them to PyPI. Every pull request builds and checks the same files without uploading them.
111
+
112
+ PyPI trusts the workflow through trusted publishing, from the repository's `pypi` environment, so no token is stored
113
+ anywhere.
semita-0.1.0/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # semita
2
+
3
+ Shortest and cheapest paths and distances on a grid, in C, for Python: jump point search for one path, A* for one
4
+ where tiles cost differently, and Dijkstra for the cheapest paths from a point to every tile. *Semita* is Latin for a
5
+ footpath.
6
+
7
+ Written for [NachOS](https://github.com/maxnus/NachOS), a StarCraft II bot library, though nothing in it is about
8
+ StarCraft.
9
+
10
+ ```python
11
+ import numpy
12
+
13
+ import semita
14
+
15
+ passable = numpy.ones((5, 5), dtype=bool)
16
+ passable[2, :4] = False # a wall, open at its far end
17
+
18
+ path = semita.find_path(passable, (0, 0), (4, 0))
19
+ assert round(path.distance, 2) == 10.83 # round the wall's end
20
+ assert path.tiles[:3].tolist() == [[0, 0], [1, 1], [1, 2]]
21
+
22
+ assert semita.find_path(passable, (0, 0), (2, 1)) is None # the end is blocked
23
+
24
+ paths = semita.cheapest_paths_from(passable, (0, 0)) # to every tile, in one pass
25
+ assert paths.distances[4, 0] == path.distance
26
+ assert paths.distances[2, 1] == float("inf")
27
+ assert paths.path_to((4, 0)).cost == path.cost
28
+
29
+ # Costs: each value is the cost of moving a unit of distance across its tile, 1 on normal ground and more where it
30
+ # is best avoided; 0 blocks, as False does.
31
+ costs = numpy.ones((7, 7))
32
+ costs[2:5, 2:5] = 10.0 # a patch to keep out of
33
+ around = semita.find_path(costs, (3, 0), (3, 6))
34
+ assert round(around.cost, 2) == round(around.distance, 2) == 7.66 # round the patch, on ground costing 1
35
+ assert semita.find_path(numpy.ones((7, 7), dtype=bool), (3, 0), (3, 6)).distance == 6.0 # straight, on bools
36
+ ```
37
+
38
+ ## The rules
39
+
40
+ - A grid is a 2-D numpy array of bools or real numbers, or anything numpy reads as one, indexed `[i, j]`. Its values
41
+ are the tiles' costs: the cost of moving one unit of distance across each, 1 for normal ground, less where movement
42
+ is faster, more where it is slower or best avoided.
43
+ - 0 and `inf` block a tile. A grid of bools is the simplest: `True` costs 1 and `False` blocks. Otherwise a cost is
44
+ positive; NaN and negatives raise `ValueError`.
45
+ - A step goes to one of the eight neighboring tiles, 1 long straight and √2 diagonally, and only between passable
46
+ tiles. A diagonal step needs both tiles beside it passable too, so no path cuts a corner. A step costs its length
47
+ times the mean of the costs of the two tiles it joins, so a path costs the same both ways.
48
+ - The start is a path to itself, costing nothing, even on a blocked tile, but no step leaves or enters a blocked one.
49
+ - A start or an end outside the grid raises `IndexError`; a negative index counts as outside.
50
+ - `find_path` returns a `Path`, its `distance`, its `cost` and its `tiles` from the start to the end, or `None` if
51
+ there is no path. Of several equally cheap paths it returns one, but which is not part of the contract.
52
+ - `cheapest_paths_from` returns the cheapest path from the start to every tile, found in one pass, as
53
+ `CheapestPaths`: their `costs` and their `distances`, two read-only arrays of the grid's shape, `inf` where no path
54
+ reaches, and `path_to(tile)`, a tile's path as a `Path`, or `None` where none reaches. Where several paths to a tile
55
+ cost the same, it holds one of them. On a grid of bools the distances are the costs.
56
+ - A grid whose passable tiles all cost the same, a grid of bools included, is searched by jump point search; any other
57
+ by A*, which visits more tiles, the more so the cheaper the cheapest tile is, since its estimate of what is left is
58
+ the straight distance times that cost.
59
+ - A grid of bools, or a C-contiguous one of float64, is read as it is; any other is converted on each call, so one
60
+ searched often is best kept as one of those.
61
+
62
+ Each search allocates its own memory and releases the GIL, so searches run in parallel on threads. A grid must not be
63
+ written to while a search reads it.
64
+
65
+ ## Building
66
+
67
+ semita is a C extension on Python's stable ABI, so one wheel serves every Python from 3.12 on a platform. Installing
68
+ from source compiles it with the platform's own compiler: MSVC on Windows, gcc or clang elsewhere.
69
+
70
+ ```bash
71
+ uv sync --extra dev
72
+ uv run pytest
73
+ ```
74
+
75
+ The tests check every search against scipy's Dijkstra over the same steps.
76
+
77
+ ## Releasing
78
+
79
+ Publish a GitHub release tagged `v` and the new version, such as `v0.2.0`. The version is the tag, which
80
+ setuptools-scm reads from git; no file holds it.
81
+
82
+ The `release` workflow then builds the sdist, builds from it a wheel for Linux x86_64 and aarch64, Windows AMD64 and
83
+ macOS arm64 and x86_64, tests and audits each for the stable ABI alone, checks that the tag is a version,
84
+ and uploads them to PyPI. Every pull request builds and checks the same files without uploading them.
85
+
86
+ PyPI trusts the workflow through trusted publishing, from the repository's `pypi` environment, so no token is stored
87
+ anywhere.
@@ -0,0 +1,90 @@
1
+ /*
2
+ * The cheapest path between two tiles on a grid with costs, by A*. Its estimate of what is left, the octile distance
3
+ * times the cheapest cost on the grid, never exceeds what a step there can cost, so the first time the search
4
+ * settles the end it has found the cheapest path.
5
+ */
6
+
7
+ #include <math.h>
8
+ #include <stdint.h>
9
+ #include <stdlib.h>
10
+
11
+ #include "heap.h"
12
+ #include "search.h"
13
+
14
+ /* What a search knows of a tile: nothing yet, a cost to it that may still fall, or its final cost. */
15
+ enum { UNREACHED = 0, REACHED = 1, SETTLED = 2 };
16
+
17
+ SearchStatus find_cheapest_path(
18
+ const Grid *grid, int start, int end, double cheapest, int **tiles, int *tile_count, double *cost,
19
+ double *distance
20
+ ) {
21
+ int height = grid->height;
22
+ int end_i = end / height, end_j = end % height;
23
+ *tiles = NULL;
24
+ *tile_count = 0;
25
+ *cost = *distance = INFINITY;
26
+ if (start == end) {
27
+ int *path = malloc(2 * sizeof *path);
28
+ if (!path) return SEARCH_OUT_OF_MEMORY;
29
+ path[0] = end_i;
30
+ path[1] = end_j;
31
+ *tiles = path;
32
+ *tile_count = 1;
33
+ *cost = *distance = 0.0;
34
+ return SEARCH_DONE;
35
+ }
36
+ if (!is_passable(grid, start / height, start % height) || !is_passable(grid, end_i, end_j)) return SEARCH_DONE;
37
+
38
+ size_t count = (size_t)grid->width * (size_t)height;
39
+ double *best = malloc(count * sizeof *best);
40
+ int *parent = malloc(count * sizeof *parent);
41
+ uint8_t *state = calloc(count, 1);
42
+ Heap heap = {0};
43
+ SearchStatus status = SEARCH_DONE;
44
+ if (!best || !parent || !state) {
45
+ status = SEARCH_OUT_OF_MEMORY;
46
+ goto done;
47
+ }
48
+ state[start] = REACHED;
49
+ best[start] = 0.0;
50
+ parent[start] = -1;
51
+ if (heap_push(&heap, cheapest * octile_distance(start / height, start % height, end_i, end_j), start)) {
52
+ status = SEARCH_OUT_OF_MEMORY;
53
+ goto done;
54
+ }
55
+
56
+ while (heap.size > 0) {
57
+ int tile = heap_pop(&heap).tile;
58
+ if (state[tile] == SETTLED) continue;
59
+ state[tile] = SETTLED;
60
+ if (tile == end) {
61
+ status = write_path(grid, end, parent, tiles, tile_count, distance);
62
+ if (status == SEARCH_DONE) *cost = best[end];
63
+ break;
64
+ }
65
+ int i = tile / height, j = tile % height;
66
+ for (int step = 0; step < 8; step++) {
67
+ int di = STEP_I[step], dj = STEP_J[step];
68
+ if (!can_step(grid, i, j, di, dj)) continue;
69
+ int neighbor = tile + di * height + dj;
70
+ if (state[neighbor] == SETTLED) continue;
71
+ double neighbor_cost = best[tile] + step_cost(grid, tile, di, dj);
72
+ if (state[neighbor] == UNREACHED || neighbor_cost < best[neighbor]) {
73
+ state[neighbor] = REACHED;
74
+ best[neighbor] = neighbor_cost;
75
+ parent[neighbor] = tile;
76
+ double estimate = neighbor_cost + cheapest * octile_distance(i + di, j + dj, end_i, end_j);
77
+ if (heap_push(&heap, estimate, neighbor)) {
78
+ status = SEARCH_OUT_OF_MEMORY;
79
+ goto done;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ done:
85
+ heap_free(&heap);
86
+ free(best);
87
+ free(parent);
88
+ free(state);
89
+ return status;
90
+ }
@@ -0,0 +1,25 @@
1
+ /* The costs a grid gives its tiles, checked before a search reads them. */
2
+
3
+ #include <math.h>
4
+ #include <stddef.h>
5
+
6
+ #include "search.h"
7
+
8
+ SearchStatus check_costs(const Grid *grid, double *cheapest, double *dearest, int *bad_tile) {
9
+ size_t tile_count = (size_t)grid->width * (size_t)grid->height;
10
+ double lowest = INFINITY, highest = 0.0;
11
+ for (size_t tile = 0; tile < tile_count; tile++) {
12
+ double cost = grid->costs[tile];
13
+ if (cost == 0.0 || cost == INFINITY) continue;
14
+ /* Written so that NaN fails it too. */
15
+ if (!(cost > 0.0)) {
16
+ *bad_tile = (int)tile;
17
+ return SEARCH_BAD_COST;
18
+ }
19
+ if (cost < lowest) lowest = cost;
20
+ if (cost > highest) highest = cost;
21
+ }
22
+ *cheapest = lowest;
23
+ *dearest = highest;
24
+ return SEARCH_DONE;
25
+ }
@@ -0,0 +1,54 @@
1
+ /* The cheapest path from one tile to every tile, by Dijkstra's algorithm: each path's cost, its length and the tile
2
+ it arrives from. */
3
+
4
+ #include <math.h>
5
+ #include <stdint.h>
6
+ #include <stdlib.h>
7
+
8
+ #include "heap.h"
9
+ #include "search.h"
10
+
11
+ SearchStatus fill_flood(const Grid *grid, int start, double *costs, double *distances, int *parents) {
12
+ int height = grid->height;
13
+ size_t tile_count = (size_t)grid->width * (size_t)height;
14
+ /* A tile's parent is written when it is reached, and read only where its cost is finite. */
15
+ for (size_t tile = 0; tile < tile_count; tile++) costs[tile] = distances[tile] = INFINITY;
16
+ costs[start] = distances[start] = 0.0;
17
+ parents[start] = -1;
18
+ if (!is_passable(grid, start / height, start % height)) return SEARCH_DONE;
19
+
20
+ uint8_t *settled = calloc(tile_count, 1);
21
+ if (!settled) return SEARCH_OUT_OF_MEMORY;
22
+ Heap heap = {0};
23
+ SearchStatus status = SEARCH_DONE;
24
+ if (heap_push(&heap, 0.0, start)) {
25
+ status = SEARCH_OUT_OF_MEMORY;
26
+ goto done;
27
+ }
28
+ while (heap.size > 0) {
29
+ int tile = heap_pop(&heap).tile;
30
+ if (settled[tile]) continue;
31
+ settled[tile] = 1;
32
+ int i = tile / height, j = tile % height;
33
+ for (int step = 0; step < 8; step++) {
34
+ int di = STEP_I[step], dj = STEP_J[step];
35
+ if (!can_step(grid, i, j, di, dj)) continue;
36
+ int neighbor = tile + di * height + dj;
37
+ if (settled[neighbor]) continue;
38
+ double cost = costs[tile] + step_cost(grid, tile, di, dj);
39
+ if (cost < costs[neighbor]) {
40
+ costs[neighbor] = cost;
41
+ distances[neighbor] = distances[tile] + (di && dj ? SQRT_2 : 1.0);
42
+ parents[neighbor] = tile;
43
+ if (heap_push(&heap, cost, neighbor)) {
44
+ status = SEARCH_OUT_OF_MEMORY;
45
+ goto done;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ done:
51
+ heap_free(&heap);
52
+ free(settled);
53
+ return status;
54
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * A binary min-heap of tiles keyed by cost, growing as it fills. A tile may be pushed again at a lower cost; the
3
+ * searches skip the entries they have already settled.
4
+ */
5
+
6
+ #ifndef SEMITA_HEAP_H
7
+ #define SEMITA_HEAP_H
8
+
9
+ #include <stddef.h>
10
+ #include <stdlib.h>
11
+
12
+ typedef struct {
13
+ double cost;
14
+ int tile;
15
+ } HeapEntry;
16
+
17
+ typedef struct {
18
+ HeapEntry *entries;
19
+ size_t size;
20
+ size_t capacity;
21
+ } Heap;
22
+
23
+ #define HEAP_FIRST_CAPACITY 256
24
+
25
+ /* Push `tile` at `cost`: 0 if pushed, -1 if memory ran out. */
26
+ static inline int heap_push(Heap *heap, double cost, int tile) {
27
+ if (heap->size == heap->capacity) {
28
+ size_t capacity = heap->capacity ? 2 * heap->capacity : HEAP_FIRST_CAPACITY;
29
+ HeapEntry *entries = realloc(heap->entries, capacity * sizeof *entries);
30
+ if (!entries) return -1;
31
+ heap->entries = entries;
32
+ heap->capacity = capacity;
33
+ }
34
+ size_t position = heap->size++;
35
+ while (position > 0) {
36
+ size_t parent = (position - 1) / 2;
37
+ if (heap->entries[parent].cost <= cost) break;
38
+ heap->entries[position] = heap->entries[parent];
39
+ position = parent;
40
+ }
41
+ heap->entries[position].cost = cost;
42
+ heap->entries[position].tile = tile;
43
+ return 0;
44
+ }
45
+
46
+ /* Remove and return the cheapest entry. The heap must not be empty. */
47
+ static inline HeapEntry heap_pop(Heap *heap) {
48
+ HeapEntry cheapest = heap->entries[0];
49
+ HeapEntry last = heap->entries[--heap->size];
50
+ size_t position = 0;
51
+ for (;;) {
52
+ size_t child = 2 * position + 1;
53
+ if (child >= heap->size) break;
54
+ if (child + 1 < heap->size && heap->entries[child + 1].cost < heap->entries[child].cost) child++;
55
+ if (last.cost <= heap->entries[child].cost) break;
56
+ heap->entries[position] = heap->entries[child];
57
+ position = child;
58
+ }
59
+ if (heap->size > 0) heap->entries[position] = last;
60
+ return cheapest;
61
+ }
62
+
63
+ static inline void heap_free(Heap *heap) {
64
+ free(heap->entries);
65
+ heap->entries = NULL;
66
+ heap->size = heap->capacity = 0;
67
+ }
68
+
69
+ #endif /* SEMITA_HEAP_H */