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.

Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Nov 19, 2022 at 16:34
\$\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\$ 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).

answered Nov 19, 2022 at 23:33
\$\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

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.