Complete Roguelike Tutorial, using python3+libtcod, part 2 code
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()