Added path length calculations

This commit is contained in:
Emi Simpson 2023-02-12 16:06:28 -05:00
parent 1e420e83e9
commit 8236281181
Signed by: Emi
GPG key ID: A12F2C2FFDC3D847

View file

@ -52,6 +52,15 @@ class Point(NamedTuple):
def __repr__(self):
return f'({self.x}, {self.y})'
@dataclass(frozen=True)
class OobError:
"""
The distance cannot be computed because one point is out of bounds
This means off the map, not the OOB terrain type.
"""
p: Point
@dataclass
class World:
tiles: Sequence[Tuple[Terrain, int]]
@ -270,6 +279,8 @@ class World:
>>> world = World([], lon_scale = 500, lat_scale = 400)
>>> world.heuristic(Point(0, 0), Point(3, 5))
1360000
For a slower algorithm that also includes elevation, see `calculate_distance()`.
"""
# Taxicab distance in each direction
@ -294,6 +305,93 @@ class World:
return estimated_speed * total_flat_distance
def calculate_distance(self, a: Point, b:Point) -> Result[int, OobError]:
"""
Calculate the distance between the centers of two tiles, incl elevation difference
Looks up the elevation at both points **a** and **b**, converting them into points
in 3D space, then computes the integer distance between
For a faster algorithm that does not include elevation, see `heuristic()`.
>>> world = World( # We instantiate a simple, small world
... [ (Terrain.OPEN_LAND, 1_000), (Terrain.BRUSH, 850), (Terrain.WET, 500)
... , (Terrain.ROUGH_MEADOW, 950), (Terrain.EASY_FOREST, 750), (Terrain.WET, 500)
... ],
... width = 3, # Our simple world is only two tiles wide
... lon_scale = 600, # Pick an easy number for example
... lat_scale = 500
... )
>>> world.calculate_distance(Point(0, 0), Point(2, 1))
Ok(1392)
>>> world.calculate_distance(Point(0, 0), Point(2, 2))
Err(OobError(p=(2, 2)))
Notice that this is distance as-the-crow-flies between two points in 3D space. If
you're looking for actual distance, you must either use two adjacent points, or
first pathfind between those two points and calculate the distance that way.
"""
lon_dist = abs(a.x - b.x) * self.lon_scale
lat_dist = abs(a.y - b.y) * self.lat_scale
try:
a_elev = self[a][1]
except IndexError:
return Err(OobError(a))
try:
b_elev = self[b][1]
except IndexError:
return Err(OobError(b))
elev_dist = abs(a_elev - b_elev)
return Ok(isqrt(lon_dist * lon_dist + lat_dist * lat_dist + elev_dist * elev_dist))
@tco_rec
def calculate_path_length(self, points: Sequence[Point], addend: int = 0) -> Result[int, OobError]:
"""
Calculate the length of a path to the greatest degree of accuracy possible
Points are expected to be a sequence of at least two adjacent points. The
returned distance will be the distance travelling between the centers of the tiles
of each tile in sequence, including elevation changes.
Asequential points will not result in an error, but will result in a drop in
accuracy. Diagonal moves to adjacent tiles, however, are valid and allowed.
Any points which fall outside the bounds of the map will result in an `OobError`
indicating the first point which was out of bounds.
On a successful run, the returned units will be in the units the world was
instantiated with, typically millimeters.
>>> world = World( # We instantiate a simple, small world
... [ (Terrain.OPEN_LAND, 1_000), (Terrain.BRUSH, 850), (Terrain.WET, 500)
... , (Terrain.ROUGH_MEADOW, 950), (Terrain.EASY_FOREST, 750), (Terrain.WET, 500)
... ],
... width = 3, # Our simple world is only two tiles wide
... lon_scale = 600, # Pick an easy number for example
... lat_scale = 500
... )
>>> world.calculate_path_length([Point(0,0), Point(1, 0), Point(2,1)])
Ok(1473)
Calling this method with only two points is equivalent to calling
`calculate_distance()`.
>>> world.calculate_path_length([Point(0, 0), Point(2, 1)])
Ok(1392)
>>> world.calculate_distance(Point(0, 0), Point(2, 1))
Ok(1392)
"""
match points:
case [a, b, *rest]:
match self.calculate_distance(a, b):
case Ok(this_dist):
return Recur(self, (b, *rest), addend + this_dist)
case err:
return Return(Ok(err))
case _: # single or empty
return Return(Ok(addend))
if __name__ == '__main__':
import doctest
doctest.testmod()