home · contact · privacy
Decouple awakeness/sleep stats from Map to MapChunk.
[plomrogue2-experiments] / new / plomrogue / mapping.py
index 64dad7c8a5ac0490c1f440dc3909876de4d5aa36..b670938e64658e2ff34306483bdb6cb7adf12d77 100644 (file)
@@ -1,33 +1,27 @@
 from plomrogue.errors import ArgError
+import collections
 
 
 
-class MapBase:
+class YX(collections.namedtuple('YX', ('y', 'x'))):
 
-    def __init__(self, size=(0, 0)):
-        self.size = size
-        self.terrain = '?'*self.size_i
+    def __add__(self, other):
+        return YX(self.y + other.y, self.x + other.x)
 
-    @property
-    def size_i(self):
-        return self.size[0] * self.size[1]
+    def __sub__(self, other):
+        return YX(self.y - other.y, self.x - other.x)
+
+    def __str__(self):
+        return 'Y:%s,X:%s' % (self.y, self.x)
 
-    def set_line(self, y, line):
-        height_map = self.size[0]
-        width_map = self.size[1]
-        if y >= height_map:
-            raise ArgError('too large row number %s' % y)
-        width_line = len(line)
-        if width_line > width_map:
-            raise ArgError('too large map line width %s' % width_line)
-        self.terrain = self.terrain[:y * width_map] + line +\
-                       self.terrain[(y + 1) * width_map:]
 
-    def get_position_index(self, yx):
-        return yx[0] * self.size[1] + yx[1]
 
+class Map:
 
-class Map(MapBase):
+    def __init__(self, size=YX(0, 0), init_char = '?', start_indented=True):
+        self.size = size
+        self.terrain = init_char * self.size_i
+        self.start_indented = start_indented
 
     def __getitem__(self, yx):
         return self.terrain[self.get_position_index(yx)]
@@ -41,17 +35,42 @@ class Map(MapBase):
 
     def __iter__(self):
         """Iterate over YX position coordinates."""
-        for y in range(self.size[0]):
-            for x in range(self.size[1]):
-                yield (y, x)
+        for y in range(self.size.y):
+            for x in range(self.size.x):
+                yield YX(y, x)
+
+    @property
+    def size_i(self):
+        return self.size.y * self.size.x
+
+    def set_line(self, y, line):
+        height_map = self.size.y
+        width_map = self.size.x
+        if y >= height_map:
+            raise ArgError('too large row number %s' % y)
+        width_line = len(line)
+        if width_line > width_map:
+            raise ArgError('too large map line width %s' % width_line)
+        self.terrain = self.terrain[:y * width_map] + line +\
+                       self.terrain[(y + 1) * width_map:]
+
+    def get_position_index(self, yx):
+        return yx.y * self.size.x + yx.x
 
     def lines(self):
-        width = self.size[1]
-        for y in range(self.size[0]):
+        width = self.size.x
+        for y in range(self.size.y):
             yield (y, self.terrain[y * width:(y + 1) * width])
 
-    def get_fov_map(self, yx):
-        return self.fov_map_type(self, yx)
+
+
+class MapChunk(Map):
+    awake = 0  # asleep if zero
+    stats = {}
+
+
+
+class MapGeometry():
 
     def get_directions(self):
         directions = []
@@ -60,100 +79,133 @@ class Map(MapBase):
                 directions += [name[5:]]
         return directions
 
-    def get_neighbors(self, pos):
+    def get_neighbors(self, pos, map_size, start_indented=True):
         neighbors = {}
-        pos = tuple(pos)
         if not hasattr(self, 'neighbors_to'):
             self.neighbors_to = {}
-        if pos in self.neighbors_to:
-            return self.neighbors_to[pos]
+        if not map_size in self.neighbors_to:
+            self.neighbors_to[map_size] = {}
+        if not start_indented in self.neighbors_to[map_size]:
+            self.neighbors_to[map_size][start_indented] = {}
+        if pos in self.neighbors_to[map_size][start_indented]:
+            return self.neighbors_to[map_size][start_indented][pos]
         for direction in self.get_directions():
-            neighbors[direction] = None
-            try:
-                neighbors[direction] = self.move(pos, direction)
-            except GameError:
-                pass
-        self.neighbors_to[pos] = neighbors
+            neighbors[direction] = self.move(pos, direction, map_size,
+                                             start_indented)
+        self.neighbors_to[map_size][start_indented][pos] = neighbors
         return neighbors
 
-    def new_from_shape(self, init_char):
-        import copy
-        new_map = copy.deepcopy(self)
-        for pos in new_map:
-            new_map[pos] = init_char
-        return new_map
-
-    def move(self, start_pos, direction):
+    def undouble_coordinate(self, maps_size, coordinate):
+        y = maps_size.y * coordinate[0].y + coordinate[1].y
+        x = maps_size.x * coordinate[0].x + coordinate[1].x
+        return YX(y, x)
+
+    def get_view_offset(self, maps_size, center, radius):
+        yx_to_origin = self.undouble_coordinate(maps_size, center)
+        return yx_to_origin - YX(radius, radius)
+
+    def pos_in_view(self, pos, offset, maps_size):
+        return self.undouble_coordinate(maps_size, pos) - offset
+
+    def get_view_and_seen_maps(self, maps_size, get_map, radius, view_offset):
+        m = Map(size=YX(radius*2+1, radius*2+1),
+                start_indented=(view_offset.y % 2 == 0))
+        seen_maps = []
+        for pos in m:
+            seen_pos = self.correct_double_coordinate(maps_size, (0,0),
+                                                      pos + view_offset)
+            if seen_pos[0] not in seen_maps:
+                seen_maps += [seen_pos[0]]
+            seen_map = get_map(seen_pos[0])
+            if seen_map is None:
+                seen_map = Map(size=maps_size)
+            m[pos] = seen_map[seen_pos[1]]
+        return m, seen_maps
+
+    def correct_double_coordinate(self, map_size, big_yx, little_yx):
+
+        def adapt_axis(axis):
+            maps_crossed = little_yx[axis] // map_size[axis]
+            new_big = big_yx[axis] + maps_crossed
+            new_little = little_yx[axis] % map_size[axis]
+            return new_big, new_little
+
+        new_big_y, new_little_y = adapt_axis(0)
+        new_big_x, new_little_x = adapt_axis(1)
+        return YX(new_big_y, new_big_x), YX(new_little_y, new_little_x)
+
+    def move(self, start_pos, direction, map_size, start_indented=True):
         mover = getattr(self, 'move_' + direction)
-        new_pos = mover(start_pos)
-        if new_pos[0] < 0 or new_pos[1] < 0 or \
-                new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
-            raise GameError('would move outside map bounds')
-        return new_pos
+        big_yx, little_yx = start_pos
+        uncorrected_target = mover(little_yx, start_indented)
+        return self.correct_double_coordinate(map_size, big_yx,
+                                              uncorrected_target)
 
 
 
-class MapWithLeftRightMoves(Map):
+class MapGeometryWithLeftRightMoves(MapGeometry):
 
-    def move_LEFT(self, start_pos):
-        return (start_pos[0], start_pos[1] - 1)
+    def move_LEFT(self, start_pos, _):
+        return YX(start_pos.y, start_pos.x - 1)
 
-    def move_RIGHT(self, start_pos):
-        return (start_pos[0], start_pos[1] + 1)
+    def move_RIGHT(self, start_pos, _):
+        return YX(start_pos.y, start_pos.x + 1)
 
 
 
-class MapSquare(MapWithLeftRightMoves):
+class MapGeometrySquare(MapGeometryWithLeftRightMoves):
 
-    def move_UP(self, start_pos):
-        return (start_pos[0] - 1, start_pos[1])
+    def move_UP(self, start_pos, _):
+        return YX(start_pos.y - 1, start_pos.x)
 
-    def move_DOWN(self, start_pos):
-        return (start_pos[0] + 1, start_pos[1])
+    def move_DOWN(self, start_pos, _):
+        return YX(start_pos.y + 1, start_pos.x)
 
 
 
-class MapHex(MapWithLeftRightMoves):
+class MapGeometryHex(MapGeometryWithLeftRightMoves):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.fov_map_type = FovMapHex
 
-    def move_UPLEFT(self, start_pos):
-        if start_pos[0] % 2 == 1:
-            return (start_pos[0] - 1, start_pos[1] - 1)
+    def move_UPLEFT(self, start_pos, start_indented):
+        if start_pos.y % 2 == start_indented:
+            return YX(start_pos.y - 1, start_pos.x - 1)
         else:
-            return (start_pos[0] - 1, start_pos[1])
+            return YX(start_pos.y - 1, start_pos.x)
 
-    def move_UPRIGHT(self, start_pos):
-        if start_pos[0] % 2 == 1:
-            return (start_pos[0] - 1, start_pos[1])
+    def move_UPRIGHT(self, start_pos, start_indented):
+        if start_pos.y % 2 == start_indented:
+            return YX(start_pos.y - 1, start_pos.x)
         else:
-            return (start_pos[0] - 1, start_pos[1] + 1)
+            return YX(start_pos.y - 1, start_pos.x + 1)
 
-    def move_DOWNLEFT(self, start_pos):
-        if start_pos[0] % 2 == 1:
-             return (start_pos[0] + 1, start_pos[1] - 1)
+    def move_DOWNLEFT(self, start_pos, start_indented):
+        if start_pos.y % 2 == start_indented:
+             return YX(start_pos.y + 1, start_pos.x - 1)
         else:
-               return (start_pos[0] + 1, start_pos[1])
+               return YX(start_pos.y + 1, start_pos.x)
 
-    def move_DOWNRIGHT(self, start_pos):
-        if start_pos[0] % 2 == 1:
-            return (start_pos[0] + 1, start_pos[1])
+    def move_DOWNRIGHT(self, start_pos, start_indented):
+        if start_pos.y % 2 == start_indented:
+            return YX(start_pos.y + 1, start_pos.x)
         else:
-            return (start_pos[0] + 1, start_pos[1] + 1)
+            return YX(start_pos.y + 1, start_pos.x + 1)
 
 
 
-class FovMap:
+class FovMap(Map):
 
-    def __init__(self, source_map, yx):
+    def __init__(self, source_map, center):
         self.source_map = source_map
         self.size = self.source_map.size
+        self.fov_radius = (self.size.y / 2) - 0.5
+        self.start_indented = source_map.start_indented
         self.terrain = '?' * self.size_i
-        self[yx] = '.'
+        self[center] = '.'
         self.shadow_cones = []
-        self.circle_out(yx, self.shadow_process_hex)
+        self.circle_out(center, self.shadow_process_hex)
 
     def shadow_process_hex(self, yx, distance_to_center, dir_i, dir_progress):
         # Possible optimization: If no shadow_cones yet and self[yx] == '.',
@@ -224,10 +276,10 @@ class FovMap:
 
     def basic_circle_out_move(self, pos, direction):
         """Move position pos into direction. Return whether still in map."""
-        mover = getattr(self, 'move_' + direction)
-        pos = mover(pos)
-        if pos[0] < 0 or pos[1] < 0 or \
-            pos[0] >= self.size[0] or pos[1] >= self.size[1]:
+        mover = getattr(self.geometry, 'move_' + direction)
+        pos = mover(pos, self.start_indented)
+        if pos.y < 0 or pos.x < 0 or \
+            pos.y >= self.size.y or pos.x >= self.size.x:
             return pos, False
         return pos, True
 
@@ -240,11 +292,14 @@ class FovMap:
         # would lose shade growth through hexes at shade borders.)
 
         # TODO: Start circling only in earliest obstacle distance.
+        # TODO: get rid of circle_in_map logic
         circle_in_map = True
         distance = 1
-        yx = yx[:]
+        yx = YX(yx.y, yx.x)
         #print('DEBUG CIRCLE_OUT', yx)
         while circle_in_map:
+            if distance > self.fov_radius:
+                break
             circle_in_map = False
             yx, _ = self.basic_circle_out_move(yx, 'RIGHT')
             for dir_i in range(len(self.circle_out_directions)):
@@ -258,19 +313,28 @@ class FovMap:
 
 
 
-class FovMapHex(FovMap, MapHex):
+class FovMapHex(FovMap):
     circle_out_directions = ('DOWNLEFT', 'LEFT', 'UPLEFT',
                              'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
 
+    def __init__(self, *args, **kwargs):
+        self.geometry = MapGeometryHex()
+        super().__init__(*args, **kwargs)
+
     def circle_out_move(self, yx, direction):
         return self.basic_circle_out_move(yx, direction)
 
 
 
-class FovMapSquare(FovMap, MapSquare):
+class FovMapSquare(FovMap):
     circle_out_directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'),
                              ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
 
+    def __init__(self, *args, **kwargs):
+        self.geometry = MapGeometrySquare()
+        super().__init__(*args, **kwargs)
+
     def circle_out_move(self, yx, direction):
         self.basic_circle_out_move(yx, direction[0])
         return self.basic_circle_out_move(yx, direction[1])
+