from __future__ import annotations
import abc
import math
from typing import List, Dict, Tuple, TYPE_CHECKING
import miniworlds.base.app as app
import miniworlds.worlds.tiled_world.tile as tile_mod
if TYPE_CHECKING:
import miniworlds.worlds.world as world_mod
[Doku]
class TileBase(abc.ABC):
"""Base class for Tiles and TileDelimiters (like Corners, Edges)."""
corner_vectors: dict = {}
tile_vectors: dict = {}
# Cache: maps world id to a dict of tile positions and their corresponding pixel coordinates
_position_pixel_cache: Dict[int, Dict[Tuple[int, int], Tuple[float, float]]] = {}
# Cache: maps world id to a list of (tile position, pixel coordinate) tuples for fast iteration
_position_pixel_list_cache: Dict[int, List[Tuple[Tuple[int, int], Tuple[float, float]]]] = {}
def __init__(self, position: Tuple[int, int], world: world_mod.World = None):
self._neighbour_tiles = None
self.int_coord = self._internal_coordinates()
self.world = world or app.App.get_running_world()
self.position = position
self._world_position = position
self.positions = [self.position]
[Doku]
@classmethod
@abc.abstractmethod
def from_position(cls, position: Tuple[int, int], world: world_mod.World) -> TileBase:
"""Create a tile from a world-relative position."""
pass
@staticmethod
def _get_corner_cls():
return TileDelimiter
@staticmethod
def _get_edge_cls():
return TileDelimiter
[Doku]
@classmethod
def from_pixel(cls, pixel_position: Tuple[float, float], world: world_mod.World) -> TileBase:
"""
Finds the nearest tile to a given pixel coordinate.
This method uses caching to avoid redundant recalculation of the mapping
between tile grid positions and their corresponding pixel coordinates for
a given world. It also uses squared distance instead of Euclidean distance
for better performance.
Args:
pixel_position (Tuple[float, float]): The (x, y) pixel position.
world (world_mod.World): The world instance containing the tile layout.
Returns:
TileBase: The tile object closest to the given pixel position.
"""
world_id = id(world)
if world_id not in cls._position_pixel_cache:
# Compute and cache the pixel positions for all tiles in the world
position_pixel_dict = cls.get_position_pixel_dict(world)
cls._position_pixel_cache[world_id] = position_pixel_dict
cls._position_pixel_list_cache[world_id] = list(position_pixel_dict.items())
# Find the tile position with the smallest squared distance to the pixel
nearest_pos = min(
cls._position_pixel_list_cache[world_id],
key=lambda item: (item[1][0] - pixel_position[0]) ** 2 + (item[1][1] - pixel_position[1]) ** 2
)[0]
return cls.from_position(nearest_pos, world)
[Doku]
@staticmethod
def get_position_pixel_dict(world: world_mod.World) -> dict:
"""Returns a mapping of tile positions to pixel coordinates."""
pass
[Doku]
@staticmethod
def get_local_center_coordinate(world: world_mod.World) -> Tuple[float, float]:
"""Returns the center point offset for a tile, in local pixel coordinates."""
ts = world.tile_size
return ts / 2, ts / 2
@staticmethod
def _internal_coordinates() -> Tuple[float, float]:
"""Returns internal coordinates, if needed (override in subclass)."""
return 0.0, 0.0
[Doku]
def merge(self, other: TileBase):
"""Merge tile positions from another tile at the same location."""
if other.position != self.position:
raise ValueError(f"Tiles must share the same position to merge. Got {self.position} and {other.position}.")
for pos in other.positions:
if pos not in self.positions:
self.positions.append(pos)
[Doku]
def get_actors(self) -> List:
"""Returns all actors currently on this tile."""
return [actor for actor in self.world.actors if actor.position == self.position]
[Doku]
def add_actor(self, actor):
"""Places an actor on this tile."""
actor.position = self.position
[Doku]
def get_neighbour_tiles(self) -> List[tile_mod.Tile]:
"""Returns neighboring tiles around this tile."""
if self._neighbour_tiles is not None:
return self._neighbour_tiles
neighbours = []
for direction, vector in self.tile_vectors.items():
new_pos = tuple(self.position[i] + vector[i] for i in range(2))
if self.world.is_tile(new_pos):
tile = self.world.get_tile(new_pos)
if tile and tile not in neighbours:
neighbours.append(tile)
self._neighbour_tiles = neighbours
return neighbours
[Doku]
def get_local_corner_points(self):
"""Returns corner point offsets for rendering."""
return [
self._get_corner_cls()(self._world_position, direction, self.world)
.get_local_coordinate_for_tile(self)
for direction, vector in self.corner_vectors.items()
]
class TileDelimiter(TileBase):
"""Base class for corners and edges (tile delimiters)."""
angles: Dict[str, int] = {}
direction_angles: Dict[str, int] = {}
def __init__(self, position, direction: str, world):
super().__init__(position, world)
self.tile = self.world.get_tile(position)
self.direction_str = direction
self.direction = self.direction_vectors()[direction]
self.position = tuple(self.tile.position[i] + self.direction[i] for i in range(2))
self.positions = [(self.position, self.direction)]
self.angle = self.direction_angles[direction]
@classmethod
def from_position(cls, position, world):
"""Create a tile delimiter (override expected)."""
return cls(position, direction="default", world=world)
@classmethod
@abc.abstractmethod
def direction_vectors(cls) -> Dict[str, Tuple[int, int]]:
"""Returns the available directions."""
pass
def get_direction(self):
return self.direction_angles[self.direction_str]
def get_local_coordinate_for_tile(self, tile) -> Tuple[float, float]:
"""Returns pixel offset of delimiter relative to given tile."""
dx = self.to_pixel()[0] - tile.to_pixel()[0]
dy = self.to_pixel()[1] - tile.to_pixel()[1]
return dx, dy
def get_local_coordinate_for_base_tile(self) -> Tuple[float, float]:
"""Returns offset in pixels from base tile center."""
center = TileBase.get_local_center_coordinate(self.world)
if self.angles:
direction = self._get_direction_string(self.direction)
angle_index = self.angles[direction]
angle_rad = 2.0 * math.pi * (self.start_angle() - angle_index) / len(self.angles)
radius = self.world.tile_size / 2
return (
center[0] + radius * math.cos(angle_rad),
center[1] + radius * math.sin(angle_rad),
)
return center
def to_pixel(self) -> Tuple[float, float]:
"""Returns absolute pixel position of this delimiter."""
base_pixel = self.tile.to_pixel()
offset = self.get_local_coordinate_for_base_tile()
return base_pixel[0] + offset[0], base_pixel[1] + offset[1]
def _get_direction_string(self, direction: Tuple[int, int]) -> str:
"""Returns the string key for a direction vector."""
for name, vec in self.direction_vectors().items():
if vec == direction:
return name
raise ValueError(f"Unknown direction vector: {direction}")
def get_angle(self, direction: str) -> int:
"""Returns angle for a given direction string."""
return self.angles[direction]
def start_angle(self) -> int:
"""Override: starting angle offset for layouting (e.g. hex rotation)."""
return 0