Added path length calculations
This commit is contained in:
parent
1e420e83e9
commit
8236281181
98
world.py
98
world.py
|
@ -52,6 +52,15 @@ class Point(NamedTuple):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'({self.x}, {self.y})'
|
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
|
@dataclass
|
||||||
class World:
|
class World:
|
||||||
tiles: Sequence[Tuple[Terrain, int]]
|
tiles: Sequence[Tuple[Terrain, int]]
|
||||||
|
@ -270,6 +279,8 @@ class World:
|
||||||
>>> world = World([], lon_scale = 500, lat_scale = 400)
|
>>> world = World([], lon_scale = 500, lat_scale = 400)
|
||||||
>>> world.heuristic(Point(0, 0), Point(3, 5))
|
>>> world.heuristic(Point(0, 0), Point(3, 5))
|
||||||
1360000
|
1360000
|
||||||
|
|
||||||
|
For a slower algorithm that also includes elevation, see `calculate_distance()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Taxicab distance in each direction
|
# Taxicab distance in each direction
|
||||||
|
@ -294,6 +305,93 @@ class World:
|
||||||
|
|
||||||
return estimated_speed * total_flat_distance
|
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__':
|
if __name__ == '__main__':
|
||||||
import doctest
|
import doctest
|
||||||
doctest.testmod()
|
doctest.testmod()
|
Loading…
Reference in a new issue