From 2d82d9834fc18766fc2e80092d5b98234d9064d0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 7 Mar 2023 22:18:31 -0800 Subject: [PATCH] Do pathfinding from the hero to the mouse point It works finally! And uses A*! --- erynrl/interface/__init__.py | 6 ++-- erynrl/interface/window/__init__.py | 8 +++-- erynrl/interface/window/map.py | 46 ++++++++++++++++------------- erynrl/map/__init__.py | 17 +++++------ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/erynrl/interface/__init__.py b/erynrl/interface/__init__.py index d843d44..3375cd7 100644 --- a/erynrl/interface/__init__.py +++ b/erynrl/interface/__init__.py @@ -28,7 +28,8 @@ class Interface: self.map_window = MapWindow( Rect.from_raw_values(0, 0, size.width, size.height - 5), - engine.map) + engine.map, + engine.hero) self.info_window = InfoWindow( Rect.from_raw_values(0, size.height - 5, 28, 5)) self.message_window = MessageLogWindow( @@ -43,7 +44,7 @@ class Interface: hero = self.engine.hero self.info_window.update_hero(hero) - self.map_window.update_drawable_map_bounds(hero) + self.map_window.update_drawable_map_bounds() sorted_entities = sorted(self.engine.entities, key=lambda e: e.render_order.value) self.map_window.entities = sorted_entities @@ -64,6 +65,7 @@ class Interface: context.present(self.console) for event in tev.wait(): + context.convert_event(event) did_handle = self.event_handler.dispatch(event) if did_handle: continue diff --git a/erynrl/interface/window/__init__.py b/erynrl/interface/window/__init__.py index bde8a08..3119246 100644 --- a/erynrl/interface/window/__init__.py +++ b/erynrl/interface/window/__init__.py @@ -1,23 +1,25 @@ # Eryn Wells -from typing import Optional +from typing import Generic, Optional, TypeVar from tcod import event as tev from tcod.console import Console from ...geometry import Point, Rect, Vector +WindowT = TypeVar('WindowT', bound='Window') + class Window: '''A user interface window. It can be framed and it can handle events.''' - class EventHandler(tev.EventDispatch[bool]): + class EventHandler(tev.EventDispatch[bool], Generic[WindowT]): ''' Handles events for a Window. Event dispatch methods return True if the event was handled and no further action is needed. ''' - def __init__(self, window: 'Window'): + def __init__(self, window: WindowT): super().__init__() self.window = window diff --git a/erynrl/interface/window/map.py b/erynrl/interface/window/map.py index 118b0f6..a7ab6ce 100644 --- a/erynrl/interface/window/map.py +++ b/erynrl/interface/window/map.py @@ -1,6 +1,6 @@ # Eryn Wells -from typing import List +from typing import List, Optional import numpy as np import tcod.event as tev @@ -16,7 +16,7 @@ from ...object import Entity, Hero class MapWindow(Window): '''A Window that displays a game map''' - class EventHandler(Window.EventHandler): + class EventHandler(Window.EventHandler['MapWindow']): '''An event handler for the MapWindow.''' def ev_mousemotion(self, event: tev.MouseMotion) -> bool: @@ -24,23 +24,40 @@ class MapWindow(Window): if not mouse_point: return False - # TODO: Convert window point to map point - # TODO: Perform a path finding operation from the hero to the mouse point - # TODO: Highlight those points on the map + log.UI.info('Mouse point in window %s', mouse_point) + + hero = self.window.hero + if not hero: + return False + + map_point = self.window.convert_window_point_to_map(mouse_point) + log.UI.info('Mouse point in map %s', map_point) + + map_ = self.window.map + path = map_.find_walkable_path_from_point_to_point(hero.position, map_point) + map_.highlight_points(path) return False # pylint: disable=redefined-builtin - def __init__(self, bounds: Rect, map: Map, **kwargs): - super().__init__(bounds, **kwargs) + def __init__(self, bounds: Rect, map: Map, hero: Hero, **kwargs): + super().__init__(bounds, event_handler=self.__class__.EventHandler(self), **kwargs) self.map = map self.drawable_map_bounds = map.bounds + self.hero = hero self.entities: List[Entity] = [] self._draw_bounds = self.drawable_bounds - def update_drawable_map_bounds(self, hero: Hero): + def convert_window_point_to_map(self, point: Point) -> Point: + ''' + Convert a point in window coordinates to a point relative to the map's + origin point. + ''' + return point - Vector.from_point(self._draw_bounds.origin) + + def update_drawable_map_bounds(self): ''' Figure out what portion of the map is drawable and update `self.drawable_map_bounds`. This method attempts to keep the hero @@ -59,7 +76,7 @@ class MapWindow(Window): return # Attempt to keep the player centered in the viewport. - hero_point = hero.position + hero_point = self.hero.position if viewport_is_wider_than_map: x = 0 @@ -115,8 +132,6 @@ class MapWindow(Window): drawable_map_bounds = self.drawable_map_bounds drawable_bounds = self.drawable_bounds - log.UI.info('Drawing map') - map_slice = np.s_[ drawable_map_bounds.min_x: drawable_map_bounds.max_x + 1, drawable_map_bounds.min_y: drawable_map_bounds.max_y + 1] @@ -126,19 +141,12 @@ class MapWindow(Window): console_draw_bounds.min_x: console_draw_bounds.max_x + 1, console_draw_bounds.min_y: console_draw_bounds.max_y + 1] - log.UI.debug('Map bounds=%s, slice=%s', drawable_map_bounds, map_slice) - log.UI.debug('Console bounds=%s, slice=%s', drawable_bounds, console_slice) - console.tiles_rgb[console_slice] = self.map.composited_tiles[map_slice] - log.UI.info('Done drawing map') - def _draw_entities(self, console): map_bounds_vector = Vector.from_point(self.drawable_map_bounds.origin) draw_bounds_vector = Vector.from_point(self._draw_bounds.origin) - log.UI.info('Drawing entities') - for ent in self.entities: # Only draw entities that are in the field of view if not self.map.point_is_visible(ent.position): @@ -161,5 +169,3 @@ class MapWindow(Window): string=ent.symbol, fg=ent.foreground, bg=tuple(map_tile_at_entity_position['bg'][:3])) - - log.UI.info('Done drawing entities') diff --git a/erynrl/map/__init__.py b/erynrl/map/__init__.py index c7c71bd..7eb80f2 100644 --- a/erynrl/map/__init__.py +++ b/erynrl/map/__init__.py @@ -111,16 +111,13 @@ class Map: for pt in points: self.highlighted[pt.x, pt.y] = True - def print_to_console(self, console: tcod.Console, bounds: Rect) -> None: - '''Render the map to the console.''' - size = self.size - - # If a tile is in the visible array, draw it with the "light" color. If it's not, but it's in the explored - # array, draw it with the "dark" color. Otherwise, draw it as Empty. - console.tiles_rgb[0:size.width, 0:size.height] = np.select( - condlist=[self.highlighted, self.visible, self.explored], - choicelist=[self.tiles['highlighted'], self.tiles['light'], self.tiles['dark']], - default=Shroud) + def find_walkable_path_from_point_to_point(self, point_a: Point, point_b: Point) -> Iterable[Point]: + ''' + Find a path between point A and point B using tcod's A* implementation. + ''' + a_star = tcod.path.AStar(self.tiles['walkable']) + path = a_star.get_path(point_a.x, point_a.y, point_b.x, point_b.y) + return map(lambda t: Point(t[0], t[1]), path) def __str__(self): string = ''