Complete Roguelike Tutorial, using python3+libtcod, part 2 code

From RogueBasin
Revision as of 22:15, 15 September 2017 by Bix (talk | contribs) (→‎The Map)
Jump to navigation Jump to search

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()