9
\$\begingroup\$

The following code is a simple text-based RPG game where you can move between rooms and fight with monsters until you defeat the boss.

import random
import sys


class player(object):
    name = "Hero Arius"
    hp = 200
    power = 20
    armor = 20


class gnome(object):
    name = "Flimp the Gnome"
    hp = 10
    power = 1
    armor = 3
    loot = random.randint(0, 2)


class strongerGnome(object):
    name = "Flimp+ the Gnome"
    hp = 20
    power = 2
    armor = 6
    loot = random.randint(0, 2)


class goblin(object):
    name = "Driekol the Goblin"
    hp = 30
    power = 4
    armor = 4
    loot = random.randint(0, 2)


class strongerGoblin(object):
    name = "Driekol+ the Goblin"
    hp = 60
    power = 8
    armor = 8
    loot = random.randint(0, 2)


class minotaurus(object):
    name = "Gratus the Minotaurus"
    hp = 120
    power = 10
    armor = 3
    loot = random.randint(0, 2)


class strongerMinotaurus(object):
    name = "Gratus+ the Minotaurus"
    hp = 240
    power = 20
    armor = 6
    loot = random.randint(0, 2)


class wizard(object):
    name = "Gandalf the Wizard"
    hp = 480
    power = 40
    armor = 2
    loot = random.randint(0, 2)


hero = player()
flimp = gnome()
strongerFlimp = strongerGnome()
driekol = goblin()
strongerDriekol= strongerGoblin()
gratus = minotaurus()
strongerGratus = strongerMinotaurus()
gandalf = wizard()


class boss(object):
    name = "Leoric the Skeletonking"
    hp = 960
    power = 60
    armor = 60


def gameOver(character, points):
    if character.hp <= 0:
        print(f"\n You are dead!")
        print(f"\n Thanks for playing!")
        print(f"\n Points: {points}")

        writeScore(points)
        quit()


def gameWin(points):
    print(f"\n You defeated Leoric the Skeletonking!")
    print(f"\n You successfully rescued the world! Congrats!")
    print(f"\n Points: {points}")

    writeScore(points)
    exit()


def writeScore(points):
    f = open("score.txt", "a")
    name = input("Enter your name: ")
    print(f"Player name: {name}\nPlayer points: {points}\n", file=f)
    f.close()


def loot():
    loots = ["Sword", "Armor"]
    lootChance = random.randint(0, 2)
    lootDrop = loots[lootChance]
    return lootDrop


def lootEffect(lootDrop, character):
    if lootDrop == "Sword":
        character.power = character.power + 10
        print("You got a new sword!")
        print("Power increased by 10.")
        print(f"Your power is now: {character.power}")
        return character
    elif lootDrop == "Armor":
        character.armor = character.armor + 10
        print("You got a new shield!")
        print("Armor increased by 10.")
        print(f"Your armor is now: {character.armor}")
        return character


def battle(points: int) -> int:
    global enemy
    if current_room == "Terrifying Mine":
        enemy = flimp
    elif current_room == "Tunnel of Hell":
        enemy = strongerFlimp
    elif current_room == "Deceptive Cave":
        enemy = driekol
    elif current_room == "Illusion Cave":
        enemy = strongerDriekol
    elif current_room == "Unstable Vortex":
        enemy = gratus
    elif current_room == "Imaginary Labyrinth":
        enemy = strongerGratus
    else:
        enemy = gandalf
    print(f"{enemy.name} showed up!")
    print("You have two options:")
    while enemy.hp >= 0:
        choice = input("\n [1] - Attack\n [2] - Retreat\n ")
        if choice == "1":
            print(f"\n{'-' * 27}")
            print(f"{hero.name} swung his sword, attacking {enemy.name}!")
            hitchance = random.randint(0, 10)
            if hitchance > 3:
                enemy.hp = enemy.hp - hero.power
                print(f"You wound the enemy, the enemy's life: {enemy.hp} hp")
                if enemy.hp > 1:
                    hero.hp = round(hero.hp - (enemy.power / hero.armor))
                    print(f"{enemy.name} is striking back, it has wounded you! Health: {hero.hp} hp")
                    print(f"{'-' * 27}")
                    gameOver(hero, points)
                else:
                    if enemy.name == "Flimp the Gnome":
                        points += 5
                    elif enemy.name == "Flimp+ the Gnome":
                        points += 10
                    elif enemy.name == "Driekol the Goblin":
                        points += 15
                    elif enemy.name == "Driekol+ the Goblin":
                        points += 30
                    elif enemy.name == "Gratus the Minotaurus":
                        points += 60
                    elif enemy.name == "Gratus+ the Minotaurus":
                        points += 120
                    elif enemy.name == "Gandalf the Wizard":
                        points += 240
                    print(f"You have defeated the enemy: {enemy.name}")
                    print(f"{'-' * 27}")
                    lootDrop = loot()
                    print(f"\n{'-' * 27}")
                    print(f"You have acquired an item: {lootDrop}")
                    lootEffect(lootDrop, hero)
                    print(f"{'-' * 27}")
                    return points
            else:
                print("Your sword slips from your hand, you missed the attack!")
                print(f"{enemy.name} takes this opportunity and seriously injures you!")
                hero.hp = hero.hp - enemy.power
                print(f"Health: {hero.hp} hp")
                print(f"{'-' * 27}")
                gameOver(hero, points)
        elif choice == "2":
            print(f"\n{'-' * 27}")
            runchance = random.randint(1, 10)
            if runchance > 4:
                print("You have successfully escaped!")
                print(f"{'-' * 27}")
                sys.exit()
            else:
                print("You try to run away, but you slip and fall!")
                print("You try to defend yourself, but fail, so the enemy wounds you badly!")
                hero.hp -= enemy.power
                print(f"Health: {hero.hp} hp")
                print(f"{'-' * 27}")
        else:
            print(f"\n{'-' * 27}")
            print("The number is not allowed! Please enter only 1 or 2!")
            print(f"{'-' * 27}")


def boss_battle(points: int) -> int:
    enemy = boss()
    print("The arch-enemy of the world,", enemy.name, "showed up!")
    print("You have two options:")
    while enemy.hp > 0:
        choice = input("\n [1] - Attack\n [2] - Retreat\n ")
        if choice == "1":
            print(f"\n{'-' * 27}")
            print(f"{hero.name} swung his sword, attacking {enemy.name}-t!")
            hitchance = random.randint(0, 10)
            if hitchance > 3:
                enemy.hp = enemy.hp - hero.power
                print(f"You wound the enemy, the enemy's life: {enemy.hp} hp")
                if enemy.hp > 1:
                    hero.hp = round(hero.hp - (enemy.power / hero.armor))
                    print(f"{enemy.name} is striking back, it has wounded you! Health: {hero.hp} hp")
                    print(f"{'-' * 27}")
                    gameOver(hero, points)
                else:
                    if enemy.name == "Leoric the Skeletonking":
                        points += 480
                    print(f"You have defeated the enemy: {enemy.name}\n{'-' * 27}")
                    print(f"\n{'-' * 27}")
                    gameWin(hero)
                    return points
            else:
                print("Your sword slips from your hand, you missed the attack!")
                print(f"{enemy.name} takes this opportunity and seriously injures you!")
                hero.hp -= enemy.power
                print(f"Health: {hero.hp} hp")
                print(f"{'-' * 27}")
                gameOver(hero, points)
        elif choice == "2":
            print(f"\n{'-' * 27}")
            runchance = random.randint(1, 10)
            if runchance > 4:
                print("You have successfully escaped!")
                print("Everyone is disappointed in you! You have run away from your duty and, therefore, the people continue to fear the terrible reign of Skeleton King Leoric!")
                print("The world is infested with the Skeleton King and his followers! Leonic is on his way to take over the next world!\n The End!\n")
                print(f"{'-' * 27}")
                break
            else:
                print("You try to run away, but you slip and fall!")
                print("You try to defend yourself, but fail, so the enemy wounds you badly!")
                hero.hp -= enemy.power
                print(f"Health: {hero.hp} hp")
                print(f"{'-' * 27}")
                gameOver(hero, points)
        else:
            print(f"\n{'-' * 27}")
            print("The number is not allowed! Please enter only 1 or 2!")
            print(f"{'-' * 27}")


def introduction():
    print("\t\tWelcome chosen one, Hero Arius!\n\
        Fate has chosen you as the hero to free the world from the terrible reign of Skeleton King Leoric!\n\
        Get stronger through obstacles, defeat the evil's allies, and fight evil at the very end!\n\
        Command: move [direction] (move north, east, west, south)\n")

    input("Press ENTER to continue.")


rooms = {
    'Start': {'North': 'Terrifying Mine'},
    'Terrifying Mine': {'North': 'Tunnel of Hell'},
    'Tunnel of Hell': {'East': 'Deceptive Cave'},
    'Deceptive Cave': {'East': 'Illusion Cave'},
    'Illusion Cave': {'East': 'Unstable Vortex'},
    'Unstable Vortex': {'South': 'Imaginary Labyrinth'},
    'Imaginary Labyrinth': {'South': 'Garden of the Wizard'},
    'Garden of the Wizard': {'South': 'Bone Crusher Castle'},
    'Bone Crusher Castle': {'Boss': 'Leoric the Skeletonking'}
}

current_room = "Start"
msg = ""

introduction()
totalpoints = 0

print(f"\n{'=' * 27}\nYou are here now: {current_room}\n{'=' * 27}")

while True:
    user_input = input("Enter your move:\n")
    next_move = user_input.split(' ')
    action = next_move[0].title()
    item = "Item"
    direction = "null"
    if len(next_move) > 1:
        item = next_move[1:]
        direction = next_move[1].title()
        item = " ".join(item).title()
    if action == "Move" or "M":
        try:
            current_room = rooms[current_room][direction]
            msg = f"\n{'=' * 27}\n{hero.name} is heading to {direction}!\n{'=' * 27}"
            print(msg)
            print(f"\n{'=' * 27}\nYou are here now: {current_room}\n{'=' * 27}")
            if "Boss" in rooms[current_room].keys():
                totalpoints = boss_battle(totalpoints)
                print(totalpoints)
                break
            else:
                totalpoints = battle(totalpoints)
                print(totalpoints)
        except:
            msg = f"\n{'=' * 27}\nYou can't go that way!\n{'=' * 27}"
            print(msg)
    elif action == "Exit":
        break
    else:
        msg = "Invalid command!"

The code is working. I'm looking for tips in terms of coding styles, readability, and perhaps optimizations if any.

\$\endgroup\$
2
  • \$\begingroup\$ I don't have time for a full review right now, but wanted to mention one possible bug. You keep using the pattern of loot = random.randint(0, 2). That random call will get run once at class definition, which means that all your gnomes will have the same amount of loot as all other gnomes. If that isn't what you want, you'll need to move the randint call somewhere that gets run repeatedly, such as an __init__ method. \$\endgroup\$
    – Josiah
    Commented Nov 19, 2022 at 16:43
  • \$\begingroup\$ @Josiah That's OK because in the game you only need to fight once with every enemy until you get to the boss. \$\endgroup\$ Commented Nov 19, 2022 at 16:47

1 Answer 1

11
\$\begingroup\$

Character Classes

Okay, let's start at the top. You are using classes poorly. You declare a class, create a single instance of that class, and then modify the class variables of that class in that single instance. This works okay for you, because you only ever use one instance of each class, but if you were to ever create (for example) two minotaurus objects, you would discover that subtracting health from one of them would deduct health from both. Which is not desired behavior.

The correct way to make a class like this would be

class Minotaurus:
    def __init__(self):
        self.name = "Gratus the Minotaurus"
        self.hp = 120
        self.power = 10
        self.armor = 3
        self.loot = random.randint(0, 2)

This way the variables are defined when the instance is created and is individual to that instance.

Also, I removed the (object) part - all python classes automatically inherit from object unless otherwise stated.

But this brings up another point - all of the character classes you created are doing the same thing, just with different initial values. This means that they make more sense as instances of a single character class, like so:

class Character:
    def __init__(self, name, hp, power, armor, loot=None):
        self.name = name
        self.hp = hp
        self.power = power
        self.armor = armor
        self.loot = loot
        
player = Character('Hero Arius', 200, 20, 20)
minotaurus = Character('Gratus the Minotaurus', 120, 10, 3, random.randint(0,2))
#etc

If you want to be fancy, you could use dataclasses to make the class look more like your original class format:

from dataclasses import dataclass

@dataclass
class Character:
    """ Characters in the game """
    name: str
    hp: int 
    power: int 
    armor: int
    loot: int|None = None

But since all your monsters have the same loot, I'd actually do it a little differently

@dataclass
class Character:
    """ Characters in the game """
    name: str
    hp: int 
    power: int 
    armor: int

class Monster(Character):
    """ Characters that drop loot when defeated """
    def __init__(self, *args, **kwargs):
        super.__init__(*args, **kwargs)
        self.loot = random.randint(0,2)

Note that I'm also including a docstring here. Always include a docstring for your classes and functions. There isn't really much need for one here, because the classes are self-explanatory, but include one anyways.

And of course, with this code the creation of instances would look like this:

hero = Character("Hero Arius", 200, 20, 20)
flimp = Monster("Flimp the Gnome", 10, 1, 3)
stronger_flimp = Monster("Flimp+ the Gnome", 20, 2, 6)
driekol = Monster("Driekol the Goblin", 30, 4, 4)
stronger_driekol= Monster("Driekol+ the Goblin", 60, 8, 8)
gratus = Monster("Gratus the Minotaurus", 120, 10, 3)
stronger_gratus = Monster("Gratus+ the Minotaurus", 240, 20, 6)
gandalf = Monster("Gandalf the Wizard", 480, 40, 2)
boss = Character("Leoric the Skeletonking", 960, 60, 60)

This is a lot better, but you're still creating the characters in the global scope, which means that they're still effectively one use. Now again, I know that you're only using them once, but it's still good practice to avoid mutable global variables unless they need to be in the global scope.

What I would do would be to define the stats for each character in a dictionary and the access the dictionary whenever you need to create a character object.

STATS = {
    'hero':("Hero Arius", 200, 20, 20),
    'flimp':("Flimp the Gnome", 10, 1, 3),
    'stronger_flimp':("Flimp+ the Gnome", 20, 2, 6),
    'driekol':("Driekol the Goblin", 30, 4, 4),
    'stronger_driekol':("Driekol+ the Goblin", 60, 8, 8),
    'gratus':("Gratus the Minotaurus", 120, 10, 3),
    'stronger_gratus':("Gratus+ the Minotaurus", 240, 20, 6),
    'gandalf':("Gandalf the Wizard", 480, 40, 2),
    'boss':("Leoric the Skeletonking", 960, 60, 60)
 }

and then when you need to create an instance you'd call it like so:

hero = Character(*STATS['hero'])
enemy = Monster(*STATS['flimp'])

General Formatting and Style

You will notice I capitalized the name of my classes in the previous section. The standard convention for Python (PEP 8) is to use TitleCase for class names, where the first letter is capitalized, instead of camelCase like you used). Variables are lowercase_with_underscores, and constants (like ROOMS) are in ALL_CAPS.

Also, the part of your code that is actually running (starting with current_room = "Start") should be wrapped in if __name__ == '__main__': block. Again it's not strictly relevant for your code since it's entirely in a single file, but it prevents code from running unexpectedly if you have multiple files. It's also a good marker separating the code that defines things from the code that does things.

Miscellaneous

The number of points earned for defeating an enemy is tied to that specific enemy, so rather than including an if/elif block, I'd include points as a variable in the Character class.

@dataclass
class Character:
    """ Characters in the game """
    name: str
    hp: int 
    power: int 
    armor: int
    points: int = 0

You also assign enemies to rooms using an if/elif block. That could be done with a dictionary

ENEMIES = {"Terrifying Mine":'flimp',
           "Tunnel of Hell":'stronger_flimp',
           "Deceptive Cave":'driekol',
           "Illusion Cave":'stronger_driekol',
           "Unstable Vortex":'gratus',
           "Imaginary Labyrinth":'stronger_gratus'}

enemy_name = ENEMIES.get(current_room, 'gandalf')
enemy = Monster(*STATS[enemy_name])

I'm not sure why you declared enemy to be global in the battle() function - it doesn't look like it needs to be (and if it doesn't need to be, it definitely shouldn't be).

\$\endgroup\$
3
  • \$\begingroup\$ Hello! I realize that I still have a lot of work to do, which is understandable given that I'm only getting started with Python. I've seen some folks use init to create characters, but I don't understand it yet. I don't even know what dataclasses is, but I'll read it over and study it. The global in the battle() function is an accident; I forgot to remove it. I had a lot of thoughts and improvisations after reading your response. I appreciate your assistance and the time you spent! \$\endgroup\$ Commented Nov 20, 2022 at 0:22
  • \$\begingroup\$ @VoretoRobert Of course! That's why I linked to the documentation. The difference between using init and what you were doing are kind of subtle and relate to how Python treats classes, so it's not surprising it confused you. \$\endgroup\$ Commented Nov 20, 2022 at 0:45
  • \$\begingroup\$ I noticed you linked it, which simplified things. I am grateful for it and especially for your answer! \$\endgroup\$ Commented Nov 20, 2022 at 0:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.