from __future__ import annotations
from typing import Dict, List, Tuple
import miniworlds.base.exceptions as miniworlds_exception
import miniworlds.worlds.hex_world.hex_camera_manager as hex_camera_manager
import miniworlds.worlds.hex_world.hex_corner as hex_corner_mod
import miniworlds.worlds.hex_world.hex_edge as hex_edge_mod
import miniworlds.worlds.hex_world.hex_tile as hex_tile_mod
import miniworlds.worlds.hex_world.hex_tile_factory as hex_tile_factory
import miniworlds.worlds.hex_world.hex_world_connector as hex_world_connector
import miniworlds.worlds.hex_world.hex_coords as hex_coords
import miniworlds.worlds.tiled_world.tiled_world as tiled_world
[docs]
class HexWorld(tiled_world.TiledWorld):
"""A world that consists of hexagonal tiles.
Hexes are addressed with offset coordinates `(column, row)`, like
tiles in a TiledWorld. Odd rows are shifted half a hex to the right
(pointy-top layout). Actors can also be placed on corners (shared by
up to three hexes) or edges (shared by two hexes).
The six neighbors of a hex are reached with the directions
`"o"`, `"so"`, `"sw"`, `"w"`, `"nw"`, `"no"`. Because odd rows are
shifted, the diagonal neighbors depend on the row parity:
* Odd rows: `"no"` and `"so"` point to `(column + 1, row - 1)` and
`(column + 1, row + 1)`, while `"nw"` and `"sw"` point to
`(column, row - 1)` and `(column, row + 1)`.
* Even rows: `"no"` and `"so"` point to `(column, row - 1)` and
`(column, row + 1)`, while `"nw"` and `"sw"` point to
`(column - 1, row - 1)` and `(column - 1, row + 1)`.
`"o"` and `"w"` always point to `(column + 1, row)` and
`(column - 1, row)`.
Examples:
::
from miniworlds import HexWorld, Actor
world = HexWorld(7, 7, tile_size=40)
actor = Actor((3, 3))
actor.fill_color = (255, 0, 0)
world.run()
"""
is_hex = True
[docs]
def __init__(self, x: int = 20, y: int = 16, tile_size: int = 40, empty=False):
"""Create a hex world.
Args:
x: Number of columns.
y: Number of rows.
tile_size: The hex size in pixels (circumradius of the hex).
empty: If `True`, tiles, edges, and corners are not created
automatically.
"""
super().__init__(x=x, y=y, tile_size=tile_size, empty=empty)
@staticmethod
def _get_camera_manager_class():
return hex_camera_manager.HexCameraManager
@staticmethod
def _get_world_connector_class():
return hex_world_connector.HexWorldConnector
def _get_tile_factory(self):
return hex_tile_factory.HexTileFactory()
def _templates(self):
return hex_tile_mod.HexTile, hex_edge_mod.HexEdge, hex_corner_mod.HexCorner
[docs]
def to_pixel(self, position, size=(0, 0), origin=(0, 0)):
"""Convert a hex position to the pixel position of its center.
Args:
position: Hex position as `(column, row)`.
size: Reserved for compatibility.
origin: Pixel offset added to the converted position.
Returns:
Pixel position as `(x, y)`.
Examples:
::
pixel = world.to_pixel((2, 3))
"""
center = hex_coords.hex_center(position, self.tile_size)
return (center[0] + origin[0], center[1] + origin[1])
[docs]
def get_tile_from_pixel(self, position):
"""Return the hex tile under a screen pixel.
Args:
position: Pixel position as `(x, y)`.
Returns:
The hex tile under the pixel, or `None` if the pixel lies
outside the world (no tile exists at the rounded hex
position).
Examples:
::
tile = world.get_tile_from_pixel((80, 120))
if tile is not None:
tile.fill_color = (255, 0, 0)
"""
tile_cls = self.tile_factory.tile_cls
tile = tile_cls.from_pixel(position, self)
if not self.is_tile(tile.position):
return None
return tile
[docs]
def get_from_pixel(self, position):
"""Return the hex position for a screen pixel.
Args:
position: Pixel position as `(x, y)`.
Returns:
Hex position as `(column, row)`, or `None` if the pixel
lies outside the world.
Examples:
::
hex_position = world.get_from_pixel((80, 120))
"""
tile = self.get_tile_from_pixel(position)
if tile is None:
return None
return tile.position
[docs]
def get_actors_from_pixel(self, position):
"""Return all actors on the hex tile under a screen pixel.
Args:
position: Pixel position as `(x, y)`.
Returns:
Actors located on the hex tile under the pixel. An empty
list if the pixel lies outside the world.
Examples:
::
actors = world.get_actors_from_pixel(mouse_pos)
"""
tile = self.get_tile_from_pixel(position)
if tile is None:
return []
return self.detect_actors(tile.position)
[docs]
def get_corner_from_pixel(self, position):
"""Return the corner nearest to a screen pixel.
Args:
position: Pixel position as `(x, y)`.
Returns:
The nearest corner, or `None` if the pixel is outside
the world.
Examples:
::
corner = world.get_corner_from_pixel(mouse_pos)
"""
tile = self.get_tile_from_pixel(position)
if tile is None:
return None
nearest_corner = None
nearest_distance = None
for direction in hex_coords.CORNER_ORDER:
corner = self.get_corner(tile.position, direction)
corner_pixel = corner.to_pixel()
distance = (
(corner_pixel[0] - position[0]) ** 2
+ (corner_pixel[1] - position[1]) ** 2
)
if nearest_distance is None or distance < nearest_distance:
nearest_corner = corner
nearest_distance = distance
return nearest_corner
[docs]
def get_edge_from_pixel(self, position):
"""Return the edge nearest to a screen pixel.
Args:
position: Pixel position as `(x, y)`.
Returns:
The nearest edge, or `None` if the pixel is outside
the world.
Examples:
::
edge = world.get_edge_from_pixel(mouse_pos)
"""
tile = self.get_tile_from_pixel(position)
if tile is None:
return None
nearest_edge = None
nearest_distance = None
for direction in hex_coords.CUBE_DIRECTIONS:
edge = self.get_edge(tile.position, direction)
edge_pixel = edge.to_pixel()
distance = (
(edge_pixel[0] - position[0]) ** 2
+ (edge_pixel[1] - position[1]) ** 2
)
if nearest_distance is None or distance < nearest_distance:
nearest_edge = edge
nearest_distance = distance
return nearest_edge
[docs]
def get_center_points(self) -> Dict[Tuple[int, int], Tuple[float, float]]:
"""Return a mapping of hex positions to their center pixels."""
center_points = dict()
for position, tile in self.tiles.items():
center_points[position] = tile.to_center()
return center_points
[docs]
def get_corner(self, position, direction=None):
"""Return a corner by corner position or hex position plus direction.
Args:
position: Corner position, or hex position when `direction` is set.
direction: Optional corner direction, e.g. `"no"`.
Returns:
The matching corner.
Raises:
CornerNotFoundError: If no corner exists at the resolved position.
Examples:
::
corner = world.get_corner((3, 3), "no")
"""
if direction is not None:
position = hex_coords.corner_position(position, direction)
if position in self.corners:
return self.corners[position]
raise miniworlds_exception.CornerNotFoundError(position)
[docs]
def get_edge(self, position, direction=None):
"""Return an edge by edge position or hex position plus direction.
Args:
position: Edge position, or hex position when `direction` is set.
direction: Optional edge direction, e.g. `"o"`.
Returns:
The matching edge.
Raises:
EdgeNotFoundError: If no edge exists at the resolved position.
Examples:
::
edge = world.get_edge((3, 3), "o")
"""
if direction is not None:
position = hex_coords.edge_position(position, direction)
if position in self.edges:
return self.edges[position]
raise miniworlds_exception.EdgeNotFoundError(position)
[docs]
def get_tiles_in_radius(self, position, radius: int) -> List[hex_tile_mod.HexTile]:
"""Return all tiles within a hex distance of `radius`.
Neighbors are computed in cube coordinates, so the result is
independent of the row parity described in the `HexWorld`
docstring.
Args:
position: Hex position as `(column, row)`.
radius: Maximum hex distance (0 includes only the tile itself).
Returns:
List of tiles within the radius, including the tile itself,
ordered from the center outwards in ring traversal order.
Positions outside the world are skipped.
Examples:
::
neighbours = world.get_tiles_in_radius((3, 3), 1)
"""
if radius < 0:
return []
tiles = []
for ring_radius in range(radius + 1):
tiles.extend(self.get_tiles_in_ring(position, ring_radius))
return tiles
[docs]
def get_tiles_in_ring(self, position, radius: int) -> List[hex_tile_mod.HexTile]:
"""Return all tiles at exactly `radius` hex distance.
Neighbors are computed in cube coordinates, so the result is
independent of the row parity described in the `HexWorld`
docstring.
Args:
position: Hex position as `(column, row)`.
radius: Hex distance.
Returns:
List of tiles at the given distance, in ring traversal
order starting at the westernmost hex of the ring.
Positions outside the world are skipped.
Examples:
::
ring = world.get_tiles_in_ring((3, 3), 2)
"""
cube = hex_coords.offset_to_cube(position)
tiles = []
for ring_cube in hex_coords.cube_ring(cube, radius):
tile_position = hex_coords.cube_to_offset(ring_cube)
if self.is_tile(tile_position):
tiles.append(self.get_tile(tile_position))
return tiles