├── arial10x10.png ├── components ├── ai.py ├── equipment.py ├── equippable.py ├── fighter.py ├── inventory.py ├── item.py ├── level.py └── stairs.py ├── death_functions.py ├── engine.py ├── entity.py ├── equipment_slots.py ├── game_messages.py ├── game_states.py ├── input_handlers.py ├── item_functions.py ├── loader_functions ├── data_loaders.py └── initialize_new_game.py ├── map_utils.py ├── menu_background.png ├── menus.py ├── random_utils.py └── render_functions.py /arial10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TStand90/roguelike_tutorial_revised_tdl/f50a99b1020ca2cca3920970938a76467df99a3e/arial10x10.png -------------------------------------------------------------------------------- /components/ai.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from game_messages import Message 4 | 5 | 6 | class BasicMonster: 7 | def take_turn(self, target, game_map, entities): 8 | results = [] 9 | 10 | monster = self.owner 11 | 12 | if game_map.fov[monster.x, monster.y]: 13 | if monster.distance_to(target) >= 2: 14 | monster.move_towards(target.x, target.y, game_map, entities) 15 | 16 | elif target.fighter.hp > 0: 17 | attack_results = monster.fighter.attack(target) 18 | results.extend(attack_results) 19 | 20 | return results 21 | 22 | 23 | class ConfusedMonster: 24 | def __init__(self, previous_ai, number_of_turns=10): 25 | self.previous_ai = previous_ai 26 | self.number_of_turns = number_of_turns 27 | 28 | def take_turn(self, target, game_map, entities): 29 | results = [] 30 | 31 | if self.number_of_turns > 0: 32 | random_x = self.owner.x + randint(0, 2) - 1 33 | random_y = self.owner.y + randint(0, 2) - 1 34 | 35 | if random_x != self.owner.x and random_y != self.owner.y: 36 | self.owner.move_towards(random_x, random_y, game_map, entities) 37 | 38 | self.number_of_turns -= 1 39 | else: 40 | self.owner.ai = self.previous_ai 41 | results.append({'message': Message('The {0} is no longer confused!'.format(self.owner.name))}) 42 | 43 | return results 44 | -------------------------------------------------------------------------------- /components/equipment.py: -------------------------------------------------------------------------------- 1 | from equipment_slots import EquipmentSlots 2 | 3 | 4 | class Equipment: 5 | def __init__(self, main_hand=None, off_hand=None): 6 | self.main_hand = main_hand 7 | self.off_hand = off_hand 8 | 9 | @property 10 | def max_hp_bonus(self): 11 | bonus = 0 12 | 13 | if self.main_hand and self.main_hand.equippable: 14 | bonus += self.main_hand.equippable.max_hp_bonus 15 | 16 | if self.off_hand and self.off_hand.equippable: 17 | bonus += self.off_hand.equippable.max_hp_bonus 18 | 19 | return bonus 20 | 21 | @property 22 | def power_bonus(self): 23 | bonus = 0 24 | 25 | if self.main_hand and self.main_hand.equippable: 26 | bonus += self.main_hand.equippable.power_bonus 27 | 28 | if self.off_hand and self.off_hand.equippable: 29 | bonus += self.off_hand.equippable.power_bonus 30 | 31 | return bonus 32 | 33 | @property 34 | def defense_bonus(self): 35 | bonus = 0 36 | 37 | if self.main_hand and self.main_hand.equippable: 38 | bonus += self.main_hand.equippable.defense_bonus 39 | 40 | if self.off_hand and self.off_hand.equippable: 41 | bonus += self.off_hand.equippable.defense_bonus 42 | 43 | return bonus 44 | 45 | def toggle_equip(self, equippable_entity): 46 | results = [] 47 | 48 | slot = equippable_entity.equippable.slot 49 | 50 | if slot == EquipmentSlots.MAIN_HAND: 51 | if self.main_hand == equippable_entity: 52 | self.main_hand = None 53 | results.append({'dequipped': equippable_entity}) 54 | else: 55 | if self.main_hand: 56 | results.append({'dequipped': self.main_hand}) 57 | 58 | self.main_hand = equippable_entity 59 | results.append({'equipped': equippable_entity}) 60 | elif slot == EquipmentSlots.OFF_HAND: 61 | if self.off_hand == equippable_entity: 62 | self.off_hand = None 63 | results.append({'dequipped': equippable_entity}) 64 | else: 65 | if self.off_hand: 66 | results.append({'dequipped': self.off_hand}) 67 | 68 | self.off_hand = equippable_entity 69 | results.append({'equipped': equippable_entity}) 70 | 71 | return results 72 | -------------------------------------------------------------------------------- /components/equippable.py: -------------------------------------------------------------------------------- 1 | class Equippable: 2 | def __init__(self, slot, power_bonus=0, defense_bonus=0, max_hp_bonus=0): 3 | self.slot = slot 4 | self.power_bonus = power_bonus 5 | self.defense_bonus = defense_bonus 6 | self.max_hp_bonus = max_hp_bonus 7 | -------------------------------------------------------------------------------- /components/fighter.py: -------------------------------------------------------------------------------- 1 | from game_messages import Message 2 | 3 | 4 | class Fighter: 5 | def __init__(self, hp, defense, power, xp=0): 6 | self.base_max_hp = hp 7 | self.hp = hp 8 | self.base_defense = defense 9 | self.base_power = power 10 | self.xp = xp 11 | 12 | @property 13 | def max_hp(self): 14 | if self.owner and self.owner.equipment: 15 | bonus = self.owner.equipment.max_hp_bonus 16 | else: 17 | bonus = 0 18 | 19 | return self.base_max_hp + bonus 20 | 21 | @property 22 | def power(self): 23 | if self.owner and self.owner.equipment: 24 | bonus = self.owner.equipment.power_bonus 25 | else: 26 | bonus = 0 27 | 28 | return self.base_power + bonus 29 | 30 | @property 31 | def defense(self): 32 | if self.owner and self.owner.equipment: 33 | bonus = self.owner.equipment.defense_bonus 34 | else: 35 | bonus = 0 36 | 37 | return self.base_defense + bonus 38 | 39 | def take_damage(self, amount): 40 | results = [] 41 | 42 | self.hp -= amount 43 | 44 | if self.hp <= 0: 45 | results.append({'dead': self.owner, 'xp': self.xp}) 46 | 47 | return results 48 | 49 | def heal(self, amount): 50 | self.hp += amount 51 | 52 | if self.hp > self.max_hp: 53 | self.hp = self.max_hp 54 | 55 | def attack(self, target): 56 | results = [] 57 | 58 | damage = self.power - target.fighter.defense 59 | 60 | if damage > 0: 61 | results.append({'message': Message('{0} attacks {1} for {2} hit points.'.format( 62 | self.owner.name.capitalize(), target.name, str(damage)))}) 63 | results.extend(target.fighter.take_damage(damage)) 64 | else: 65 | results.append({'message': Message('{0} attacks {1} but does no damage.'.format( 66 | self.owner.name.capitalize(), target.name))}) 67 | 68 | return results 69 | -------------------------------------------------------------------------------- /components/inventory.py: -------------------------------------------------------------------------------- 1 | from game_messages import Message 2 | 3 | 4 | class Inventory: 5 | def __init__(self, capacity): 6 | self.capacity = capacity 7 | self.items = [] 8 | 9 | def add_item(self, item, colors): 10 | results = [] 11 | 12 | if len(self.items) >= self.capacity: 13 | results.append({ 14 | 'item_added': None, 15 | 'message': Message('You cannot carry any more, your inventory is full', colors.get('yellow')) 16 | }) 17 | else: 18 | results.append({ 19 | 'item_added': item, 20 | 'message': Message('You pick up the {0}!'.format(item.name), colors.get('blue')) 21 | }) 22 | 23 | self.items.append(item) 24 | 25 | return results 26 | 27 | def use(self, item_entity, colors, **kwargs): 28 | results = [] 29 | 30 | item_component = item_entity.item 31 | 32 | if item_component.use_function is None: 33 | equippable_component = item_entity.equippable 34 | 35 | if equippable_component: 36 | results.append({'equip': item_entity}) 37 | else: 38 | results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), 39 | colors.get('yellow'))}) 40 | else: 41 | if item_component.targeting and not (kwargs.get('target_x') or kwargs.get('target_y')): 42 | results.append({'targeting': item_entity}) 43 | else: 44 | kwargs = {**item_component.function_kwargs, **kwargs} 45 | item_use_results = item_component.use_function(self.owner, colors, **kwargs) 46 | 47 | for item_use_result in item_use_results: 48 | if item_use_result.get('consumed'): 49 | self.remove_item(item_entity) 50 | 51 | results.extend(item_use_results) 52 | 53 | return results 54 | 55 | def remove_item(self, item): 56 | self.items.remove(item) 57 | 58 | def drop_item(self, item, colors): 59 | results = [] 60 | 61 | if self.owner.equipment.main_hand == item or self.owner.equipment.off_hand == item: 62 | self.owner.equipment.toggle_equip(item) 63 | 64 | item.x = self.owner.x 65 | item.y = self.owner.y 66 | 67 | self.remove_item(item) 68 | results.append({'item_dropped': item, 'message': Message('You dropped the {0}'.format(item.name), 69 | colors.get('yellow'))}) 70 | 71 | return results 72 | -------------------------------------------------------------------------------- /components/item.py: -------------------------------------------------------------------------------- 1 | class Item: 2 | def __init__(self, use_function=None, targeting=False, targeting_message=None, **kwargs): 3 | self.use_function = use_function 4 | self.targeting = targeting 5 | self.targeting_message = targeting_message 6 | self.function_kwargs = kwargs 7 | -------------------------------------------------------------------------------- /components/level.py: -------------------------------------------------------------------------------- 1 | class Level: 2 | def __init__(self, current_level=1, current_xp=0, level_up_base=200, level_up_factor=150): 3 | self.current_level = current_level 4 | self.current_xp = current_xp 5 | self.level_up_base = level_up_base 6 | self.level_up_factor = level_up_factor 7 | 8 | @property 9 | def experience_to_next_level(self): 10 | return self.level_up_base + self.current_level * self.level_up_factor 11 | 12 | def add_xp(self, xp): 13 | self.current_xp += xp 14 | 15 | if self.current_xp > self.experience_to_next_level: 16 | self.current_xp -= self.experience_to_next_level 17 | self.current_level += 1 18 | 19 | return True 20 | else: 21 | return False 22 | -------------------------------------------------------------------------------- /components/stairs.py: -------------------------------------------------------------------------------- 1 | class Stairs: 2 | def __init__(self, floor): 3 | self.floor = floor 4 | -------------------------------------------------------------------------------- /death_functions.py: -------------------------------------------------------------------------------- 1 | from game_messages import Message 2 | 3 | from game_states import GameStates 4 | 5 | from render_functions import RenderOrder 6 | 7 | 8 | def kill_player(player, colors): 9 | player.char = '%' 10 | player.color = colors.get('dark_red') 11 | 12 | return Message('You died!', colors.get('red')), GameStates.PLAYER_DEAD 13 | 14 | 15 | def kill_monster(monster, colors): 16 | death_message = Message('{0} is dead!'.format(monster.name.capitalize()), colors.get('orange')) 17 | 18 | monster.char = '%' 19 | monster.color = colors.get('dark_red') 20 | monster.blocks = False 21 | monster.fighter = None 22 | monster.ai = None 23 | monster.name = 'remains of ' + monster.name 24 | monster.render_order = RenderOrder.CORPSE 25 | 26 | return death_message 27 | -------------------------------------------------------------------------------- /engine.py: -------------------------------------------------------------------------------- 1 | import tdl 2 | 3 | from tcod import image_load 4 | 5 | from death_functions import kill_monster, kill_player 6 | from entity import get_blocking_entities_at_location 7 | from game_messages import Message 8 | from game_states import GameStates 9 | from input_handlers import handle_keys, handle_mouse, handle_main_menu 10 | from loader_functions.initialize_new_game import get_constants, get_game_variables 11 | from loader_functions.data_loaders import load_game, save_game 12 | from map_utils import next_floor 13 | from menus import main_menu, message_box 14 | from render_functions import clear_all, render_all 15 | 16 | 17 | def play_game(player, entities, game_map, message_log, game_state, root_console, con, panel, constants): 18 | tdl.set_font('arial10x10.png', greyscale=True, altLayout=True) 19 | 20 | fov_recompute = True 21 | 22 | mouse_coordinates = (0, 0) 23 | 24 | previous_game_state = game_state 25 | 26 | targeting_item = None 27 | 28 | while not tdl.event.is_window_closed(): 29 | if fov_recompute: 30 | game_map.compute_fov(player.x, player.y, fov=constants['fov_algorithm'], radius=constants['fov_radius'], 31 | light_walls=constants['fov_light_walls']) 32 | 33 | render_all(con, panel, entities, player, game_map, fov_recompute, root_console, message_log, 34 | constants['screen_width'], constants['screen_height'], constants['bar_width'], 35 | constants['panel_height'], constants['panel_y'], mouse_coordinates, constants['colors'], 36 | game_state) 37 | tdl.flush() 38 | 39 | clear_all(con, entities) 40 | 41 | fov_recompute = False 42 | 43 | for event in tdl.event.get(): 44 | if event.type == 'KEYDOWN': 45 | user_input = event 46 | break 47 | elif event.type == 'MOUSEMOTION': 48 | mouse_coordinates = event.cell 49 | elif event.type == 'MOUSEDOWN': 50 | user_mouse_input = event 51 | break 52 | else: 53 | user_input = None 54 | user_mouse_input = None 55 | 56 | if not (user_input or user_mouse_input): 57 | continue 58 | 59 | action = handle_keys(user_input, game_state) 60 | mouse_action = handle_mouse(user_mouse_input) 61 | 62 | move = action.get('move') 63 | wait = action.get('wait') 64 | pickup = action.get('pickup') 65 | show_inventory = action.get('show_inventory') 66 | drop_inventory = action.get('drop_inventory') 67 | inventory_index = action.get('inventory_index') 68 | take_stairs = action.get('take_stairs') 69 | level_up = action.get('level_up') 70 | show_character_screen = action.get('show_character_screen') 71 | exit = action.get('exit') 72 | fullscreen = action.get('fullscreen') 73 | 74 | left_click = mouse_action.get('left_click') 75 | right_click = mouse_action.get('right_click') 76 | 77 | player_turn_results = [] 78 | 79 | if move and game_state == GameStates.PLAYERS_TURN: 80 | dx, dy = move 81 | destination_x = player.x + dx 82 | destination_y = player.y + dy 83 | 84 | if game_map.walkable[destination_x, destination_y]: 85 | target = get_blocking_entities_at_location(entities, destination_x, destination_y) 86 | 87 | if target: 88 | attack_results = player.fighter.attack(target) 89 | player_turn_results.extend(attack_results) 90 | else: 91 | player.move(dx, dy) 92 | 93 | fov_recompute = True 94 | 95 | game_state = GameStates.ENEMY_TURN 96 | 97 | elif wait: 98 | game_state = GameStates.ENEMY_TURN 99 | 100 | elif pickup and game_state == GameStates.PLAYERS_TURN: 101 | for entity in entities: 102 | if entity.item and entity.x == player.x and entity.y == player.y: 103 | pickup_results = player.inventory.add_item(entity, constants['colors']) 104 | player_turn_results.extend(pickup_results) 105 | 106 | break 107 | else: 108 | message_log.add_message(Message('There is nothing here to pick up.', constants['colors'].get('yellow'))) 109 | 110 | if show_inventory: 111 | previous_game_state = game_state 112 | game_state = GameStates.SHOW_INVENTORY 113 | 114 | if drop_inventory: 115 | previous_game_state = game_state 116 | game_state = GameStates.DROP_INVENTORY 117 | 118 | if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len( 119 | player.inventory.items): 120 | item = player.inventory.items[inventory_index] 121 | 122 | if game_state == GameStates.SHOW_INVENTORY: 123 | player_turn_results.extend(player.inventory.use(item, constants['colors'], entities=entities, 124 | game_map=game_map)) 125 | elif game_state == GameStates.DROP_INVENTORY: 126 | player_turn_results.extend(player.inventory.drop_item(item, constants['colors'])) 127 | 128 | if take_stairs and game_state == GameStates.PLAYERS_TURN: 129 | for entity in entities: 130 | if entity.stairs and entity.x == player.x and entity.y == player.y: 131 | game_map, entities = next_floor(player, message_log, entity.stairs.floor, constants) 132 | fov_recompute = True 133 | con.clear() 134 | 135 | break 136 | else: 137 | message_log.add_message(Message('There are no stairs here.', constants['colors'].get('yellow'))) 138 | 139 | if level_up: 140 | if level_up == 'hp': 141 | player.fighter.base_max_hp += 20 142 | player.fighter.hp += 20 143 | elif level_up == 'str': 144 | player.fighter.base_power += 1 145 | elif level_up == 'def': 146 | player.fighter.base_defense += 1 147 | 148 | game_state = previous_game_state 149 | 150 | if show_character_screen: 151 | previous_game_state = game_state 152 | game_state = GameStates.CHARACTER_SCREEN 153 | 154 | if game_state == GameStates.TARGETING: 155 | if left_click: 156 | target_x, target_y = left_click 157 | 158 | item_use_results = player.inventory.use(targeting_item, constants['colors'], entities=entities, 159 | game_map=game_map, target_x=target_x, target_y=target_y) 160 | player_turn_results.extend(item_use_results) 161 | elif right_click: 162 | player_turn_results.append({'targeting_cancelled': True}) 163 | 164 | if exit: 165 | if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY, GameStates.CHARACTER_SCREEN): 166 | game_state = previous_game_state 167 | elif game_state == GameStates.TARGETING: 168 | player_turn_results.append({'targeting_cancelled': True}) 169 | else: 170 | save_game(player, entities, game_map, message_log, game_state) 171 | 172 | return True 173 | 174 | if fullscreen: 175 | tdl.set_fullscreen(not tdl.get_fullscreen()) 176 | 177 | for player_turn_result in player_turn_results: 178 | message = player_turn_result.get('message') 179 | dead_entity = player_turn_result.get('dead') 180 | item_added = player_turn_result.get('item_added') 181 | item_consumed = player_turn_result.get('consumed') 182 | item_dropped = player_turn_result.get('item_dropped') 183 | equip = player_turn_result.get('equip') 184 | targeting = player_turn_result.get('targeting') 185 | targeting_cancelled = player_turn_result.get('targeting_cancelled') 186 | xp = player_turn_result.get('xp') 187 | 188 | if message: 189 | message_log.add_message(message) 190 | 191 | if dead_entity: 192 | if dead_entity == player: 193 | message, game_state = kill_player(dead_entity, constants['colors']) 194 | else: 195 | message = kill_monster(dead_entity, constants['colors']) 196 | 197 | message_log.add_message(message) 198 | 199 | if item_added: 200 | entities.remove(item_added) 201 | 202 | game_state = GameStates.ENEMY_TURN 203 | 204 | if item_consumed: 205 | game_state = GameStates.ENEMY_TURN 206 | 207 | if item_dropped: 208 | entities.append(item_dropped) 209 | 210 | game_state = GameStates.ENEMY_TURN 211 | 212 | if equip: 213 | equip_results = player.equipment.toggle_equip(equip) 214 | 215 | for equip_result in equip_results: 216 | equipped = equip_result.get('equipped') 217 | dequipped = equip_result.get('dequipped') 218 | 219 | if equipped: 220 | message_log.add_message(Message('You equipped the {0}'.format(equipped.name))) 221 | 222 | if dequipped: 223 | message_log.add_message(Message('You dequipped the {0}'.format(dequipped.name))) 224 | 225 | game_state = GameStates.ENEMY_TURN 226 | 227 | if targeting: 228 | previous_game_state = GameStates.PLAYERS_TURN 229 | game_state = GameStates.TARGETING 230 | 231 | targeting_item = targeting 232 | 233 | message_log.add_message(targeting_item.item.targeting_message) 234 | 235 | if targeting_cancelled: 236 | game_state = previous_game_state 237 | 238 | message_log.add_message(Message('Targeting cancelled')) 239 | 240 | if xp: 241 | leveled_up = player.level.add_xp(xp) 242 | message_log.add_message(Message('You gain {0} experience points.'.format(xp))) 243 | 244 | if leveled_up: 245 | message_log.add_message(Message( 246 | 'Your battle skills grow stronger! You reached level {0}'.format( 247 | player.level.current_level) + '!', 248 | constants['colors'].get('yellow'))) 249 | previous_game_state = game_state 250 | game_state = GameStates.LEVEL_UP 251 | 252 | if game_state == GameStates.ENEMY_TURN: 253 | for entity in entities: 254 | if entity.ai: 255 | enemy_turn_results = entity.ai.take_turn(player, game_map, entities) 256 | 257 | for enemy_turn_result in enemy_turn_results: 258 | message = enemy_turn_result.get('message') 259 | dead_entity = enemy_turn_result.get('dead') 260 | 261 | if message: 262 | message_log.add_message(message) 263 | 264 | if dead_entity: 265 | if dead_entity == player: 266 | message, game_state = kill_player(dead_entity, constants['colors']) 267 | else: 268 | message = kill_monster(dead_entity, constants['colors']) 269 | 270 | message_log.add_message(message) 271 | 272 | if game_state == GameStates.PLAYER_DEAD: 273 | break 274 | 275 | if game_state == GameStates.PLAYER_DEAD: 276 | break 277 | else: 278 | game_state = GameStates.PLAYERS_TURN 279 | 280 | def main(): 281 | constants = get_constants() 282 | 283 | tdl.set_font('arial10x10.png', greyscale=True, altLayout=True) 284 | 285 | root_console = tdl.init(constants['screen_width'], constants['screen_height'], constants['window_title']) 286 | con = tdl.Console(constants['screen_width'], constants['screen_height']) 287 | panel = tdl.Console(constants['screen_width'], constants['panel_height']) 288 | 289 | player = None 290 | entities = [] 291 | game_map = None 292 | message_log = None 293 | game_state = None 294 | 295 | show_main_menu = True 296 | show_load_error_message = False 297 | 298 | main_menu_background_image = image_load('menu_background.png') 299 | 300 | while not tdl.event.is_window_closed(): 301 | for event in tdl.event.get(): 302 | if event.type == 'KEYDOWN': 303 | user_input = event 304 | break 305 | else: 306 | user_input = None 307 | 308 | if show_main_menu: 309 | main_menu(con, root_console, main_menu_background_image, constants['screen_width'], 310 | constants['screen_height'], constants['colors']) 311 | 312 | if show_load_error_message: 313 | message_box(con, root_console, 'No save game to load', 50, constants['screen_width'], 314 | constants['screen_height']) 315 | 316 | tdl.flush() 317 | 318 | action = handle_main_menu(user_input) 319 | 320 | new_game = action.get('new_game') 321 | load_saved_game = action.get('load_game') 322 | exit_game = action.get('exit') 323 | 324 | if show_load_error_message and (new_game or load_saved_game or exit_game): 325 | show_load_error_message = False 326 | elif new_game: 327 | player, entities, game_map, message_log, game_state = get_game_variables(constants) 328 | game_state = GameStates.PLAYERS_TURN 329 | 330 | show_main_menu = False 331 | elif load_saved_game: 332 | try: 333 | player, entities, game_map, message_log, game_state = load_game() 334 | show_main_menu = False 335 | except FileNotFoundError: 336 | show_load_error_message = True 337 | elif exit_game: 338 | break 339 | 340 | else: 341 | root_console.clear() 342 | con.clear() 343 | panel.clear() 344 | play_game(player, entities, game_map, message_log, game_state, root_console, con, panel, constants) 345 | 346 | show_main_menu = True 347 | 348 | 349 | if __name__ == '__main__': 350 | main() 351 | -------------------------------------------------------------------------------- /entity.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from components.item import Item 4 | 5 | from render_functions import RenderOrder 6 | 7 | 8 | class Entity: 9 | """ 10 | A generic object to represent players, enemies, items, etc. 11 | """ 12 | def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None, 13 | item=None, inventory=None, stairs=None, level=None, equipment=None, equippable=None): 14 | self.x = x 15 | self.y = y 16 | self.char = char 17 | self.color = color 18 | self.name = name 19 | self.blocks = blocks 20 | self.render_order = render_order 21 | self.fighter = fighter 22 | self.ai = ai 23 | self.item = item 24 | self.inventory = inventory 25 | self.stairs = stairs 26 | self.level = level 27 | self.equipment = equipment 28 | self.equippable = equippable 29 | 30 | if self.fighter: 31 | self.fighter.owner = self 32 | 33 | if self.ai: 34 | self.ai.owner = self 35 | 36 | if self.item: 37 | self.item.owner = self 38 | 39 | if self.inventory: 40 | self.inventory.owner = self 41 | 42 | if self.stairs: 43 | self.stairs.owner = self 44 | 45 | if self.level: 46 | self.level.owner = self 47 | 48 | if self.equipment: 49 | self.equipment.owner = self 50 | 51 | if self.equippable: 52 | self.equippable.owner = self 53 | 54 | if not self.item: 55 | item = Item() 56 | self.item = item 57 | self.item.owner = self 58 | 59 | def move(self, dx, dy): 60 | # Move the entity by a given amount 61 | self.x += dx 62 | self.y += dy 63 | 64 | def move_towards(self, target_x, target_y, game_map, entities): 65 | path = game_map.compute_path(self.x, self.y, target_x, target_y) 66 | 67 | if path: 68 | dx = path[0][0] - self.x 69 | dy = path[0][1] - self.y 70 | 71 | if game_map.walkable[path[0][0], path[0][1]] and not get_blocking_entities_at_location(entities, self.x + dx, 72 | self.y + dy): 73 | self.move(dx, dy) 74 | 75 | def distance(self, x, y): 76 | return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2) 77 | 78 | def distance_to(self, other): 79 | dx = other.x - self.x 80 | dy = other.y - self.y 81 | return math.sqrt(dx ** 2 + dy ** 2) 82 | 83 | 84 | def get_blocking_entities_at_location(entities, destination_x, destination_y): 85 | for entity in entities: 86 | if entity.blocks and entity.x == destination_x and entity.y == destination_y: 87 | return entity 88 | 89 | return None 90 | -------------------------------------------------------------------------------- /equipment_slots.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EquipmentSlots(Enum): 5 | MAIN_HAND = 1 6 | OFF_HAND = 2 7 | -------------------------------------------------------------------------------- /game_messages.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | 4 | class Message: 5 | def __init__(self, text, color=(255, 255, 255)): 6 | self.text = text 7 | self.color = color 8 | 9 | 10 | class MessageLog: 11 | def __init__(self, x, width, height): 12 | self.messages = [] 13 | self.x = x 14 | self.width = width 15 | self.height = height 16 | 17 | def add_message(self, message): 18 | # Split the message if necessary, among multiple lines 19 | new_msg_lines = textwrap.wrap(message.text, self.width) 20 | 21 | for line in new_msg_lines: 22 | # If the buffer is full, remove the first line to make room for the new one 23 | if len(self.messages) == self.height: 24 | del self.messages[0] 25 | 26 | # Add the new line as a Message object, with the text and the color 27 | self.messages.append(Message(line, message.color)) 28 | -------------------------------------------------------------------------------- /game_states.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GameStates(Enum): 5 | PLAYERS_TURN = 1 6 | ENEMY_TURN = 2 7 | PLAYER_DEAD = 3 8 | SHOW_INVENTORY = 4 9 | DROP_INVENTORY = 5 10 | TARGETING = 6 11 | LEVEL_UP = 7 12 | CHARACTER_SCREEN = 8 13 | -------------------------------------------------------------------------------- /input_handlers.py: -------------------------------------------------------------------------------- 1 | from game_states import GameStates 2 | 3 | 4 | def handle_keys(user_input, game_state): 5 | if user_input: 6 | if game_state == GameStates.PLAYERS_TURN: 7 | return handle_player_turn_keys(user_input) 8 | elif game_state == GameStates.PLAYER_DEAD: 9 | return handle_player_dead_keys(user_input) 10 | elif game_state == GameStates.TARGETING: 11 | return handle_targeting_keys(user_input) 12 | elif game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY): 13 | return handle_inventory_keys(user_input) 14 | elif game_state == GameStates.LEVEL_UP: 15 | return handle_level_up_menu(user_input) 16 | elif game_state == GameStates.CHARACTER_SCREEN: 17 | return handle_character_screen(user_input) 18 | 19 | return {} 20 | 21 | 22 | def handle_player_turn_keys(user_input): 23 | key_char = user_input.char 24 | 25 | # Movement keys 26 | if user_input.key == 'UP' or key_char == 'k': 27 | return {'move': (0, -1)} 28 | elif user_input.key == 'DOWN' or key_char == 'j': 29 | return {'move': (0, 1)} 30 | elif user_input.key == 'LEFT' or key_char == 'h': 31 | return {'move': (-1, 0)} 32 | elif user_input.key == 'RIGHT' or key_char == 'l': 33 | return {'move': (1, 0)} 34 | elif key_char == 'y': 35 | return {'move': (-1, -1)} 36 | elif key_char == 'u': 37 | return {'move': (1, -1)} 38 | elif key_char == 'b': 39 | return {'move': (-1, 1)} 40 | elif key_char == 'n': 41 | return {'move': (1, 1)} 42 | elif key_char == 'z': 43 | return {'wait': True} 44 | 45 | if key_char == 'g': 46 | return {'pickup': True} 47 | 48 | elif key_char == 'i': 49 | return {'show_inventory': True} 50 | 51 | elif key_char == 'd': 52 | return {'drop_inventory': True} 53 | 54 | elif key_char == '.' and user_input.shift: 55 | return {'take_stairs': True} 56 | 57 | elif key_char == 'c': 58 | return {'show_character_screen': True} 59 | 60 | if user_input.key == 'ENTER' and user_input.alt: 61 | # Alt+Enter: toggle full screen 62 | return {'fullscreen': True} 63 | elif user_input.key == 'ESCAPE': 64 | # Exit the game 65 | return {'exit': True} 66 | 67 | # No key was pressed 68 | return {} 69 | 70 | 71 | def handle_targeting_keys(user_input): 72 | if user_input.key == 'ESCAPE': 73 | return {'exit': True} 74 | 75 | return {} 76 | 77 | 78 | def handle_player_dead_keys(user_input): 79 | key_char = user_input.char 80 | 81 | if key_char == 'i': 82 | return {'show_inventory': True} 83 | 84 | if user_input.key == 'ENTER' and user_input.alt: 85 | # Alt+Enter: toggle full screen 86 | return {'fullscreen': True} 87 | elif user_input.key == 'ESCAPE': 88 | # Exit the game 89 | return {'exit': True} 90 | 91 | # No key was pressed 92 | return {} 93 | 94 | 95 | def handle_inventory_keys(user_input): 96 | if not user_input.char: 97 | return {} 98 | 99 | index = ord(user_input.char) - ord('a') 100 | 101 | if index >= 0: 102 | return {'inventory_index': index} 103 | 104 | if user_input.key == 'ENTER' and user_input.alt: 105 | # Alt+Enter: toggle full screen 106 | return {'fullscreen': True} 107 | elif user_input.key == 'ESCAPE': 108 | # Exit the game 109 | return {'exit': True} 110 | 111 | return {} 112 | 113 | 114 | def handle_main_menu(user_input): 115 | if user_input: 116 | key_char = user_input.char 117 | 118 | if key_char == 'a': 119 | return {'new_game': True} 120 | elif key_char == 'b': 121 | return {'load_game': True} 122 | elif key_char == 'c' or user_input.key == 'ESCAPE': 123 | return {'exit': True} 124 | 125 | return {} 126 | 127 | 128 | def handle_level_up_menu(user_input): 129 | if user_input: 130 | key_char = user_input.char 131 | 132 | if key_char == 'a': 133 | return {'level_up': 'hp'} 134 | elif key_char == 'b': 135 | return {'level_up': 'str'} 136 | elif key_char == 'c': 137 | return {'level_up': 'def'} 138 | 139 | return {} 140 | 141 | 142 | def handle_character_screen(user_input): 143 | if user_input.key == 'ESCAPE': 144 | return {'exit': True} 145 | 146 | return {} 147 | 148 | 149 | def handle_mouse(mouse_event): 150 | if mouse_event: 151 | (x, y) = mouse_event.cell 152 | 153 | if mouse_event.button == 'LEFT': 154 | return {'left_click': (x, y)} 155 | elif mouse_event.button == 'RIGHT': 156 | return {'right_click': (x, y)} 157 | 158 | return {} 159 | -------------------------------------------------------------------------------- /item_functions.py: -------------------------------------------------------------------------------- 1 | from components.ai import ConfusedMonster 2 | 3 | from game_messages import Message 4 | 5 | 6 | def heal(*args, **kwargs): 7 | entity = args[0] 8 | colors = args[1] 9 | amount = kwargs.get('amount') 10 | 11 | results = [] 12 | 13 | if entity.fighter.hp == entity.fighter.max_hp: 14 | results.append({'consumed': False, 'message': Message('You are already at full health', colors.get('yellow'))}) 15 | else: 16 | entity.fighter.heal(amount) 17 | results.append({'consumed': True, 'message': Message('Your wounds start to feel better!', colors.get('green'))}) 18 | 19 | return results 20 | 21 | 22 | def cast_lightning(*args, **kwargs): 23 | caster = args[0] 24 | colors = args[1] 25 | entities = kwargs.get('entities') 26 | game_map = kwargs.get('game_map') 27 | damage = kwargs.get('damage') 28 | maximum_range = kwargs.get('maximum_range') 29 | 30 | results = [] 31 | 32 | target = None 33 | closest_distance = maximum_range + 1 34 | 35 | for entity in entities: 36 | if entity.fighter and entity != caster and game_map.fov[entity.x, entity.y]: 37 | distance = caster.distance_to(entity) 38 | 39 | if distance < closest_distance: 40 | target = entity 41 | closest_distance = distance 42 | 43 | if target: 44 | results.append({'consumed': True, 'target': target, 'message': Message('A lighting bolt strikes the {0} with a loud thunder! The damage is {1}'.format(target.name, damage))}) 45 | results.extend(target.fighter.take_damage(damage)) 46 | else: 47 | results.append({'consumed': False, 'target': None, 'message': Message('No enemy is close enough to strike.', colors.get('red'))}) 48 | 49 | return results 50 | 51 | 52 | def cast_fireball(*args, **kwargs): 53 | colors = args[1] 54 | entities = kwargs.get('entities') 55 | game_map = kwargs.get('game_map') 56 | damage = kwargs.get('damage') 57 | radius = kwargs.get('radius') 58 | target_x = kwargs.get('target_x') 59 | target_y = kwargs.get('target_y') 60 | 61 | results = [] 62 | 63 | if not game_map.fov[target_x, target_y]: 64 | results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', 65 | colors.get('yellow'))}) 66 | return results 67 | 68 | results.append({'consumed': True, 69 | 'message': Message('The fireball explodes, burning everything within {0} tiles!'.format(radius), 70 | colors.get('orange'))}) 71 | 72 | for entity in entities: 73 | if entity.distance(target_x, target_y) <= radius and entity.fighter: 74 | results.append({'message': Message('The {0} gets burned for {1} hit points.'.format(entity.name, damage), 75 | colors.get('orange'))}) 76 | results.extend(entity.fighter.take_damage(damage)) 77 | 78 | return results 79 | 80 | 81 | def cast_confuse(*args, **kwargs): 82 | colors = args[1] 83 | entities = kwargs.get('entities') 84 | game_map = kwargs.get('game_map') 85 | target_x = kwargs.get('target_x') 86 | target_y = kwargs.get('target_y') 87 | 88 | results = [] 89 | 90 | if not game_map.fov[target_x, target_y]: 91 | results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', 92 | colors.get('yellow'))}) 93 | return results 94 | 95 | for entity in entities: 96 | if entity.x == target_x and entity.y == target_y and entity.ai: 97 | confused_ai = ConfusedMonster(entity.ai, 10) 98 | 99 | confused_ai.owner = entity 100 | entity.ai = confused_ai 101 | 102 | results.append({'consumed': True, 'message': Message('The eyes of the {0} look vacant, as he starts to stumble around!'.format(entity.name), 103 | colors.get('light_green'))}) 104 | 105 | break 106 | else: 107 | results.append({'consumed': False, 'message': Message('There is no targetable enemy at that location.', 108 | colors.get('yellow'))}) 109 | 110 | return results 111 | -------------------------------------------------------------------------------- /loader_functions/data_loaders.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import shelve 4 | 5 | 6 | def save_game(player, entities, game_map, message_log, game_state): 7 | with shelve.open('savegame.dat', 'n') as data_file: 8 | data_file['player_index'] = entities.index(player) 9 | data_file['entities'] = entities 10 | data_file['game_map'] = game_map 11 | data_file['message_log'] = message_log 12 | data_file['game_state'] = game_state 13 | 14 | 15 | def load_game(): 16 | if not os.path.isfile('savegame.dat'): 17 | raise FileNotFoundError 18 | 19 | with shelve.open('savegame.dat', 'r') as data_file: 20 | player_index = data_file['player_index'] 21 | entities = data_file['entities'] 22 | game_map = data_file['game_map'] 23 | message_log = data_file['message_log'] 24 | game_state = data_file['game_state'] 25 | 26 | player = entities[player_index] 27 | 28 | return player, entities, game_map, message_log, game_state 29 | -------------------------------------------------------------------------------- /loader_functions/initialize_new_game.py: -------------------------------------------------------------------------------- 1 | from components.equipment import Equipment 2 | from components.equippable import Equippable 3 | from components.fighter import Fighter 4 | from components.inventory import Inventory 5 | from components.level import Level 6 | 7 | from entity import Entity 8 | 9 | from equipment_slots import EquipmentSlots 10 | 11 | from game_messages import MessageLog 12 | 13 | from game_states import GameStates 14 | 15 | from map_utils import GameMap, make_map 16 | 17 | from render_functions import RenderOrder 18 | 19 | 20 | def get_constants(): 21 | window_title = 'Roguelike Tutorial Revised' 22 | 23 | screen_width = 80 24 | screen_height = 50 25 | 26 | bar_width = 20 27 | panel_height = 7 28 | panel_y = screen_height - panel_height 29 | 30 | message_x = bar_width + 2 31 | message_width = screen_width - bar_width - 2 32 | message_height = panel_height - 1 33 | 34 | map_width = 80 35 | map_height = 43 36 | 37 | room_max_size = 10 38 | room_min_size = 6 39 | max_rooms = 30 40 | 41 | fov_algorithm = 'BASIC' 42 | fov_light_walls = True 43 | fov_radius = 10 44 | 45 | max_monsters_per_room = 3 46 | max_items_per_room = 2 47 | 48 | colors = { 49 | 'dark_wall': (0, 0, 100), 50 | 'dark_ground': (50, 50, 150), 51 | 'light_wall': (130, 110, 50), 52 | 'light_ground': (200, 180, 50), 53 | 'desaturated_green': (63, 127, 63), 54 | 'darker_green': (0, 127, 0), 55 | 'dark_red': (191, 0, 0), 56 | 'white': (255, 255, 255), 57 | 'black': (0, 0, 0), 58 | 'red': (255, 0, 0), 59 | 'orange': (255, 127, 0), 60 | 'light_red': (255, 114, 114), 61 | 'darker_red': (127, 0, 0), 62 | 'violet': (127, 0, 255), 63 | 'yellow': (255, 255, 0), 64 | 'blue': (0, 0, 255), 65 | 'green': (0, 255, 0), 66 | 'light_cyan': (114, 255, 255), 67 | 'light_pink': (255, 114, 184), 68 | 'light_yellow': (255, 255, 114), 69 | 'light_violet': (184, 114, 255), 70 | 'sky': (0, 191, 255), 71 | 'darker_orange': (127, 63, 0) 72 | } 73 | 74 | constants = { 75 | 'window_title': window_title, 76 | 'screen_width': screen_width, 77 | 'screen_height': screen_height, 78 | 'bar_width': bar_width, 79 | 'panel_height': panel_height, 80 | 'panel_y': panel_y, 81 | 'message_x': message_x, 82 | 'message_width': message_width, 83 | 'message_height': message_height, 84 | 'map_width': map_width, 85 | 'map_height': map_height, 86 | 'room_max_size': room_max_size, 87 | 'room_min_size': room_min_size, 88 | 'max_rooms': max_rooms, 89 | 'fov_algorithm': fov_algorithm, 90 | 'fov_light_walls': fov_light_walls, 91 | 'fov_radius': fov_radius, 92 | 'max_monsters_per_room': max_monsters_per_room, 93 | 'max_items_per_room': max_items_per_room, 94 | 'colors': colors 95 | } 96 | 97 | return constants 98 | 99 | 100 | def get_game_variables(constants): 101 | fighter_component = Fighter(hp=100, defense=1, power=2) 102 | inventory_component = Inventory(26) 103 | level_component = Level() 104 | equipment_component = Equipment() 105 | player = Entity(0, 0, '@', (255, 255, 255), 'Player', blocks=True, render_order=RenderOrder.ACTOR, 106 | fighter=fighter_component, inventory=inventory_component, level=level_component, 107 | equipment=equipment_component) 108 | entities = [player] 109 | 110 | equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=2) 111 | dagger = Entity(0, 0, '-', constants['colors'].get('sky'), 'Dagger', equippable=equippable_component) 112 | player.inventory.add_item(dagger, constants['colors']) 113 | player.equipment.toggle_equip(dagger) 114 | 115 | game_map = GameMap(constants['map_width'], constants['map_height']) 116 | make_map(game_map, constants['max_rooms'], constants['room_min_size'], 117 | constants['room_max_size'], constants['map_width'], constants['map_height'], player, entities, 118 | constants['colors']) 119 | 120 | message_log = MessageLog(constants['message_x'], constants['message_width'], 121 | constants['message_height']) 122 | 123 | game_state = GameStates.PLAYERS_TURN 124 | 125 | return player, entities, game_map, message_log, game_state 126 | -------------------------------------------------------------------------------- /map_utils.py: -------------------------------------------------------------------------------- 1 | from tdl.map import Map 2 | 3 | from random import randint 4 | 5 | from components.ai import BasicMonster 6 | from components.equipment import EquipmentSlots 7 | from components.equippable import Equippable 8 | from components.fighter import Fighter 9 | from components.item import Item 10 | from components.stairs import Stairs 11 | 12 | from entity import Entity 13 | 14 | from game_messages import Message 15 | 16 | from item_functions import cast_confuse, cast_fireball, cast_lightning, heal 17 | 18 | from random_utils import from_dungeon_level, random_choice_from_dict 19 | 20 | from render_functions import RenderOrder 21 | 22 | 23 | class GameMap(Map): 24 | def __init__(self, width, height, dungeon_level=1): 25 | super().__init__(width, height) 26 | self.explored = [[False for y in range(height)] for x in range(width)] 27 | 28 | self.dungeon_level = dungeon_level 29 | 30 | 31 | class Rect: 32 | def __init__(self, x, y, w, h): 33 | self.x1 = x 34 | self.y1 = y 35 | self.x2 = x + w 36 | self.y2 = y + h 37 | 38 | def center(self): 39 | center_x = int((self.x1 + self.x2) / 2) 40 | center_y = int((self.y1 + self.y2) / 2) 41 | return (center_x, center_y) 42 | 43 | def intersect(self, other): 44 | # returns true if this rectangle intersects with another one 45 | return (self.x1 <= other.x2 and self.x2 >= other.x1 and 46 | self.y1 <= other.y2 and self.y2 >= other.y1) 47 | 48 | 49 | def create_room(game_map, room): 50 | # go through the tiles in the rectangle and make them passable 51 | for x in range(room.x1 + 1, room.x2): 52 | for y in range(room.y1 + 1, room.y2): 53 | game_map.walkable[x, y] = True 54 | game_map.transparent[x, y] = True 55 | 56 | 57 | def create_h_tunnel(game_map, x1, x2, y): 58 | for x in range(min(x1, x2), max(x1, x2) + 1): 59 | game_map.walkable[x, y] = True 60 | game_map.transparent[x, y] = True 61 | 62 | 63 | def create_v_tunnel(game_map, y1, y2, x): 64 | for y in range(min(y1, y2), max(y1, y2) + 1): 65 | game_map.walkable[x, y] = True 66 | game_map.transparent[x, y] = True 67 | 68 | 69 | def place_entities(room, entities, dungeon_level, colors): 70 | max_monsters_per_room = from_dungeon_level([[2, 1], [3, 4], [5, 6]], dungeon_level) 71 | max_items_per_room = from_dungeon_level([[1, 1], [2, 4]], dungeon_level) 72 | 73 | # Get a random number of monsters 74 | number_of_monsters = randint(0, max_monsters_per_room) 75 | number_of_items = randint(0, max_items_per_room) 76 | 77 | monster_chances = { 78 | 'orc': 80, 79 | 'troll': from_dungeon_level([[15, 3], [30, 5], [60, 7]], dungeon_level) 80 | } 81 | 82 | item_chances = { 83 | 'healing_potion': 35, 84 | 'sword': from_dungeon_level([[5, 4]], dungeon_level), 85 | 'shield': from_dungeon_level([[15, 8]], dungeon_level), 86 | 'lightning_scroll': from_dungeon_level([[25, 4]], dungeon_level), 87 | 'fireball_scroll': from_dungeon_level([[25, 6]], dungeon_level), 88 | 'confusion_scroll': from_dungeon_level([[10, 2]], dungeon_level) 89 | } 90 | 91 | for i in range(number_of_monsters): 92 | # Choose a random location in the room 93 | x = randint(room.x1 + 1, room.x2 - 1) 94 | y = randint(room.y1 + 1, room.y2 - 1) 95 | 96 | if not any([entity for entity in entities if entity.x == x and entity.y == y]): 97 | monster_choice = random_choice_from_dict(monster_chances) 98 | 99 | if monster_choice == 'orc': 100 | fighter_component = Fighter(hp=20, defense=0, power=4, xp=35) 101 | ai_component = BasicMonster() 102 | 103 | monster = Entity(x, y, 'o', colors.get('desaturated_green'), 'Orc', blocks=True, 104 | render_order=RenderOrder.ACTOR, fighter=fighter_component, ai=ai_component) 105 | else: 106 | fighter_component = Fighter(hp=30, defense=2, power=8, xp=100) 107 | ai_component = BasicMonster() 108 | 109 | monster = Entity(x, y, 'T', colors.get('darker_green'), 'Troll', blocks=True, 110 | render_order=RenderOrder.ACTOR, fighter=fighter_component, ai=ai_component) 111 | 112 | entities.append(monster) 113 | 114 | for i in range(number_of_items): 115 | x = randint(room.x1 + 1, room.x2 - 1) 116 | y = randint(room.y1 + 1, room.y2 - 1) 117 | 118 | if not any([entity for entity in entities if entity.x == x and entity.y == y]): 119 | item_choice = random_choice_from_dict(item_chances) 120 | 121 | if item_choice == 'healing_potion': 122 | item_component = Item(use_function=heal, amount=40) 123 | item = Entity(x, y, '!', colors.get('violet'), 'Healing Potion', render_order=RenderOrder.ITEM, 124 | item=item_component) 125 | elif item_choice == 'sword': 126 | equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=3) 127 | item = Entity(x, y, '/', colors.get('sky'), 'Sword', equippable=equippable_component) 128 | elif item_choice == 'shield': 129 | equippable_component = Equippable(EquipmentSlots.OFF_HAND, defense_bonus=1) 130 | item = Entity(x, y, '[', colors.get('darker_orange'), 'Shield', equippable=equippable_component) 131 | elif item_choice == 'fireball_scroll': 132 | item_component = Item(use_function=cast_fireball, targeting=True, targeting_message=Message( 133 | 'Left-click a target tile for the fireball, or right-click to cancel.', colors.get('light_cyan')), 134 | damage=25, radius=3) 135 | item = Entity(x, y, '#', colors.get('red'), 'Fireball Scroll', render_order=RenderOrder.ITEM, 136 | item=item_component) 137 | elif item_choice == 'confusion_scroll': 138 | item_component = Item(use_function=cast_confuse, targeting=True, targeting_message=Message( 139 | 'Left-click an enemy to confuse it, or right-click to cancel.', colors.get('light_cyan'))) 140 | item = Entity(x, y, '#', colors.get('light_pink'), 'Confusion Scroll', render_order=RenderOrder.ITEM, 141 | item=item_component) 142 | else: 143 | item_component = Item(use_function=cast_lightning, damage=20, maximum_range=5) 144 | item = Entity(x, y, '#', colors.get('yellow'), 'Lightning Scroll', render_order=RenderOrder.ITEM, 145 | item=item_component) 146 | 147 | entities.append(item) 148 | 149 | 150 | def make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities, colors): 151 | rooms = [] 152 | num_rooms = 0 153 | 154 | center_of_last_room_x = None 155 | center_of_last_room_y = None 156 | 157 | for r in range(max_rooms): 158 | # random width and height 159 | w = randint(room_min_size, room_max_size) 160 | h = randint(room_min_size, room_max_size) 161 | # random position without going out of the boundaries of the map 162 | x = randint(0, map_width - w - 1) 163 | y = randint(0, map_height - h - 1) 164 | 165 | # "Rect" class makes rectangles easier to work with 166 | new_room = Rect(x, y, w, h) 167 | 168 | # run through the other rooms and see if they intersect with this one 169 | for other_room in rooms: 170 | if new_room.intersect(other_room): 171 | break 172 | else: 173 | # this means there are no intersections, so this room is valid 174 | 175 | # "paint" it to the map's tiles 176 | create_room(game_map, new_room) 177 | 178 | # center coordinates of new room, will be useful later 179 | (new_x, new_y) = new_room.center() 180 | 181 | center_of_last_room_x = new_x 182 | center_of_last_room_y = new_y 183 | 184 | if num_rooms == 0: 185 | # this is the first room, where the player starts at 186 | player.x = new_x 187 | player.y = new_y 188 | else: 189 | # all rooms after the first: 190 | # connect it to the previous room with a tunnel 191 | 192 | # center coordinates of previous room 193 | (prev_x, prev_y) = rooms[num_rooms - 1].center() 194 | 195 | # flip a coin (random number that is either 0 or 1) 196 | if randint(0, 1) == 1: 197 | # first move horizontally, then vertically 198 | create_h_tunnel(game_map, prev_x, new_x, prev_y) 199 | create_v_tunnel(game_map, prev_y, new_y, new_x) 200 | else: 201 | # first move vertically, then horizontally 202 | create_v_tunnel(game_map, prev_y, new_y, prev_x) 203 | create_h_tunnel(game_map, prev_x, new_x, new_y) 204 | 205 | place_entities(new_room, entities, game_map.dungeon_level, colors) 206 | 207 | # finally, append the new room to the list 208 | rooms.append(new_room) 209 | num_rooms += 1 210 | 211 | stairs_component = Stairs(game_map.dungeon_level + 1) 212 | down_stairs = Entity(center_of_last_room_x, center_of_last_room_y, '>', (255, 255, 255), 'Stairs', 213 | render_order=RenderOrder.STAIRS, stairs=stairs_component) 214 | entities.append(down_stairs) 215 | 216 | 217 | def next_floor(player, message_log, dungeon_level, constants): 218 | game_map = GameMap(constants['map_width'], constants['map_height'], dungeon_level) 219 | entities = [player] 220 | 221 | make_map(game_map, constants['max_rooms'], constants['room_min_size'], 222 | constants['room_max_size'], constants['map_width'], constants['map_height'], player, entities, 223 | constants['colors']) 224 | 225 | player.fighter.heal(player.fighter.max_hp // 2) 226 | 227 | message_log.add_message(Message('You take a moment to rest, and recover your strength.', 228 | constants['colors'].get('light_violet'))) 229 | 230 | return game_map, entities 231 | -------------------------------------------------------------------------------- /menu_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TStand90/roguelike_tutorial_revised_tdl/f50a99b1020ca2cca3920970938a76467df99a3e/menu_background.png -------------------------------------------------------------------------------- /menus.py: -------------------------------------------------------------------------------- 1 | import tdl 2 | 3 | import textwrap 4 | 5 | 6 | def menu(con, root, header, options, width, screen_width, screen_height): 7 | if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.') 8 | 9 | # calculate total height for the header (after textwrap) and one line per option 10 | header_wrapped = textwrap.wrap(header, width) 11 | header_height = len(header_wrapped) 12 | height = len(options) + header_height 13 | 14 | # create an off-screen console that represents the menu's window 15 | window = tdl.Console(width, height) 16 | 17 | # print the header, with wrapped text 18 | window.draw_rect(0, 0, width, height, None, fg=(255, 255, 255), bg=None) 19 | for i, line in enumerate(header_wrapped): 20 | window.draw_str(0, 0 + i, header_wrapped[i]) 21 | 22 | y = header_height 23 | letter_index = ord('a') 24 | for option_text in options: 25 | text = '(' + chr(letter_index) + ') ' + option_text 26 | window.draw_str(0, y, text, bg=None) 27 | y += 1 28 | letter_index += 1 29 | 30 | # blit the contents of "window" to the root console 31 | x = screen_width // 2 - width // 2 32 | y = screen_height // 2 - height // 2 33 | root.blit(window, x, y, width, height, 0, 0) 34 | 35 | 36 | def inventory_menu(con, root, header, player, inventory_width, screen_width, screen_height): 37 | # show a menu with each item of the inventory as an option 38 | if len(player.inventory.items) == 0: 39 | options = ['Inventory is empty.'] 40 | else: 41 | options = [] 42 | 43 | for item in player.inventory.items: 44 | if player.equipment.main_hand == item: 45 | options.append('{0} (on main hand)'.format(item.name)) 46 | elif player.equipment.off_hand == item: 47 | options.append('{0} (on off hand)'.format(item.name)) 48 | else: 49 | options.append(item.name) 50 | 51 | menu(con, root, header, options, inventory_width, screen_width, screen_height) 52 | 53 | 54 | def main_menu(con, root_console, background_image, screen_width, screen_height, colors): 55 | background_image.blit_2x(root_console, 0, 0) 56 | 57 | title = 'TOMBS OF THE ANCIENT KINGS' 58 | center = (screen_width - len(title)) // 2 59 | root_console.draw_str(center, screen_height // 2 - 4, title, bg=None, fg=colors.get('light_yellow')) 60 | 61 | title = 'By (Your name here)' 62 | center = (screen_width - len(title)) // 2 63 | root_console.draw_str(center, screen_height - 2, title, bg=None, fg=colors.get('light_yellow')) 64 | 65 | menu(con, root_console, '', ['Play a new game', 'Continue last game', 'Quit'], 24, screen_width, screen_height) 66 | 67 | 68 | def level_up_menu(con, root, header, player, menu_width, screen_width, screen_height): 69 | options = ['Constitution (+20 HP, from {0})'.format(player.fighter.max_hp), 70 | 'Strength (+1 attack, from {0})'.format(player.fighter.power), 71 | 'Agility (+1 defense, from {0})'.format(player.fighter.defense)] 72 | 73 | menu(con, root, header, options, menu_width, screen_width, screen_height) 74 | 75 | 76 | def character_screen(root_console, player, character_screen_width, character_screen_height, screen_width, 77 | screen_height): 78 | window = tdl.Console(character_screen_width, character_screen_height) 79 | 80 | window.draw_rect(0, 0, character_screen_width, character_screen_height, None, fg=(255, 255, 255), bg=None) 81 | 82 | window.draw_str(0, 1, 'Character Information') 83 | window.draw_str(0, 2, 'Level: {0}'.format(player.level.current_level)) 84 | window.draw_str(0, 3, 'Experience: {0}'.format(player.level.current_xp)) 85 | window.draw_str(0, 4, 'Experience to Level: {0}'.format(player.level.experience_to_next_level)) 86 | window.draw_str(0, 6, 'Maximum HP: {0}'.format(player.fighter.max_hp)) 87 | window.draw_str(0, 7, 'Attack: {0}'.format(player.fighter.power)) 88 | window.draw_str(0, 8, 'Defense: {0}'.format(player.fighter.defense)) 89 | 90 | x = screen_width // 2 - character_screen_width // 2 91 | y = screen_height // 2 - character_screen_height // 2 92 | root_console.blit(window, x, y, character_screen_width, character_screen_height, 0, 0) 93 | 94 | 95 | def message_box(con, root_console, header, width, screen_width, screen_height): 96 | menu(con, root_console, header, [], width, screen_width, screen_height) 97 | -------------------------------------------------------------------------------- /random_utils.py: -------------------------------------------------------------------------------- 1 | from numpy.random import choice 2 | 3 | 4 | def from_dungeon_level(table, dungeon_level): 5 | for (value, level) in reversed(table): 6 | if dungeon_level >= level: 7 | return value 8 | 9 | return 0 10 | 11 | 12 | def random_choice_from_dict(choice_dict): 13 | choices = list(choice_dict.keys()) 14 | chances = list(choice_dict.values()) 15 | 16 | decimal_chances = [chance / sum(chances) for chance in chances] 17 | 18 | return choice(choices, p=decimal_chances) 19 | -------------------------------------------------------------------------------- /render_functions.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from game_states import GameStates 4 | 5 | from menus import character_screen, inventory_menu, level_up_menu 6 | 7 | 8 | class RenderOrder(Enum): 9 | STAIRS = 1 10 | CORPSE = 2 11 | ITEM = 3 12 | ACTOR = 4 13 | 14 | 15 | def get_names_under_mouse(mouse_coordinates, entities, game_map): 16 | x, y = mouse_coordinates 17 | 18 | names = [entity.name for entity in entities 19 | if entity.x == x and entity.y == y and game_map.fov[entity.x, entity.y]] 20 | names = ', '.join(names) 21 | 22 | return names.capitalize() 23 | 24 | 25 | def render_bar(panel, x, y, total_width, name, value, maximum, bar_color, back_color, string_color): 26 | # Render a bar (HP, experience, etc). first calculate the width of the bar 27 | bar_width = int(float(value) / maximum * total_width) 28 | 29 | # Render the background first 30 | panel.draw_rect(x, y, total_width, 1, None, bg=back_color) 31 | 32 | # Now render the bar on top 33 | if bar_width > 0: 34 | panel.draw_rect(x, y, bar_width, 1, None, bg=bar_color) 35 | 36 | # Finally, some centered text with the values 37 | text = name + ': ' + str(value) + '/' + str(maximum) 38 | x_centered = x + int((total_width-len(text)) / 2) 39 | 40 | panel.draw_str(x_centered, y, text, fg=string_color, bg=None) 41 | 42 | 43 | def render_all(con, panel, entities, player, game_map, fov_recompute, root_console, message_log, screen_width, 44 | screen_height, bar_width, panel_height, panel_y, mouse_coordinates, colors, game_state): 45 | # Draw all the tiles in the game map 46 | if fov_recompute: 47 | for x, y in game_map: 48 | wall = not game_map.transparent[x, y] 49 | 50 | if game_map.fov[x, y]: 51 | if wall: 52 | con.draw_char(x, y, None, fg=None, bg=colors.get('light_wall')) 53 | else: 54 | con.draw_char(x, y, None, fg=None, bg=colors.get('light_ground')) 55 | 56 | game_map.explored[x][y] = True 57 | elif game_map.explored[x][y]: 58 | if wall: 59 | con.draw_char(x, y, None, fg=None, bg=colors.get('dark_wall')) 60 | else: 61 | con.draw_char(x, y, None, fg=None, bg=colors.get('dark_ground')) 62 | 63 | # Draw all entities in the list 64 | entities_in_render_order = sorted(entities, key=lambda x: x.render_order.value) 65 | 66 | for entity in entities_in_render_order: 67 | draw_entity(con, entity, game_map) 68 | 69 | con.draw_str(1, screen_height - 2, 'HP: {0:02}/{1:02}'.format(player.fighter.hp, player.fighter.max_hp)) 70 | 71 | root_console.blit(con, 0, 0, screen_width, screen_height, 0, 0) 72 | 73 | panel.clear(fg=colors.get('white'), bg=colors.get('black')) 74 | 75 | # Print the game messages, one line at a time 76 | y = 1 77 | for message in message_log.messages: 78 | panel.draw_str(message_log.x, y, message.text, bg=None, fg=message.color) 79 | y += 1 80 | 81 | render_bar(panel, 1, 1, bar_width, 'HP', player.fighter.hp, player.fighter.max_hp, 82 | colors.get('light_red'), colors.get('darker_red'), colors.get('white')) 83 | 84 | panel.draw_str(1, 0, get_names_under_mouse(mouse_coordinates, entities, game_map)) 85 | panel.draw_str(1, 3, 'Dungeon Level: {0}'.format(game_map.dungeon_level), fg=colors.get('white'), bg=None) 86 | 87 | root_console.blit(panel, 0, panel_y, screen_width, panel_height, 0, 0) 88 | 89 | if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY): 90 | if game_state == GameStates.SHOW_INVENTORY: 91 | inventory_title = 'Press the key next to an item to use it, or Esc to cancel.\n' 92 | else: 93 | inventory_title = 'Press the key next to an item to drop it, or Esc to cancel.\n' 94 | 95 | inventory_menu(con, root_console, inventory_title, player, 50, screen_width, screen_height) 96 | 97 | elif game_state == GameStates.LEVEL_UP: 98 | level_up_menu(con, root_console, 'Level up! Choose a stat to raise:', player, 40, screen_width, 99 | screen_height) 100 | 101 | elif game_state == GameStates.CHARACTER_SCREEN: 102 | character_screen(root_console, player, 30, 10, screen_width, screen_height) 103 | 104 | 105 | def clear_all(con, entities): 106 | for entity in entities: 107 | clear_entity(con, entity) 108 | 109 | 110 | def draw_entity(con, entity, game_map): 111 | if game_map.fov[entity.x, entity.y] or (entity.stairs and game_map.explored[entity.x][entity.y]): 112 | con.draw_char(entity.x, entity.y, entity.char, entity.color, bg=None) 113 | 114 | 115 | def clear_entity(con, entity): 116 | # erase the character that represents this object 117 | con.draw_char(entity.x, entity.y, ' ', entity.color, bg=None) 118 | --------------------------------------------------------------------------------