Difference between revisions of "Complete Roguelike Tutorial, using python3+libtcod, part 2 code"

From RogueBasin
Jump to navigation Jump to search
Line 281: Line 281:


<div style="background-color: #EEEEEE; border-style: dotted"><syntaxhighlight lang="python">
<div style="background-color: #EEEEEE; border-style: dotted"><syntaxhighlight lang="python">
import libtcodpy as libtcod
#!/usr/bin/env python
import os


#actual size of the window
import libtcodpy as tcod
SCREEN_WIDTH = 80
SCREEN_HEIGHT = 50


#size of the map
MAP_WIDTH = 80
MAP_HEIGHT = 45


LIMIT_FPS = 20 #20 frames-per-second maximum
# ######################################################################
# Game Constants
# ######################################################################
# Size of the terminal window in characters
SCREEN_WIDTH = 80  # characters wide
SCREEN_HEIGHT = 50 # characters tall


# Size of the map floor (this should fit within the screen width/height)
FLOOR_WIDTH = 80  # characters wide
FLOOR_HEIGHT = 45  # characters tall


color_dark_wall = libtcod.Color(0, 0, 100)
LIMIT_FPS = 20  # 20 frames-per-second maximum
color_dark_ground = libtcod.Color(50, 50, 150)
REALTIME = False  # set True for real-time, False for turn-based




class Tile:
# ######################################################################
    #a tile of the map and its properties
# Exceptions
    def __init__(self, blocked, block_sight = None):
# ######################################################################
        self.blocked = blocked
class GameError(Exception):
       
    """Base Exception for all game errors"""
        #by default, if a tile is blocked, it also blocks sight
        if block_sight is None: block_sight = blocked
        self.block_sight = block_sight


class Object:
 
     #this is a generic object: the player, a monster, an item, the stairs...
class FontError(GameError):
     #it's always represented by a character on screen.
    """Font could not be loaded"""
     def __init__(self, x, y, char, color):
 
 
class PositionError(GameError):
    """Position not available"""
 
 
# ######################################################################
# Classes
# ######################################################################
class Direction(object):
    """Defines direction of movement
 
    Matrix:
 
      -1, -1 | 0, -1 |  1, -1
      -1, 0  | 0, 0  |  1, 0
      -1, 1  | 0, 1  |  1, 1
 
          |  UP  |
      LEFT | NONE | RIGHT
          | DOWN |
 
        NW | NORTH | NE
      WEST | NONE  | EAST
        SW | SOUTH | SE
 
    """
    NONE = (0, 0)
 
    UP = (0, -1)
    DOWN = (0, 1)
    LEFT = (-1, 0)
    RIGHT = (1, 0)
 
    NORTH = UP
    NE = (1, -1)
    EAST = RIGHT
    SE = (1, 1)
    SOUTH = DOWN
    SW = (-1, 1)
    WEST = LEFT
    NW = (-1, -1)
 
 
class Map(object):
    """A representation of the dungeon map.
 
    Adds extra functionality for iterating over the map and accessing
    individual map cells.
 
    Args:
        width (int, optional): the width of the floor [default: FLOOR_WIDTH]
        height (int, optional): the height of the floor [default: FLOOR_HEIGHT]
    """
    def block(self, position):
        """Blocks a position on the map
 
        Args:
            position (Position, tuple): the location to block
        """
        map_cell = self.__getitem__(position)
        map_cell.block()
 
    def unblock(self, position):
        """Unblocks a position on the map
 
        Args:
            position (Position, tuple): the location to unblock
        """
        map_cell = self.__getitem__(position)
        map_cell.unblock()
 
     def __init__(self, width=None, height=None):
        self.width = width or FLOOR_WIDTH
        self.height = height or FLOOR_HEIGHT
 
        # initialize map
        self.map = {}
        for y in range(self.height):
            for x in range(self.width):
                pos = Position(x, y)
                cell = MapCell()
                self.map[pos] = cell
 
    def __iter__(self):
        for y in range(self.height):
            for x in range(self.width):
                yield x, y, self.__getitem__((x, y))
 
    def __getitem__(self, key):
        value = None
        if isinstance(key, Position):
            value = self.map[key]
        elif isinstance(key, (list, tuple)):
            key = Position(*key)
            value = self.map[key]
        else:
            raise PositionError('Invalid position: {}'.format(key))
        return value
 
 
class MapCell(object):
    """A representation of a map cell which contains metadata information
    related to this specific cell within the map
 
    Args:
        blocked (bool, optional): blocks movement [default: False]
        block_sight (bool, optional): blocks visual [default: False]
    """
 
    def __init__(self, blocked=None, block_sight=None):
        self.blocked = blocked or False
        self.block_sight = block_sight or blocked or False
 
    def block(self):
        self.blocked = True
        self.block_sight = True
 
    def unblock(self):
        self.blocked = False
        self.block_sight = False
 
 
class Object(object):
    """Game object.  This is used for any displayable game object such
    as the player, a mob, an item, a staircase, etc.
 
    Args:
        character (str): the character to display
        position (Position): (x, y) the position of the object on the map
        color (tcod.Color, optional): (R, G, B) color to use when drawing [default: tcod.white]
    """
 
    def __init__(self, character, position, color=tcod.white):
        self.character = character
        self.position = position
        self.color = color
 
     def clear(self):
        """Erase the character from the console"""
        self.draw(' ')
 
    def draw(self, character=None):
        """Controls how the object is displayed on the screen.
 
        Args:
            character (str, optional): the character to display [default: self.character]
        """
        global con
 
        character = character or self.character
        tcod.console_set_default_foreground(con, self.color)
        x, y = self.position
        tcod.console_put_char(con, x, y, character, tcod.BKGND_NONE)
 
    def move(self, direction):
        """Moves the object in a specific direction.
 
        Modifies the object position.
 
        Args:
            direction (Direction, tuple): UP, DOWN, LEFT, RIGHT
        """
        self.position += direction
 
 
class Position(object):
    """A class to help with position-related math in 2d space
 
    Args:
        x (int): the width coordinate value
        y (int): the height coordinate value
    """
     def __init__(self, x, y):
         self.x = x
         self.x = x
         self.y = y
         self.y = y
         self.char = char
 
         self.color = color
    def __eq__(self, other):
   
         equal = False
     def move(self, dx, dy):
        if isinstance(other, Position):
         #move by the given amount, if the destination is not blocked
            if self.x == other.x and self.y == other.y:
         if not map[self.x + dx][self.y + dy].blocked:
                equal = True
         elif isinstance(other, (tuple, list)):
            if len(self) == len(other):
                if self.x == other[0] and self.y == other[1]:
                    equal = True
        return equal
 
     def __hash__(self):
         return hash((self.x, self.y))
 
    def __iter__(self):
         yield self.x
        yield self.y
 
    def __len__(self):
        return len((self.x, self.y))
 
    def __add__(self, other):
        if isinstance(other, Direction):
            dx, dy = other
             self.x += dx
             self.x += dx
             self.y += dy
             self.y += dy
      
        elif isinstance(other, (list, tuple)):
     def draw(self):
            dx, dy = other
         #set the color and then draw the character that represents this object at its position
            if len(other) == len(self):
         libtcod.console_set_default_foreground(con, self.color)
                self.x += dx
         libtcod.console_put_char(con, self.x, self.y, self.char, libtcod.BKGND_NONE)
                self.y += dy
      
        return self
     def clear(self):
 
        #erase the character that represents this object
     def __repr__(self):
        libtcod.console_put_char(con, self.x, self.y, ' ', libtcod.BKGND_NONE)
        return f'<Position ({self.x}, {self.y})>'
 
     def __str__(self):
         return f'({self.x}, {self.y})'
 
 
# ######################################################################
# User Interface Control
# ######################################################################
def handle_keys():
    """Handles keyboard input
 
    Updates:
        player_x: x coordinate of player position
        player_y: y coordinate of player position
 
    Returns:
        bool: True if exit the game is requested else False
    """
    global player
 
    exit_game = False
 
    # Run with REALTIME or turn-based
    if REALTIME:
        key = tcod.console_check_for_keypress()
    else:
        key = tcod.console_wait_for_keypress(True)
 
    if key.vk == tcod.KEY_ENTER and key.lalt:
        # Alt+Enter: toggle fullscreen
        tcod.console_set_fullscreen(not tcod.console_is_fullscreen())
    elif key.vk == tcod.KEY_ESCAPE:
        exit_game = True  # exit game
 
    # movement keys
    if tcod.console_is_key_pressed(tcod.KEY_UP):
        player.move(Direction.UP)
    elif tcod.console_is_key_pressed(tcod.KEY_DOWN):
        player.move(Direction.DOWN)
    elif tcod.console_is_key_pressed(tcod.KEY_LEFT):
        player.move(Direction.LEFT)
    elif tcod.console_is_key_pressed(tcod.KEY_RIGHT):
        player.move(Direction.RIGHT)
 
    return exit_game
 
 
# ######################################################################
# Game
# ######################################################################
def initialize_game(font_filepath=None, window_title=None, fullscreen=False):
    """Sets up libtcod and creates a window
 
    Updates:
        colors: a dictionary of colors used
        con: the tcod console
        npc: the npc game object
        game_objects: a list of game objects
        player: the player game object
 
    Args:
         font_filepath (str): the path to the font file [default: terminal.png]
        window_title (str): the title to display for the game [default: Python3 Tutorial]
    """
    global colors, con, game_objects, npc, player
 
    # Setup displayed font
    font_filepath = os.path.abspath(font_filepath or 'terminal.png')
    font_flags = tcod.FONT_TYPE_GREYSCALE | tcod.FONT_LAYOUT_ASCII_INCOL
    if not os.path.exists(font_filepath):
         raise FontError("Could not open font file: {}".format(font_filepath))
    tcod.console_set_custom_font(font_filepath, font_flags)
 
    # Setup window
    window_title = window_title or 'Python3 Tutorial'
    fullscreen = fullscreen or False
    tcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, window_title, fullscreen)
 
     # Limit frames per second
     tcod.sys_set_fps(LIMIT_FPS)
 
    # Create the console
    con = tcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)
 
    # Setup player's initial position
    player_starting_position = Position(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
    player = Object('@', player_starting_position)
 
    npc_starting_position = Position(SCREEN_WIDTH // 2 - 5, SCREEN_HEIGHT // 2)
    npc = Object('@', npc_starting_position, color=tcod.yellow)
 
    game_objects = [npc, player]
 
    # Setup some colors
    colors = {
        'white': tcod.white,
        'yellow': tcod.yellow,
        'dark wall': tcod.Color(0, 0, 100),
        'dark ground': tcod.Color(50, 50, 150),
    }


    # Create the map
    make_map()




def make_map():
def make_map():
     global map
     """Creates the global map
      
 
     #fill map with "unblocked" tiles
     Updates:
     map = [[ Tile(False)
        level: the global map
        for y in range(MAP_HEIGHT) ]
    """
            for x in range(MAP_WIDTH) ]
     # This is a "level" rather than a "map" because python has a "map"
   
     # function built-in and we might want to use that function later.
     #place two pillars to test the map
    global level
     map[30][22].blocked = True
 
    map[30][22].block_sight = True
    level = Map()
     map[50][22].blocked = True
 
     map[50][22].block_sight = True
     # Create some pillars / blocked locations
     locations = [
        (30, 22),
        (50, 22),
     ]
     for location in locations:
        level.block(location)




def render_all():
def render_all():
     global color_light_wall
     """Draws map and game objects.
     global color_light_ground
 
      
     Accesses:
     #go through all tiles, and set their background color
        colors: dictionary of colors
     for y in range(MAP_HEIGHT):
        con: the game console
        for x in range(MAP_WIDTH):
        game_objects: npcs, mobs, player, items, etc.
            wall = map[x][y].block_sight
        level: the global map
             if wall:
     """
                libtcod.console_set_char_background(con, x, y, color_dark_wall, libtcod.BKGND_SET )
     global colors, level, con, game_objects
            else:
 
                libtcod.console_set_char_background(con, x, y, color_dark_ground, libtcod.BKGND_SET )
    wall = colors.get('dark wall')
     ground = colors.get('dark ground')
    for x, y, cell in level:
        if cell.block_sight:
             tcod.console_set_char_background(con, x, y, wall, tcod.BKGND_SET)
        else:
            tcod.console_set_char_background(con, x, y, ground, tcod.BKGND_SET)
 
    for game_object in game_objects:
        game_object.draw()
 
    tcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)


    #draw all objects in the list
    for object in objects:
        object.draw()
   
    #blit the contents of "con" to the root console
    libtcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)
   
def handle_keys():
    #key = libtcod.console_check_for_keypress()  #real-time
    key = libtcod.console_wait_for_keypress(True)  #turn-based
   
    if key.vk == libtcod.KEY_ENTER and key.lalt:
        #Alt+Enter: toggle fullscreen
        libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
       
    elif key.vk == libtcod.KEY_ESCAPE:
        return True  #exit game
   
    #movement keys
    if libtcod.console_is_key_pressed(libtcod.KEY_UP):
        player.move(0, -1)
       
    elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN):
        player.move(0, 1)
       
    elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT):
        player.move(-1, 0)
       
    elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT):
        player.move(1, 0)


def main():
    global game_objects


#############################################
    initialize_game()
# Initialization & Main Loop
#############################################


libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
    # Game loop
libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False)
    exit_game = False
libtcod.sys_set_fps(LIMIT_FPS)
    while not tcod.console_is_window_closed() and exit_game is not True:
con = libtcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)
        render_all()


#create object representing the player
        tcod.console_flush()
player = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white)


#create an NPC
        for game_object in game_objects:
npc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '@', libtcod.yellow)
            game_object.clear()


#the list of objects with those two
        # handle keys
objects = [npc, player]
        exit_game = handle_keys()


#generate map (at this point it's not drawn to the screen)
make_map()


if __name__ == '__main__':
    main()


while not libtcod.console_is_window_closed():
   
    #render the screen
    render_all()
   
    libtcod.console_flush()


    #erase all objects at their old locations, before they move
    for object in objects:
        object.clear()
   
    #handle keys and exit game if needed
    exit = handle_keys()
    if exit:
        break
</syntaxhighlight></div>
</syntaxhighlight></div>


[[Category:Developing]]
[[Category:Developing]]

Revision as of 22:15, 15 September 2017

This is part of a series of tutorials; the main page can be found here.

Generalizing

#!/usr/bin/env python
import os

import libtcodpy as tcod


# ######################################################################
# Game Constants
# ######################################################################
# Size of the terminal window in characters
SCREEN_WIDTH = 80  # characters wide
SCREEN_HEIGHT = 50  # characters tall
LIMIT_FPS = 20  # 20 frames-per-second maximum
REALTIME = False  # set True for real-time, False for turn-based


# ######################################################################
# Exceptions
# ######################################################################
class GameError(Exception):
    """Base Exception for all game errors"""


class FontError(GameError):
    """Font could not be loaded"""


# ######################################################################
# Classes
# ######################################################################
class Direction(object):
    """Defines direction of movement

    Matrix:

      -1, -1 | 0, -1 |  1, -1
      -1, 0  | 0, 0  |  1, 0
      -1, 1  | 0, 1  |  1, 1

           |  UP  |
      LEFT | NONE | RIGHT
           | DOWN |

        NW | NORTH | NE
      WEST | NONE  | EAST
        SW | SOUTH | SE

    """
    NONE = (0, 0)

    UP = (0, -1)
    DOWN = (0, 1)
    LEFT = (-1, 0)
    RIGHT = (1, 0)

    NORTH = UP
    NE = (1, -1)
    EAST = RIGHT
    SE = (1, 1)
    SOUTH = DOWN
    SW = (-1, 1)
    WEST = LEFT
    NW = (-1, -1)


class Object(object):
    """Game object.  This is used for any displayable game object such
    as the player, a mob, an item, a staircase, etc.

    Args:
        character (str): the character to display
        position (Position): (x, y) the position of the object on the map
        color (tcod.Color, optional): (R, G, B) color to use when drawing [default: tcod.white]
    """

    def __init__(self, character, position, color=tcod.white):
        self.character = character
        self.position = position
        self.color = color

    def clear(self):
        """Erase the character from the console"""
        self.draw(' ')

    def draw(self, character=None):
        """Controls how the object is displayed on the screen.

        Args:
            character (str, optional): the character to display [default: self.character]
        """
        global con

        character = character or self.character
        tcod.console_set_default_foreground(con, self.color)
        x, y = self.position
        tcod.console_put_char(con, x, y, character, tcod.BKGND_NONE)

    def move(self, direction):
        """Moves the object in a specific direction.

        Modifies the object position.

        Args:
            direction (Direction, tuple): UP, DOWN, LEFT, RIGHT
        """
        self.position += direction


class Position(object):
    """A class to help with position-related math in 2d space

    Args:
        x (int): the width coordinate value
        y (int): the height coordinate value
    """
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        equal = False
        if isinstance(other, Position):
            if self.x == other.x and self.y == other.y:
                equal = True
        elif isinstance(other, (tuple, list)):
            if len(self) == len(other):
                if self.x == other[0] and self.y == other[1]:
                    equal = True
        return equal

    def __hash__(self):
        return hash((self.x, self.y))

    def __iter__(self):
        yield self.x
        yield self.y

    def __len__(self):
        return len((self.x, self.y))

    def __add__(self, other):
        if isinstance(other, Direction):
            dx, dy = other
            self.x += dx
            self.y += dy
        elif isinstance(other, (list, tuple)):
            dx, dy = other
            if len(other) == len(self):
                self.x += dx
                self.y += dy
        return self

    def __repr__(self):
        return f'<Position ({self.x}, {self.y})>'

    def __str__(self):
        return f'({self.x}, {self.y})'


# ######################################################################
# User Interface Control
# ######################################################################
def handle_keys():
    """Handles keyboard input

    Updates:
        player_x: x coordinate of player position
        player_y: y coordinate of player position

    Returns:
        bool: True if exit the game is requested else False
    """
    global player

    exit_game = False

    # Run with REALTIME or turn-based
    if REALTIME:
        key = tcod.console_check_for_keypress()
    else:
        key = tcod.console_wait_for_keypress(True)

    if key.vk == tcod.KEY_ENTER and key.lalt:
        # Alt+Enter: toggle fullscreen
        tcod.console_set_fullscreen(not tcod.console_is_fullscreen())
    elif key.vk == tcod.KEY_ESCAPE:
        exit_game = True  # exit game

    # movement keys
    if tcod.console_is_key_pressed(tcod.KEY_UP):
        player.move(Direction.UP)
    elif tcod.console_is_key_pressed(tcod.KEY_DOWN):
        player.move(Direction.DOWN)
    elif tcod.console_is_key_pressed(tcod.KEY_LEFT):
        player.move(Direction.LEFT)
    elif tcod.console_is_key_pressed(tcod.KEY_RIGHT):
        player.move(Direction.RIGHT)

    return exit_game


# ######################################################################
# Game
# ######################################################################
def initialize_game(font_filepath=None, window_title=None, fullscreen=False):
    """Sets up libtcod and creates a window

    Updates:
        player_x: x coordinate of player position
        player_y: y coordinate of player position

    Args:
        font_filepath (str): the path to the font file [default: terminal.png]
        window_title (str): the title to display for the game [default: Python3 Tutorial]
    """
    global player, con, npc

    # Setup displayed font
    font_filepath = os.path.abspath(font_filepath or 'terminal.png')
    font_flags = tcod.FONT_TYPE_GREYSCALE | tcod.FONT_LAYOUT_ASCII_INCOL
    if not os.path.exists(font_filepath):
        raise FontError("Could not open font file: {}".format(font_filepath))
    tcod.console_set_custom_font(font_filepath, font_flags)

    # Setup window
    window_title = window_title or 'Python3 Tutorial'
    fullscreen = fullscreen or False
    tcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, window_title, fullscreen)

    # Limit frames per second
    tcod.sys_set_fps(LIMIT_FPS)

    # Create the console
    con = tcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)

    # Setup player's initial position
    player_starting_position = Position(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
    player = Object('@', player_starting_position)

    npc_starting_position = Position(SCREEN_WIDTH // 2 - 5, SCREEN_HEIGHT // 2)
    npc = Object('@', npc_starting_position, color=tcod.yellow)


def main():
    global player, npc

    initialize_game()
    objects = [npc, player]

    # Game loop
    exit_game = False
    while not tcod.console_is_window_closed() and exit_game is not True:
        tcod.console_set_default_foreground(0, tcod.white)

        for game_object in objects:
            game_object.draw()

        tcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)
        tcod.console_flush()

        for game_object in objects:
            game_object.clear()

        # handle keys
        exit_game = handle_keys()


if __name__ == '__main__':
    main()

The Map

#!/usr/bin/env python
import os

import libtcodpy as tcod


# ######################################################################
# Game Constants
# ######################################################################
# Size of the terminal window in characters
SCREEN_WIDTH = 80  # characters wide
SCREEN_HEIGHT = 50  # characters tall

# Size of the map floor (this should fit within the screen width/height)
FLOOR_WIDTH = 80  # characters wide
FLOOR_HEIGHT = 45  # characters tall

LIMIT_FPS = 20  # 20 frames-per-second maximum
REALTIME = False  # set True for real-time, False for turn-based


# ######################################################################
# Exceptions
# ######################################################################
class GameError(Exception):
    """Base Exception for all game errors"""


class FontError(GameError):
    """Font could not be loaded"""


class PositionError(GameError):
    """Position not available"""


# ######################################################################
# Classes
# ######################################################################
class Direction(object):
    """Defines direction of movement

    Matrix:

      -1, -1 | 0, -1 |  1, -1
      -1, 0  | 0, 0  |  1, 0
      -1, 1  | 0, 1  |  1, 1

           |  UP  |
      LEFT | NONE | RIGHT
           | DOWN |

        NW | NORTH | NE
      WEST | NONE  | EAST
        SW | SOUTH | SE

    """
    NONE = (0, 0)

    UP = (0, -1)
    DOWN = (0, 1)
    LEFT = (-1, 0)
    RIGHT = (1, 0)

    NORTH = UP
    NE = (1, -1)
    EAST = RIGHT
    SE = (1, 1)
    SOUTH = DOWN
    SW = (-1, 1)
    WEST = LEFT
    NW = (-1, -1)


class Map(object):
    """A representation of the dungeon map.

    Adds extra functionality for iterating over the map and accessing
    individual map cells.

    Args:
        width (int, optional): the width of the floor [default: FLOOR_WIDTH]
        height (int, optional): the height of the floor [default: FLOOR_HEIGHT]
    """
    def block(self, position):
        """Blocks a position on the map

        Args:
            position (Position, tuple): the location to block
        """
        map_cell = self.__getitem__(position)
        map_cell.block()

    def unblock(self, position):
        """Unblocks a position on the map

        Args:
            position (Position, tuple): the location to unblock
        """
        map_cell = self.__getitem__(position)
        map_cell.unblock()

    def __init__(self, width=None, height=None):
        self.width = width or FLOOR_WIDTH
        self.height = height or FLOOR_HEIGHT

        # initialize map
        self.map = {}
        for y in range(self.height):
            for x in range(self.width):
                pos = Position(x, y)
                cell = MapCell()
                self.map[pos] = cell

    def __iter__(self):
        for y in range(self.height):
            for x in range(self.width):
                yield x, y, self.__getitem__((x, y))

    def __getitem__(self, key):
        value = None
        if isinstance(key, Position):
            value = self.map[key]
        elif isinstance(key, (list, tuple)):
            key = Position(*key)
            value = self.map[key]
        else:
            raise PositionError('Invalid position: {}'.format(key))
        return value


class MapCell(object):
    """A representation of a map cell which contains metadata information
    related to this specific cell within the map

    Args:
        blocked (bool, optional): blocks movement [default: False]
        block_sight (bool, optional): blocks visual [default: False]
    """

    def __init__(self, blocked=None, block_sight=None):
        self.blocked = blocked or False
        self.block_sight = block_sight or blocked or False

    def block(self):
        self.blocked = True
        self.block_sight = True

    def unblock(self):
        self.blocked = False
        self.block_sight = False


class Object(object):
    """Game object.  This is used for any displayable game object such
    as the player, a mob, an item, a staircase, etc.

    Args:
        character (str): the character to display
        position (Position): (x, y) the position of the object on the map
        color (tcod.Color, optional): (R, G, B) color to use when drawing [default: tcod.white]
    """

    def __init__(self, character, position, color=tcod.white):
        self.character = character
        self.position = position
        self.color = color

    def clear(self):
        """Erase the character from the console"""
        self.draw(' ')

    def draw(self, character=None):
        """Controls how the object is displayed on the screen.

        Args:
            character (str, optional): the character to display [default: self.character]
        """
        global con

        character = character or self.character
        tcod.console_set_default_foreground(con, self.color)
        x, y = self.position
        tcod.console_put_char(con, x, y, character, tcod.BKGND_NONE)

    def move(self, direction):
        """Moves the object in a specific direction.

        Modifies the object position.

        Args:
            direction (Direction, tuple): UP, DOWN, LEFT, RIGHT
        """
        self.position += direction


class Position(object):
    """A class to help with position-related math in 2d space

    Args:
        x (int): the width coordinate value
        y (int): the height coordinate value
    """
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        equal = False
        if isinstance(other, Position):
            if self.x == other.x and self.y == other.y:
                equal = True
        elif isinstance(other, (tuple, list)):
            if len(self) == len(other):
                if self.x == other[0] and self.y == other[1]:
                    equal = True
        return equal

    def __hash__(self):
        return hash((self.x, self.y))

    def __iter__(self):
        yield self.x
        yield self.y

    def __len__(self):
        return len((self.x, self.y))

    def __add__(self, other):
        if isinstance(other, Direction):
            dx, dy = other
            self.x += dx
            self.y += dy
        elif isinstance(other, (list, tuple)):
            dx, dy = other
            if len(other) == len(self):
                self.x += dx
                self.y += dy
        return self

    def __repr__(self):
        return f'<Position ({self.x}, {self.y})>'

    def __str__(self):
        return f'({self.x}, {self.y})'


# ######################################################################
# User Interface Control
# ######################################################################
def handle_keys():
    """Handles keyboard input

    Updates:
        player_x: x coordinate of player position
        player_y: y coordinate of player position

    Returns:
        bool: True if exit the game is requested else False
    """
    global player

    exit_game = False

    # Run with REALTIME or turn-based
    if REALTIME:
        key = tcod.console_check_for_keypress()
    else:
        key = tcod.console_wait_for_keypress(True)

    if key.vk == tcod.KEY_ENTER and key.lalt:
        # Alt+Enter: toggle fullscreen
        tcod.console_set_fullscreen(not tcod.console_is_fullscreen())
    elif key.vk == tcod.KEY_ESCAPE:
        exit_game = True  # exit game

    # movement keys
    if tcod.console_is_key_pressed(tcod.KEY_UP):
        player.move(Direction.UP)
    elif tcod.console_is_key_pressed(tcod.KEY_DOWN):
        player.move(Direction.DOWN)
    elif tcod.console_is_key_pressed(tcod.KEY_LEFT):
        player.move(Direction.LEFT)
    elif tcod.console_is_key_pressed(tcod.KEY_RIGHT):
        player.move(Direction.RIGHT)

    return exit_game


# ######################################################################
# Game
# ######################################################################
def initialize_game(font_filepath=None, window_title=None, fullscreen=False):
    """Sets up libtcod and creates a window

    Updates:
        colors: a dictionary of colors used
        con: the tcod console
        npc: the npc game object
        game_objects: a list of game objects
        player: the player game object

    Args:
        font_filepath (str): the path to the font file [default: terminal.png]
        window_title (str): the title to display for the game [default: Python3 Tutorial]
    """
    global colors, con, game_objects, npc, player

    # Setup displayed font
    font_filepath = os.path.abspath(font_filepath or 'terminal.png')
    font_flags = tcod.FONT_TYPE_GREYSCALE | tcod.FONT_LAYOUT_ASCII_INCOL
    if not os.path.exists(font_filepath):
        raise FontError("Could not open font file: {}".format(font_filepath))
    tcod.console_set_custom_font(font_filepath, font_flags)

    # Setup window
    window_title = window_title or 'Python3 Tutorial'
    fullscreen = fullscreen or False
    tcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, window_title, fullscreen)

    # Limit frames per second
    tcod.sys_set_fps(LIMIT_FPS)

    # Create the console
    con = tcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)

    # Setup player's initial position
    player_starting_position = Position(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
    player = Object('@', player_starting_position)

    npc_starting_position = Position(SCREEN_WIDTH // 2 - 5, SCREEN_HEIGHT // 2)
    npc = Object('@', npc_starting_position, color=tcod.yellow)

    game_objects = [npc, player]

    # Setup some colors
    colors = {
        'white': tcod.white,
        'yellow': tcod.yellow,
        'dark wall': tcod.Color(0, 0, 100),
        'dark ground': tcod.Color(50, 50, 150),
    }

    # Create the map
    make_map()


def make_map():
    """Creates the global map

    Updates:
        level: the global map
    """
    # This is a "level" rather than a "map" because python has a "map"
    # function built-in and we might want to use that function later.
    global level

    level = Map()

    # Create some pillars / blocked locations
    locations = [
        (30, 22),
        (50, 22),
    ]
    for location in locations:
        level.block(location)


def render_all():
    """Draws map and game objects.

    Accesses:
        colors: dictionary of colors
        con: the game console
        game_objects: npcs, mobs, player, items, etc.
        level: the global map
    """
    global colors, level, con, game_objects

    wall = colors.get('dark wall')
    ground = colors.get('dark ground')
    for x, y, cell in level:
        if cell.block_sight:
            tcod.console_set_char_background(con, x, y, wall, tcod.BKGND_SET)
        else:
            tcod.console_set_char_background(con, x, y, ground, tcod.BKGND_SET)

    for game_object in game_objects:
        game_object.draw()

    tcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)


def main():
    global game_objects

    initialize_game()

    # Game loop
    exit_game = False
    while not tcod.console_is_window_closed() and exit_game is not True:
        render_all()

        tcod.console_flush()

        for game_object in game_objects:
            game_object.clear()

        # handle keys
        exit_game = handle_keys()


if __name__ == '__main__':
    main()