Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

Python

Krasimir Georgiev
Krasimir Georgiev
19,146 Points

Updated Dungeon Game

Hey guys so here is an updated version of the dungeon game.

Sadly I had to implement classes so it steps out of the Python Collections course, but I hope you like it nevertheless.

Game features:

  • Monsters which are fightable
  • Map generation
  • Armor and Weapons which help you even the odds against the monsters
  • 4 difficulties

Suggestions on how to improve the code or the game are very welcome.

EDIT: Fixed style and changed the way types and rarity is chosen in item.py

dungeon_game.py

import random
from combat import Combat
from monster import *
from character import Character
from item import *
from monster_spawner import MonsterSpawner
from item_spawner import ItemSpawner
from map import Map


def choose_difficulty():
    print("Difficulties: \n Easy, Normal, Hard, Legendary")
    difficulty_choice = input("On which difficulty would you like to play?")
    difficulty_choice = difficulty_choice.lower()
    if difficulty_choice == "easy":
        difficulty = 1
    elif difficulty_choice == "normal":
        difficulty = 2
    elif difficulty_choice == "hard":
        difficulty = 3
    elif difficulty_choice == "legendary":
        difficulty = 4
    return difficulty


def spawn_monster_guarding_item(monster_spawner, item_spawner):
    location = map.get_location()
    monster_spawner.spawn_monster(location)
    item_spawner.spawn_item(location)


def show_help():
    print("Type QUIT to quit,CHEAT to see other entities' position,\n" +
          "STATS to see your stats,The direction you would like to" +
          " go in to move\nand HELP to see this message")

game_over = False
cheat_activated = False
play_level = True
go_to_next_level = False
exit_immediatly = False
show_help()
player = Character()
difficulty = choose_difficulty()
max_level = difficulty * 5 - 1
print("You wake up in a dark maze.")
level = 0
counter = 0
while True:
    if level >= max_level:
        print("Congratulations! You managed to escape the maze!")
        break

    if exit_immediatly:
        break

    if game_over:
        replay = input("Would you like to play again?")
        replay = replay.lower()
        if replay == "yes" or replay == "y":
            player = Character()
            gameOver = False
            monster_spawner.monster_list = []
            item_spawner.item_list = []
            level = 0
            counter = 0
        else:
            break

    monster_spawner = MonsterSpawner(level, difficulty)
    item_spawner = ItemSpawner(level, difficulty)

    if go_to_next_level:
        counter = 0
        level += 1
        go_to_next_level = False
        monster_spawner.monster_list = []
        item_spawner.item_list = []

    map_rows = 3 + round(level * 0.49)
    map_collumns = 3 + round(level * 0.49)

    map = Map()
    map.generate_map(map_rows, map_collumns)

    player.pos = map.get_location()
    player.pos_x, player.pos_y = player.pos
    door_pos = map.get_location()

    print("Starting level {}".format(level + 1))
    spawn_monster_guarding_item(monster_spawner, item_spawner)

    while play_level:

        if counter == 22 - level:
            spawn_monster_guarding_item(monster_spawner, item_spawner)
            counter = 0

        if cheat_activated:
            print("Player position {}".format(player.pos))
            print("Door position {}".format(door_pos))
            for monster in monster_spawner.monster_list:
                print("Monster position {}".format(monster.pos))
            for item in item_spawner.item_list:
                print("Item position {}".format(item.pos))

        player.heal()
        map.draw_map(player.pos)
        direction_dict = map.collision_detection(player.pos)

        direction = input("Where would you like to go?")
        direction = direction.lower()
        if direction_dict["S"] and (direction == "s" or direction == "south" or direction == "d" or direction == "down"):
            player.pos_y += 1
        elif direction_dict["N"] and (direction == "n" or direction == "north" or direction == "u" or direction == "up"):
            player.pos_y -= 1
        elif direction_dict["E"] and (direction == "e" or direction == "east" or direction == "r" or direction == "right"):
            player.pos_x += 1
        elif direction_dict["W"] and (direction == "w" or direction == "west" or direction == "l" or direction == "left"):
            player.pos_x -= 1
        elif direction == "cheat":
            cheat_activated = True
            print("Cheating activated")
            continue
        elif direction == "stats":
            player.show_stats()
            continue
        elif direction == "help":
            show_help()
            continue
        elif direction == "quit":
            exit_immediatly = True
            break
        else:
            print("Command not recognized and/or you aren't allowed to go there.")
            continue
        player.pos = player.pos_x, player.pos_y
        counter += 1

        for monster in monster_spawner.monster_list:
            if player.pos == monster.pos:
                print("You have encountered a {}".format(monster))
                battle = Combat()
                while player.hit_points > 0 and monster.hit_points > 0:
                    battle.weapon_attack(player, monster)
                    battle.weapon_attack(monster, player)
                if player.hit_points <= 0:
                    print("You have been defeated by {}".format(monster))
                    game_over = True
                    break
                elif monster.hit_points <= 0:
                    player.experience += monster.experience
                    print("Player has defeated {}, earning {} experience".format(monster, monster.experience))
                    monster_spawner.monster_list.remove(monster)

        if game_over:
            break

        for item in item_spawner.item_list:
            if player.pos == item.pos:
                print("You have found a:\n{}".format(item))
                get_item = input("Would you like to take it?")
                get_item = get_item.lower()
                if get_item == 'y' or get_item == 'yes':
                    print("You took a {} {}".format(item.type, item.name))
                    if item.__class__.__name__ == "Armor":
                        player.set_armor(item)
                    else:
                        player.set_weapon(item)
                item_spawner.item_list.remove(item)

        player.level_up()

        if player.pos == door_pos:
            print("You managed to get to the door!")
            go_to_next_level = True
            break

print("Thank you for playing! I hope you come back soon!")

character.py

import random
from map import Map
from item import *


class Character:
    level = 1
    level_up_experience = 3
    experience = 0
    hit_points = 10
    max_hit_points = 10
    heal_rate = 1

    def __init__(self, **kwargs):
        self.weapon = Weapon(0, 5, "stick")
        self.armor = Armor(1, "robes")
        for key, value in kwargs.items():
            setattr(self, key, value)

    def display_stats(self):
        print("Player stats: \n Level: {}, Experience: {}, {}".format(self.level, self.experience, self.max_hit_points))

    def __str__(self):
        return "Player"

    def show_stats(self):
        print("Player Level: {}\nExperience: {}\nHealth: {}\nHeal Rate: {}\nArmor: {}\nWeapon: {}".format(self.level, self.experience, self.hit_points, self.heal_rate, self.armor, self.weapon))

    def level_up(self):
        if self.experience >= self.level_up_experience:
            constant = 0.335 - (self.level * 0.005)
            self.level_up_experience = round((self.level / constant) * (self.level / constant))
            self.max_hit_points += 4
            self.level += 1
            if self.level % 3 == 0:
                self.heal_rate += 1
            print("You leveled up to level {}.Next level requires {} experience".format(self.level, self.level_up_experience))

    def set_weapon(self, weapon):
        self.weapon = weapon

    def set_armor(self, armor):
        self.armor = armor

    def heal(self):
        if self.hit_points < self.max_hit_points:
            self.hit_points += self.heal_rate
        while self.hit_points > self.max_hit_points:
            self.hit_points -= 1

combat.py

import random


class Combat:

    def weapon_attack(self, attacking_entity, defending_entity):
        experience_difference = attacking_entity.experience - defending_entity.experience
        roll = random.randint(0, 100)
        roll += experience_difference
        damage = attacking_entity.weapon.attack() - defending_entity.armor.defense
        if damage < 0:
            damage = 0
        if roll <= 10:
            counter_damage = defending_entity.weapon.attack() - attacking_entity.armor.defense
            if counter_damage <= 0:
                counter_damage = 1
            attacking_entity.hit_points -= counter_damage
            print("{} has attacked {} but {} has countered {}'s attack, dealing {} damage".format(attacking_entity, defending_entity, defending_entity, attacking_entity, counter_damage))
        elif roll <= 30:
            print("{} has attacked {} but {} has dodged {}'s attack, evading {} damage".format(attacking_entity, defending_entity, defending_entity, attacking_entity, damage))
        else:
            defending_entity.hit_points -= damage
            print("{} has attacked {}, dealing {} damage".format(attacking_entity, defending_entity, damage))

item.py

import random
RARITY = ['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary']
WEAPON_TYPES = ['Training', 'Stone', 'Iron', 'Steel', 'Silver', 'Dragonbone']
ARMOR_TYPES = ['Cloth', 'Leather', 'Iron', 'Steel', 'Silver', 'Dragonbone']


class Item:

    def set_item_attr(self, **kwargs):
        rarity_chance = random.randint(0, 100)
        rarity_counter = 0
        self.power = 0
        for rarity_type in RARITY:
            if rarity_chance <= round((100 - 10 * rarity_counter) * (rarity_counter + 1) / len(RARITY) + 10 * rarity_counter):
                break
            else:
                self.power += 5
                rarity_counter += 1
        self.type = RARITY[rarity_counter]
        for (key, value) in kwargs.items():
            setattr(self, key, value)

    def __str__(self):
        if self.__class__.__name__ == "Armor":
            stats = "Defense: {}".format(self.defense)
        else:
            stats = "Attack range: {} - {}".format(self.min_attack, self.max_attack)
        return "{} {}\n {} ".format(self.type, self.name, stats)


class Weapon(Item):

    def __init__(self, min_attack, max_attack, name, **kwargs):
        self.set_item_attr(**kwargs)
        self.name = name
        self.min_attack = min_attack
        self.max_attack = max_attack
        combined_attack = self.min_attack + self.max_attack
        type_counter = 0
        for weapon_type in WEAPON_TYPES:
            if combined_attack <= (100 * (type_counter + 1)) / len(WEAPON_TYPES):
                break
            else:
                type_counter += 1
        try:
            type = WEAPON_TYPES[type_counter]
        except:
            type = WEAPON_TYPES[type_counter - 1]
        self.type += " {}".format(type)

    def attack(self):
        attack = random.randint(self.min_attack, self.max_attack)
        return attack


class Armor(Item):

    def __init__(self, defense, name, **kwargs):
        self.set_item_attr(**kwargs)
        self.name = name
        self.defense = defense
        type_counter = 0
        for armor in ARMOR_TYPES:
            if defense <= (50 * (type_counter + 1)) / len(ARMOR_TYPES):
                break
            else:
                type_counter += 1
        try:
            type = ARMOR_TYPES[type_counter]
        except:
            type = ARMOR_TYPES[type_counter - 1]
        self.type += " {}".format(type)

item_spawner.py

from item import *
WEAPON_NAMES = ['Dagger', 'Sword', 'Greatsword', 'Mace', 'Axe', 'Battle Axe', 'Katana']
ARMOR_NAMES = ["Hunter's Armor", 'Griffin Armor', 'Armor', 
               'Dragonslayer Armor', 'Guard Outfit', 'Kingsguard Armor', 'High Priest Outfit']


class ItemSpawner:
    item_list = []

    def __init__(self, level, difficulty):
        self.level = level
        self.difficulty = difficulty

    def spawn_item(self, location):
        spawn_chance = random.randint(0, 100)
        if spawn_chance < 50:
            armor = round((random.randint(0, 5) * (5 - self.difficulty)) + self.level)
            item = Armor(armor, random.choice(ARMOR_NAMES))
            item.defense = item.defense + round(item.power / self.difficulty)
        else:
            min_attack = round(random.randint(0, 5) * (5 - self.difficulty) + self.level)
            max_attack = round(random.randint(5, 10) * (5 - self.difficulty) + self.level)
            item = Weapon(min_attack, max_attack, random.choice(WEAPON_NAMES))
            item.min_attack = item.min_attack + round(item.power / self.difficulty)
            item.max_attack = item.max_attack + round(item.power / self.difficulty)
        item.pos = location
        self.item_list.append(item)

map.py

import random


class Map:
    available_space = []
    game_map = []
    map_borders = ()

    def generate_map(self, rows, collumns):
        self.game_map = []
        self.available_space = []
        self.map_borders = rows - 1, collumns - 1
        for x in range(0, rows):
            for y in range(0, collumns):
                self.game_map.append((x, y))
                self.available_space.append((x, y))

    def get_location(self):
        map_space = random.choice(self.available_space)
        map_idx = self.available_space.index(map_space)
        game_map_idx = self.game_map.index(map_space)
        location = self.game_map[game_map_idx]
        del self.available_space[map_idx]
        return location

    def collision_detection(self, entity_pos):
        dict_of_directions = {}
        map_x, map_y = self.map_borders
        shadow_pos_x, shadow_pos_y = entity_pos
        shadow_pos_y -= 1
        if shadow_pos_y <= map_y and shadow_pos_y >= 0:
            dict_of_directions['N'] = True
        else:
            dict_of_directions['N'] = False
        shadow_pos_y += 2
        if shadow_pos_y <= map_y and shadow_pos_y >= 0:
            dict_of_directions['S'] = True
        else:
            dict_of_directions['S'] = False
        shadow_pos_x += 1
        if shadow_pos_x <= map_x and shadow_pos_x >= 0:
            dict_of_directions['E'] = True
        else:
            dict_of_directions['E'] = False
        shadow_pos_x -= 2
        if shadow_pos_x <= map_y and shadow_pos_x >= 0:
            dict_of_directions['W'] = True
        else:
            dict_of_directions['W'] = False
        return dict_of_directions

    def draw_map(self, player_pos):
        player_pos_x, player_pos_y = player_pos
        map_x, map_y = self.map_borders
        cell = ""
        counter = -1
        for y in range(-1, map_y + 1):
            counter += 1
            if counter != 0:
                print(" " + "_  " * (map_x + 1))
                print(cell)
            cell = ""
            for x in range(0, map_x + 1):
                if player_pos == (x, y + 1):
                    cell += "|{}|".format("X")
                else:
                    cell += "|{}|".format("_")

monster.py

import random
from map import Map
from item import *


class Monster:
    min_hit_points = 2
    max_hit_points = 5
    min_experience = 1
    max_experience = 5

    def choose_weapon(self):
        weapon_choice = random.randint(0, 10)
        if weapon_choice <= 4:
            self.weapon = Weapon(1, 2, "claws")
        elif weapon_choice <= 7:
            self.weapon = Weapon(2, 4, "wooden club")
        else:
            self.weapon = Weapon(3, 6, "sword")

    def __str__(self):
        return self.__class__.__name__

    def __init__(self, difficulty, level, **kwargs):
        self.armor = Armor(0, "light armor")
        self.choose_weapon()
        self.hit_points = random.randint(self.min_hit_points,
                                         self.max_hit_points)
        self.hit_points = round(self.hit_points * difficulty + level)
        self.experience = random.randint(self.min_experience,
                                         self.max_experience)
        self.experience = round(self.experience * (difficulty / 2) + level)
        self.experience = round(self.experience * (difficulty / 2))
        self.weapon.min_attack = round(self.weapon.min_attack * (difficulty / 2) + level)
        self.weapon.max_attack = round(self.weapon.max_attack * (difficulty / 2) + level)

        for (key, value) in kwargs.items():
            setattr(self, key, value)


class Goblin(Monster):
    min_hit_points = 2
    max_hit_points = 3
    min_experience = 1
    max_experience = 3

    def choose_weapon(self):
        weapon_choice = random.randint(0, 10)
        if weapon_choice <= 6:
            self.weapon = Weapon(1, 2, "knife")
        else:
            self.weapon = Weapon(2, 4, "cleaver")


class Troll(Monster):
    min_hit_points = 5
    max_hit_points = 10
    min_experience = 2
    max_experience = 6

    def choose_weapon(self):
        weapon_choice = random.randint(0, 10)
        if weapon_choice <= 4:
            self.weapon = Weapon(1, 2, "tree branch")
        elif weapon_choice <= 7:
            self.weapon = Weapon(3, 5, "wooden club")
        else:
            self.weapon = Weapon(5, 10, "iron mace")


class Ghoul(Monster):
    min_hit_points = 2
    max_hit_points = 4
    min_experience = 1
    max_experience = 1

    def choose_weapon(self):
        self.weapon = Weapon(1, 2, "claws")


class Giant(Monster):
    min_hit_points = 4
    max_hit_points = 8
    min_experience = 1
    max_experience = 5

    def choose_weapon(self):
        weapon_choice = random.randint(0, 10)
        if weapon_choice <= 6:
            self.weapon = Weapon(1, 2, "tree branch")
        else:
            self.weapon = Weapon(3, 5, "wooden club")

monster_spawner.py

from monster import *
import random


class MonsterSpawner:
    monster_list = []

    def __init__(self, level, difficulty):
        self.level = level
        self.difficulty = difficulty 

    def spawn_monster(self, location):
        min_spawn_chance = self.difficulty * 5 + self.level
        spawn_chance = random.randint(min_spawn_chance, 100)
        if spawn_chance <= 30:
            monster = Goblin(self.difficulty, self.level)
        elif spawn_chance <= 49:
            monster = Ghoul(self.difficulty, self.level)
        elif spawn_chance <= 69:
            monster = Monster(self.difficulty, self.level)
        elif spawn_chance <= 89:
            monster = Giant(self.difficulty, self.level)
        else:
            monster = Troll(self.difficulty, self.level)
        monster.pos = location
        self.monster_list.append(monster)

1 Answer

Kenneth Love
STAFF
Kenneth Love
Treehouse Guest Teacher

Wow, that's a lot of code! Couple of style points:

  • Don't use camelCase or StuddlyCaps names for functions. Functions should always be written in snake_case. Same for variable names. StuddlyCaps is only for classes and camelCase isn't for anything in Python.
  • Make sure there are two blank lines before your function and class definitions. Class methods should have a single blank line before them.
  • Classes that don't inherit from a parent class shouldn't have parentheses on them.
  • Make sure your spacing around commas, equals, etc is consistent.

All this and more is in the Write Better Python course which you probably haven't gotten to, so no harm, no foul.

You probably want to be careful with the magic index numbers in your Armor and other places. They'll make it harder to add or remove items without changing the output of your script, possibly unpredictably.

Krasimir Georgiev
Krasimir Georgiev
19,146 Points

Thank you so much for your review Kenneth!