text: from configparser import ConfigParser
  
  import pygame
  
  from settings import BASE_RES
  from utils import load_spritesheet_flat
  
  
  DIR_N = 'N'
  DIR_S = 'S'
  DIR_E = 'E'
  DIR_W = 'W'
  dir_vectors = {DIR_N: (0, -1), DIR_S: (0, 1), DIR_E: (1, 0), DIR_W: (-1, 0)}
  
  N_TILES = 16  # square levels of 16x16 tiles
  tile_size = BASE_RES // N_TILES
  
  
  def load_map(filename):
      # TODO: use JSON instead?
      tile_types = {}
      parser = ConfigParser()
      parser.read(filename)
      tiles = parser.get("level", "tiles").split("\n")
      for section in parser.sections():
          if len(section) == 1:  # single char = tile type
              tile_types[section] = dict(parser.items(section))
      return tiles, tile_types
          
          
  class Level():
      def __init__(self):
          self.tiles, self.tile_types = load_map('assets/level.map')
          w, h = len(self.tiles[0]), len(self.tiles)
          self.w, self.h = w, h
          self.characters = {}
          self.occupancy = [[None for _ in range(h)] for _ in range(w)]  # map pos to char
  
          
      def pre_render_map(self):
          tileset = load_spritesheet_flat('assets/tileset.png')
          bg = pygame.Surface((self.w * tile_size, self.h * tile_size))
          for map_y, line in enumerate(self.tiles):
              for map_x, tile_type in enumerate(line):
                  tileset_id = int(self.tile_types[tile_type]['tileset_id'])
                  tile_img = tileset[tileset_id]
                  bg.blit(tile_img, (map_x * tile_size, map_y * tile_size))
          return bg
      
      def get_terrain_penalty(self, pos):
          # return 1 if can't walk, 0 if full speed, 0.5 if half speed.
          x, y = pos
          tile_type = self.tiles[y][x]
          walkable = self.tile_types[tile_type]['walkable'] in ('true', 'True')
          if not walkable:
              penalty = 1 
          elif self.tile_types[tile_type]['name'] == 'grass': 
              penalty = 0.5  # 50% speed on grass
          else:
              penalty = 0
          return penalty
      
      
      def get_destination(self, cur_pos, direction):
          # return new position, delta to get there
          dx, dy = dir_vectors[direction]
          x, y = cur_pos
          newx, newy = x + dx, y + dy
          if newy < 0 or newy >= self.w or newx < 0 or newx >= self.h:
              return None, None
          else:
              return (newx, newy), (dx, dy)
          
      
      def get_occupancy(self, pos):
          x, y = pos
          target_char = self.occupancy[x][y]
          return target_char
      
  
      def move_character_to(self, char, pos):
          x, y = pos
          if char in self.characters.keys():
              oldx, oldy = self.characters[char]
              self.occupancy[oldx][oldy] = None
          self.occupancy[x][y] = char
          self.characters[char] = pos
          
  