Package tdl :: Module map |
[frames] | no frames]

# Source Code for Module tdl.map

      1  """
      2      Rogue-like map utilitys such as line-of-sight, field-of-view, and path-finding.
      3
      4  """
      5
      6  import itertools as _itertools
      7  import math as _math
      8
      9  from tcod import ffi as _ffi
     10  from tcod import lib as _lib
     11
     12  import tdl as _tdl
     13  from . import style as _style
     14
     15  _FOVTYPES = {'BASIC' : 0, 'DIAMOND': 1, 'SHADOW': 2, 'RESTRICTIVE': 12, 'PERMISSIVE': 11}
     16
     17 -def _get_fov_type(fov):
     18      "Return a FOV from a string"
     19      oldFOV = fov
     20      fov = str(fov).upper()
     21      if fov in _FOVTYPES:
     22          return _FOVTYPES[fov]
     23      if fov[:10] == 'PERMISSIVE' and fov[10].isdigit() and fov[10] != '9':
     24          return 4 + int(fov[10])
     25      raise _tdl.TDLError('No such fov option as %s' % oldFOV)
     26
     27 -class Map(object):
     28      """Fast field-of-view and path-finding on stored data.
     29
     30      Set map conditions with the walkable and transparency attributes, this
     31      object can be iterated and checked for containment similar to consoles.
     32
     33      For example, you can set all tiles and transparent and walkable with the
     34      following code::
     35
     36          map = tdl.map.Map(80, 60)
     37          for x,y in map:
     38              map.transparent[x,y] = true
     39              map.walkable[x,y] = true
     40
     41      @ivar transparent: Map transparency, access this attribute with
     42                         map.transparency[x,y]
     43
     44                         Set to True to allow field-of-view rays, False will
     45                         block field-of-view.
     46
     47                         Transparent tiles only affect field-of-view.
     48
     49      @ivar walkable: Map accessibility, access this attribute with
     50                      map.walkable[x,y]
     51
     52                      Set to True to allow path-finding through that tile,
     53                      False will block passage to that tile.
     54
     55                      Walkable tiles only affect path-finding.
     56
     57      @ivar fov: Map tiles touched by a field-of-view computation,
     58                 access this attribute with map.fov[x,y]
     59
     60                 Is True if a the tile is if view, otherwise False.
     61
     62                 You can set to this attribute if you want, but you'll typically
     63                 be using it to read the field-of-view of a L{compute_fov} call.
     64
     65      @since: 1.5.0
     66      """
     67
     68 -    class _MapAttribute(object):
     69 -        def __init__(self, map, bit_index):
     70              self.map = map
     71              self.bit_index = bit_index
     72              self.bit = 1 << bit_index
     73              self.bit_inverse = 0xFF ^ self.bit
     74
     75 -        def __getitem__(self, key):
     76              return bool(self.map._array_cdata[key[1]][key[0]] & self.bit)
     77
     78 -        def __setitem__(self, key, value):
     79              self.map._array_cdata[key[1]][key[0]] = (
     80                  (self.map._array_cdata[key[1]][key[0]] & self.bit_inverse) |
     81                  (self.bit * bool(value))
     82                  )
     83
     84 -    def __init__(self, width, height):
     85          """Create a new Map with width and height.
     86
     87          @type width: int
     88          @type height: int
     89          @param width: Width of the new Map instance, in tiles.
     90          @param width: Height of the new Map instance, in tiles.
     91          """
     92          self.width = width
     93          self.height = height
     94          self._map_cdata = _lib.TCOD_map_new(width, height)
     95          # cast array into cdata format: uint8[y][x]
     96          # for quick Python access
     97          self._array_cdata = _ffi.new('uint8[%i][%i]' % (width, height))
     98          # flat array to pass to TDL's C helpers
     99          self._array_cdata_flat = _ffi.cast('uint8 *', self._array_cdata)
    100          self.transparent = self._MapAttribute(self, 0)
    101          self.walkable = self._MapAttribute(self, 1)
    102          self.fov = self._MapAttribute(self, 2)
    103
    104 -    def __del__(self):
    105          if self._map_cdata:
    106              _lib.TCOD_map_delete(self._map_cdata)
    107              self._map_cdata = None
    108
    109 -    def compute_fov(self, x, y, fov='PERMISSIVE', radius=None, light_walls=True,
    110                      sphere=True, cumulative=False):
    111          """Compute the field-of-view of this Map and return an iterator of the
    112          points touched.
    113
    114          @type x: int
    115          @type y: int
    116
    117          @param x: x center of the field-of-view
    118          @param y: y center of the field-of-view
    119          @type fov: string
    120          @param fov: The type of field-of-view to be used.  Available types are:
    121
    122                      'BASIC', 'DIAMOND', 'SHADOW', 'RESTRICTIVE', 'PERMISSIVE',
    123                      'PERMISSIVE0', 'PERMISSIVE1', ..., 'PERMISSIVE8'
    124          @type radius: int
    125          @param radius: Raduis of the field-of-view.
    126          @type light_walls: boolean
    127          @param light_walls: Include or exclude wall tiles in the field-of-view.
    128          @type sphere: boolean
    129          @param sphere: True for a spherical field-of-view.
    130                         False for a square one.
    131          @type cumulative: boolean
    132          @param cumulative:
    133
    134          @rtype: iter((x, y), ...)
    135          @return: An iterator of (x, y) points of tiles touched by the
    136                   field-of-view.
    137
    138                   Unexpected behaviour can happen if you modify the Map while
    139                   using the iterator.
    140
    141                   You can use the Map's fov attribute as an alternative to this
    142                   iterator.
    143          """
    144          # refresh cdata
    145          _lib.TDL_map_data_from_buffer(self._map_cdata,
    146                                        self._array_cdata_flat)
    147          if radius is None: # infinite radius
    148              radius = max(self.width, self.height)
    149          _lib.TCOD_map_compute_fov(self.cdata, x, y, radius, light_walls,
    150                                    _get_fov_type(fov))
    151          _lib.TDL_map_fov_to_buffer(self._map_cdata,
    152                                     self._array_cdata_flat, cumulative)
    153          def iterate_fov():
    154              _array_cdata = self._array_cdata
    155              for y in range(self.width):
    156                  for x in range(self.height):
    157                      if(_array_cdata[y][x] & 4):
    158                          yield (x, y)
    159          return iterate_fov()
    160
    161
    162
    163 -    def compute_path(self, start_x, start_y, dest_x, dest_y,
    164                       diagonal_cost=_math.sqrt(2)):
    165          """Get the shortest path between two points.
    166
    167          The start position is not included in the list.
    168
    169          @type diagnalCost: float
    170          @param diagnalCost: Multiplier for diagonal movement.
    171
    172                              Can be set to zero to disable diagonal movement
    173                              entirely.
    174          @rtype: [(x, y), ...]
    175          @return: Returns a the shortest list of points to get to the destination
    176                   position from the starting position
    177          """
    178          # refresh cdata
    179          _lib.TDL_map_data_from_buffer(self._map_cdata,
    180                                        self._array_cdata_flat)
    181          path_cdata = _lib.TCOD_path_new_using_map(self._map_cdata, diagonal_cost)
    182          try:
    183              _lib.TCOD_path_compute(path_cdata, start_x, start_y, dest_x, dest_y)
    184              x = _ffi.new('int *')
    185              y = _ffi.new('int *')
    186              length = _lib.TCOD_path_size(path_cdata)
    187              path = [None] * length
    188              for i in range(length):
    189                  _lib.TCOD_path_get(path_cdata, i, x, y)
    190                  path[i] = ((x[0], y[0]))
    191          finally:
    192              _lib.TCOD_path_delete(path_cdata)
    193          return path
    194
    195 -    def __iter__(self):
    196          return _itertools.product(range(self.width), range(self.height))
    197
    198 -    def __contains__(self, position):
    199          x, y = position
    200          return (0 <= x < self.width) and (0 <= y < self.height)
    201
    202
    203
    204 -class AStar(object):
    205      """A* pathfinder
    206
    207      Using this class requires a callback detailed in L{AStar.__init__}
    208
    209      @undocumented: getPath
    210      """
    211
    212      __slots__ = ('_as_parameter_', '_callback', '__weakref__')
    213
    214
    215
    216 -    def __init__(self, width, height, callback,
    217                   diagnalCost=_math.sqrt(2), advanced=False):
    218          """Create an A* pathfinder using a callback.
    219
    220          Before crating this instance you should make one of two types of
    221          callbacks:
    222           - A function that returns the cost to move to (x, y)
    223          or
    224           - A function that returns the cost to move between
    225             (destX, destY, sourceX, sourceY)
    226          If path is blocked the function should return zero or None.
    227          When using the second type of callback be sure to set advanced=True
    228
    229          @type width: int
    230          @param width: width of the pathfinding area in tiles
    231          @type height: int
    232          @param height: height of the pathfinding area in tiles
    233
    234          @type callback: function
    235          @param callback: A callback taking parameters depending on the setting
    236                           of 'advanced' and returning the cost of
    237                           movement for an open tile or zero for a
    238                           blocked tile.
    239
    240          @type diagnalCost: float
    241          @param diagnalCost: Multiplier for diagonal movement.
    242
    243                              Can be set to zero to disable diagonal movement
    244                              entirely.
    245
    246          @type advanced: boolean
    247          @param advanced: A simple callback with 2 positional parameters may not
    248                           provide enough information.  Setting this to True will
    249                           call the callback with 2 additional parameters giving
    250                           you both the destination and the source of movement.
    251
    252                           When True the callback will need to accept
    253                           (destX, destY, sourceX, sourceY) as parameters.
    254                           Instead of just (destX, destY).
    255
    256          """
    257          if not diagnalCost: # set None or False to zero
    258              diagnalCost = 0.0
    259          if advanced:
    260              def newCallback(sourceX, sourceY, destX, destY, null):
    261                  pathCost = callback(destX, destY, sourceX, sourceY)
    262                  if pathCost:
    263                      return pathCost
    264                  return 0.0
    265          else:
    266              def newCallback(sourceX, sourceY, destX, destY, null):
    267                  pathCost = callback(destX, destY) # expecting a float or 0
    268                  if pathCost:
    269                      return pathCost
    270                  return 0.0
    271          # float(int, int, int, int, void*)
    272          self._callback = _ffi.callback('TCOD_path_func_t')(newCallback)
    273
    274          self._as_parameter_ = _lib.TCOD_path_new_using_function(width, height,
    275                                       self._callback, _ffi.NULL, diagnalCost)
    276
    277 -    def __del__(self):
    278          if self._as_parameter_:
    279              _lib.TCOD_path_delete(self._as_parameter_)
    280              self._as_parameter_ = None
    281
    282 -    def get_path(self, origX, origY, destX, destY):
    283          """
    284          Get the shortest path from origXY to destXY.
    285
    286          @rtype: [(x, y), ...]
    287          @return: Returns a list walking the path from origXY to destXY.
    288                   This excludes the starting point and includes the destination.
    289
    290                   If no path is found then an empty list is returned.
    291          """
    292          found = _lib.TCOD_path_compute(self._as_parameter_, origX, origY, destX, destY)
    293          if not found:
    294              return [] # path not found
    295          x, y = _ffi.new('int *'), _ffi.new('int *')
    296          recalculate = True
    297          path = []
    298          while _lib.TCOD_path_walk(self._as_parameter_, x, y, recalculate):
    299              path.append((x[0], y[0]))
    300          return path
    301
    302 -def quick_fov(x, y, callback, fov='PERMISSIVE', radius=7.5, lightWalls=True, sphere=True):
    303      """All field-of-view functionality in one call.
    304
    305      Before using this call be sure to make a function, lambda, or method that takes 2
    306      positional parameters and returns True if light can pass through the tile or False
    307      for light-blocking tiles and for indexes that are out of bounds of the
    308      dungeon.
    309
    310      This function is 'quick' as in no hassle but can quickly become a very slow
    311      function call if a large radius is used or the callback provided itself
    312      isn't optimized.
    313
    314      Always check if the index is in bounds both in the callback and in the
    315      returned values.  These values can go into the negatives as well.
    316
    317      @type x: int
    318      @param x: x center of the field-of-view
    319      @type y: int
    320      @param y: y center of the field-of-view
    321      @type callback: function
    322      @param callback: This should be a function that takes two positional arguments x,y
    323                       and returns True if the tile at that position is transparent
    324                       or False if the tile blocks light or is out of bounds.
    325      @type fov: string
    326      @param fov: The type of field-of-view to be used.  Available types are:
    327
    328                  'BASIC', 'DIAMOND', 'SHADOW', 'RESTRICTIVE', 'PERMISSIVE',
    329                  'PERMISSIVE0', 'PERMISSIVE1', ..., 'PERMISSIVE8'
    330      @type radius: float
    331      @param radius: Raduis of the field-of-view.
    332
    333                     When sphere is True a floating point can be used to fine-tune
    334                     the range.  Otherwise the radius is just rounded up.
    335
    336                     Be careful as a large radius has an exponential affect on
    337                     how long this function takes.
    338      @type lightWalls: boolean
    339      @param lightWalls: Include or exclude wall tiles in the field-of-view.
    340      @type sphere: boolean
    341      @param sphere: True for a spherical field-of-view.  False for a square one.
    342
    343      @rtype: set((x, y), ...)
    344      @return: Returns a set of (x, y) points that are within the field-of-view.
    345      """
    346      trueRadius = radius
    347      radius = int(_math.ceil(radius))
    348      mapSize = radius * 2 + 1
    349      fov = _get_fov_type(fov)
    350
    351      setProp = _lib.TCOD_map_set_properties # make local
    352      inFOV = _lib.TCOD_map_is_in_fov
    353
    354      tcodMap = _lib.TCOD_map_new(mapSize, mapSize)
    355      try:
    356          # pass no.1, write callback data to the tcodMap
    357          for x_, y_ in _itertools.product(range(mapSize), range(mapSize)):
    358              pos = (x_ + x - radius,
    359                     y_ + y - radius)
    360              transparent = bool(callback(*pos))
    361              setProp(tcodMap, x_, y_, transparent, False)
    362
    363          # pass no.2, compute fov and build a list of points
    364          _lib.TCOD_map_compute_fov(tcodMap, radius, radius, radius, lightWalls, fov)
    365          touched = set() # points touched by field of view
    366          for x_, y_ in _itertools.product(range(mapSize), range(mapSize)):
    367              if sphere and _math.hypot(x_ - radius, y_ - radius) > trueRadius:
    368                  continue
    369              if inFOV(tcodMap, x_, y_):
    370                  touched.add((x_ + x - radius, y_ + y - radius))
    371      finally:
    372          _lib.TCOD_map_delete(tcodMap)
    373      return touched
    374
    375 -def bresenham(x1, y1, x2, y2):
    376      """
    377      Return a list of points in a bresenham line.
    378
    379      Implementation hastily copied from RogueBasin.
    380
    381      @return: Returns a list of (x, y) points, including both the start and
    382               endpoints.
    383      """
    384      points = []
    385      issteep = abs(y2-y1) > abs(x2-x1)
    386      if issteep:
    387          x1, y1 = y1, x1
    388          x2, y2 = y2, x2
    389      rev = False
    390      if x1 > x2:
    391          x1, x2 = x2, x1
    392          y1, y2 = y2, y1
    393          rev = True
    394      deltax = x2 - x1
    395      deltay = abs(y2-y1)
    396      error = int(deltax / 2)
    397      y = y1
    398      ystep = None
    399      if y1 < y2:
    400          ystep = 1
    401      else:
    402          ystep = -1
    403      for x in range(x1, x2 + 1):
    404          if issteep:
    405              points.append((y, x))
    406          else:
    407              points.append((x, y))
    408          error -= deltay
    409          if error < 0:
    410              y += ystep
    411              error += deltax
    412      # Reverse the list if the coordinates were reversed
    413      if rev:
    414          points.reverse()
    415      return points
    416
    417
    418  __all__ = [_var for _var in locals().keys() if _var[0] != '_']
    419
    420  quickFOV = _style.backport(quick_fov)
    421  AStar.getPath = _style.backport(AStar.get_path)
    422