From 3795deac19be4816d54829ed2e728e78f57f86de Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Tue, 17 Nov 2020 22:08:31 +0100
Subject: [PATCH] Shrink FOV map to radius.

---
 plomrogue/commands.py | 86 ++++++++++++++++++++++++-------------------
 plomrogue/game.py     | 16 ++++----
 plomrogue/mapping.py  | 74 ++++++++++++++++++++++++-------------
 plomrogue/things.py   | 14 +++++--
 4 files changed, 116 insertions(+), 74 deletions(-)

diff --git a/plomrogue/commands.py b/plomrogue/commands.py
index 643988e..c52dd4f 100644
--- a/plomrogue/commands.py
+++ b/plomrogue/commands.py
@@ -39,35 +39,42 @@ def cmd_ALL(game, msg, connection_id):
             lowered_msg += c
         return lowered_msg
 
+    def dijkstra(speaker):
+        n_max = 255
+
+        map_size = game.map.size_i
+        dijkstra_map = [n_max for i in range(game.map.size_i)]
+        dijkstra_map[game.map.get_position_index(speaker.position)] = 0
+
+        shrunk = True
+        while shrunk:
+            shrunk = False
+            for i in range(map_size):
+                if game.map.terrain[i] == 'X':
+                    continue
+                neighbors = game.map_geometry.get_neighbors_i(i)
+                for direction in [d for d in neighbors if neighbors[d]]:
+                    j = neighbors[direction]
+                    if dijkstra_map[j] < dijkstra_map[i] - 1:
+                        dijkstra_map[i] = dijkstra_map[j] + 1
+                        shrunk = True
+        #print('DEBUG')
+        #line_to_print = []
+        #x = 0
+        #for n in dijkstra_map:
+        #    line_to_print += ['%3s' % n]
+        #    x += 1
+        #    if x >= game.map.size.x:
+        #        x = 0
+        #        print(' '.join(line_to_print))
+        #        line_to_print = []
+        return dijkstra_map
+
     import random
     if not connection_id in game.sessions:
         raise GameError('need to be logged in for this')
     speaker = game.get_thing(game.sessions[connection_id])
-    n_max = 255
-    map_size = game.map.size_i
-    dijkstra_map = [n_max for i in range(game.map.size_i)]
-    dijkstra_map[game.map.get_position_index(speaker.position)] = 0
-    shrunk = True
-    while shrunk:
-        shrunk = False
-        for i in range(map_size):
-            if game.map.terrain[i] == 'X':
-                continue
-            neighbors = game.map_geometry.get_neighbors_i(i)
-            for direction in [d for d in neighbors if neighbors[d]]:
-                j = neighbors[direction]
-                if dijkstra_map[j] < dijkstra_map[i] - 1:
-                    dijkstra_map[i] = dijkstra_map[j] + 1
-                    shrunk = True
-    #print('DEBUG')
-    #line_to_print = []
-    #x = 0
-    #for n in dijkstra_map:
-    #    line_to_print += ['%3s' % n]
-    #    x += 1
-    #    if x >= game.map.size.x:
-    #        x = 0
-    #        print(' '.join(line_to_print))
+    dijkstra_map = dijkstra(speaker)
     for c_id in game.sessions:
         listener = game.get_thing(game.sessions[c_id])
         listener_vol = dijkstra_map[game.map.get_position_index(listener.position)]
@@ -139,29 +146,31 @@ cmd_TURN.argtypes = 'int:nonneg'
 
 def cmd_ANNOTATE(game, yx, msg, pw, connection_id):
     player = game.get_thing(game.sessions[connection_id])
-    if player.fov_stencil[yx] != '.':
+    corrected_yx = yx + player.fov_stencil.offset
+    if not player.fov_test(corrected_yx):
         raise GameError('cannot annotate tile outside field of view')
-    if not game.can_do_tile_with_pw(yx, pw):
+    if not game.can_do_tile_with_pw(corrected_yx, pw):
         raise GameError('wrong password for tile')
     if msg == ' ':
-        if yx in game.annotations:
-            del game.annotations[yx]
+        if corrected_yx in game.annotations:
+            del game.annotations[corrected_yx]
     else:
-        game.annotations[yx] = msg
+        game.annotations[corrected_yx] = msg
     game.changed = True
 cmd_ANNOTATE.argtypes = 'yx_tuple:nonneg string string'
 
 def cmd_PORTAL(game, yx, msg, pw, connection_id):
     player = game.get_thing(game.sessions[connection_id])
-    if player.fov_stencil[yx] != '.':
+    corrected_yx = yx + player.fov_stencil.offset
+    if not player.fov_test(corrected_yx):
         raise GameError('cannot edit portal on tile outside field of view')
-    if not game.can_do_tile_with_pw(yx, pw):
+    if not game.can_do_tile_with_pw(corrected_yx, pw):
         raise GameError('wrong password for tile')
     if msg == ' ':
-        if yx in game.portals:
-            del game.portals[yx]
+        if corrected_yx in game.portals:
+            del game.portals[corrected_yx]
     else:
-        game.portals[yx] = msg
+        game.portals[corrected_yx] = msg
     game.changed = True
 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string string'
 
@@ -177,11 +186,12 @@ cmd_GOD_PORTAL.argtypes = 'yx_tuple:nonneg string'
 
 def cmd_GET_ANNOTATION(game, yx, connection_id):
     player = game.get_thing(game.sessions[connection_id])
+    corrected_yx = yx + player.fov_stencil.offset
     annotation = '(unknown)';
-    if player.fov_stencil[yx] == '.':
+    if player.fov_test(corrected_yx):
         annotation = '(none)';
-        if yx in game.annotations:
-            annotation = game.annotations[yx]
+        if corrected_yx in game.annotations:
+            annotation = game.annotations[corrected_yx]
     game.io.send('ANNOTATION %s %s' % (yx, quote(annotation)))
 cmd_GET_ANNOTATION.argtypes = 'yx_tuple:nonneg'
 
diff --git a/plomrogue/game.py b/plomrogue/game.py
index 7eaa28b..38a607e 100755
--- a/plomrogue/game.py
+++ b/plomrogue/game.py
@@ -113,21 +113,23 @@ class Game(GameBase):
             visible_terrain = player.fov_stencil_map(self.map)
             self.io.send('FOV %s' % quote(player.fov_stencil.terrain), c_id)
             self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(),
-                                           self.map_geometry.size,
+                                           player.fov_stencil.size,
                                            quote(visible_terrain)), c_id)
             visible_control = player.fov_stencil_map(self.map_control)
             self.io.send('MAP_CONTROL %s' % quote(visible_control), c_id)
-            for t in [t for t in self.things
-                      if player.fov_stencil[t.position] == '.']:
-                self.io.send('THING %s %s %s' % (t.position, t.type_, t.id_), c_id)
+            for t in [t for t in self.things if player.fov_test(t.position)]:
+                corrected_yx = t.position - player.fov_stencil.offset
+                self.io.send('THING %s %s %s' % (corrected_yx, t.type_, t.id_),
+                             c_id)
                 if hasattr(t, 'name'):
                     self.io.send('THING_NAME %s %s' % (t.id_, quote(t.name)), c_id)
                 if hasattr(t, 'player_char'):
                     self.io.send('THING_CHAR %s %s' % (t.id_,
                                                        quote(t.player_char)), c_id)
-            for yx in [yx for yx in self.portals
-                       if player.fov_stencil[yx] == '.']:
-                self.io.send('PORTAL %s %s' % (yx, quote(self.portals[yx])), c_id)
+            for yx in [yx for yx in self.portals if player.fov_test(yx)]:
+                corrected_yx = yx - player.fov_stencil.offset
+                self.io.send('PORTAL %s %s' % (corrected_yx,
+                                               quote(self.portals[yx])), c_id)
         self.io.send('GAME_STATE_COMPLETE')
 
     def run_tick(self):
diff --git a/plomrogue/mapping.py b/plomrogue/mapping.py
index 01c10e8..9fecffb 100644
--- a/plomrogue/mapping.py
+++ b/plomrogue/mapping.py
@@ -141,6 +141,12 @@ class Map():
             for x in range(self.size.x):
                 yield YX(y, x)
 
+    # TODO: use this for more refactoring
+    def inside(self, yx):
+        if yx.y < 0 or yx.x < 0 or yx.y >= self.size.y or yx.x >= self.size.x:
+            return False
+        return True
+
     @property
     def size_i(self):
         return self.size.y * self.size.x
@@ -167,21 +173,30 @@ class Map():
 
 
 class FovMap(Map):
-    # FIXME: player visibility asymmetrical (A can see B when B can't see A)
+    # TODO: player visibility asymmetrical (A can see B when B can't see A):
+    # does this make sense, or not?
 
-    def __init__(self, source_map, center):
+    def __init__(self, source_map, source_center):
         self.source_map = source_map
-        self.size = self.source_map.size
-        self.fov_radius = 12 # (self.size.y / 2) - 0.5
-        self.start_indented = True  #source_map.start_indented
-        self.terrain = '?' * self.size_i
-        self.center = center
+        self.fov_radius = 12
+        self.set_size_offset_center(source_center)
+        self.terrain = '?' * self.size.y * self.size.x
         self[self.center] = '.'
-        self.shadow_cones = []
         self.geometry = self.geometry_class(self.size)
+        self[self.center] = '.'
+        self.shadow_cones = []
         self.circle_out(self.center, self.shadow_process)
 
-    def shadow_process(self, yx, distance_to_center, dir_i, dir_progress):
+    def throws_shadow(self, source_yx):
+        return self.source_map[source_yx] == 'X'
+
+    def source_yx(self, yx):
+        source_yx = yx + self.offset
+        if not self.source_map.inside(source_yx):
+            return False
+        return source_yx
+
+    def shadow_process(self, yx, source_yx, distance_to_center, dir_i, dir_progress):
         # Possible optimization: If no shadow_cones yet and self[yx] == '.',
         # skip all.
         CIRCLE = 360  # Since we'll float anyways, number is actually arbitrary.
@@ -221,7 +236,7 @@ class FovMap(Map):
             if in_shadow_cone(cone):
                 return
             self[yx] = '.'
-            if self.source_map[yx] == 'X':
+            if self.throws_shadow(source_yx):
                 unmerged = True
                 while merge_cone(cone):
                     unmerged = False
@@ -242,13 +257,9 @@ class FovMap(Map):
             eval_cone([left_arm, right_arm])
 
     def basic_circle_out_move(self, pos, direction):
-        """Move position pos into direction. Return whether still in map."""
+        #"""Move position pos into direction. Return whether still in map."""
         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
+        return mover(pos)
 
     def circle_out(self, yx, f):
         # Optimization potential: Precalculate movement positions. (How to check
@@ -260,18 +271,15 @@ class FovMap(Map):
         circle_in_map = True
         distance = 1
         yx = YX(yx.y, yx.x)
-        while circle_in_map:
-            if distance > self.fov_radius:
-                break
-            circle_in_map = False
-            yx, _ = self.basic_circle_out_move(yx, 'RIGHT')
+        while distance <= self.fov_radius:
+            yx = self.basic_circle_out_move(yx, 'RIGHT')
             for dir_i in range(len(self.circle_out_directions)):
                 for dir_progress in range(distance):
                     direction = self.circle_out_directions[dir_i]
-                    yx, test = self.circle_out_move(yx, direction)
-                    if test:
-                        f(yx, distance, dir_i, dir_progress)
-                        circle_in_map = True
+                    yx = self.circle_out_move(yx, direction)
+                    source_yx = self.source_yx(yx)
+                    if source_yx:
+                        f(yx, source_yx, distance, dir_i, dir_progress)
             distance += 1
 
 
@@ -281,6 +289,14 @@ class FovMapHex(FovMap):
                              'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
     geometry_class = MapGeometryHex
 
+    def set_size_offset_center(self, source_center):
+        indent = 1 if (source_center.y % 2) else 0
+        self.size = YX(2 * self.fov_radius + 1 + indent,
+                       2 * self.fov_radius + 1)
+        self.offset = YX(source_center.y - self.fov_radius - indent,
+                         source_center.x - self.fov_radius)
+        self.center = YX(self.fov_radius + indent, self.fov_radius)
+
     def circle_out_move(self, yx, direction):
         return self.basic_circle_out_move(yx, direction)
 
@@ -291,6 +307,12 @@ class FovMapSquare(FovMap):
                              ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
     geometry_class = MapGeometrySquare
 
+    def set_size_offset_center(self, source_center):
+        self.size = YX(2 * self.fov_radius + 1, 2 * self.fov_radius + 1)
+        self.offset = YX(source_center.y - self.fov_radius,
+                         source_center.x - self.fov_radius)
+        self.center = YX(self.fov_radius, self.fov_radius)
+
     def circle_out_move(self, yx, direction):
-        yx, _ = self.basic_circle_out_move(yx, direction[0])
+        yx = self.basic_circle_out_move(yx, direction[0])
         return self.basic_circle_out_move(yx, direction[1])
diff --git a/plomrogue/things.py b/plomrogue/things.py
index 0a06601..da5cf77 100644
--- a/plomrogue/things.py
+++ b/plomrogue/things.py
@@ -97,11 +97,19 @@ class ThingAnimate(Thing):
         self._fov = fov_map_class(self.game.map, self.position)
         return self._fov
 
+    def fov_test(self, yx):
+        test_position = yx - self.fov_stencil.offset
+        if self.fov_stencil.inside(test_position):
+            if self.fov_stencil[test_position] == '.':
+                return True
+        return False
+
     def fov_stencil_map(self, map):
         visible_terrain = ''
-        for i in range(self.fov_stencil.size_i):
-            if self.fov_stencil.terrain[i] == '.':
-                visible_terrain += map.terrain[i]
+        for yx in self.fov_stencil:
+            if self.fov_stencil[yx] == '.':
+                corrected_yx = yx + self.fov_stencil.offset
+                visible_terrain += map[corrected_yx]
             else:
                 visible_terrain += ' '
         return visible_terrain
-- 
2.30.2