5
\$\begingroup\$

I have just recently picked up coding and am trying to improve my coding. I've written this bit of code to create a little adventure world. I haven't completed the code, everything runs how I want it too but I'm aware this code isn't efficient. I would like some advice and some help on how I would make my code better, particularly more efficient. Also, I have not coded further from guessing the number from the box, so it just exits the code after.

Anything would be appreciated, as I know I have a lot to learn!

import random
attack = 0
health = 0
element = 0
def character():
    number = int(input("""Please choose your character.
    \n1. You control the element of fire, and bend it to your will freely.
    \n2. You control the element of water, you feel the flow and control it elegantly.
    \n3. You control the element of ice, you're power is the to call on ice and use it lethally.
> """))
    if number == 1:
        choice = 1
        fire_character()
    elif number == 2:
        choice = 2
        water_character()
    elif number == 3:
        choice = 3
        ice_character()
    else:
        print("Sorry you can only choose a number between 1 and 3.")

def fire_character():
    global attack
    global health
    global element
    attack += 100
    health += 50
    element = 'fire'
    print(f"\nSince you have chosen fire: Attack = {attack}, Health = {health}")

def water_character():
    global attack
    global health
    global element
    attack = 75
    health = 75
    element = 'water'
    print(f"\nSince you have chosen water: Attack = {attack}, Health = {health}")

def ice_character():
    global attack
    global health
    global element
    attack = 50
    health = 100
    element = 'ice'
    print(f"\nSince you have chosen ice: Attack = {attack}, Health = {health}")

def welcome(attack, health, element):
    print(f"""
Welcome to the game! You have chosen the {element} element! Lets see how far you can get!
Best of luck, I hope the game ends up well!
    """)


def which_way():
    print(f"With your newly discovered {element} powers, you mysteriously wake up in a whole new world.")
    print("You're in a crowded forest and there's noises coming from every direction.")
    print("A small furry animal approaches you and startles you!")
    print(""" 'DO NOT WORRY SIR, I NO HARM YOU! I HEAR TO GUIDE YOU THROUGH NEW PLACE', the little creature says. """)
    print("The creature then tells you he seems to have forgotten the safe way through... He says he has a foggy idea.")
    print("He will reccomend which way to go he says.. but he says not to trust his memory...")
    print("\n The furry guy reccomends going left.")
    loop = False
    while loop == False:
        choice = input("Are you going to go left or right? > ").lower()
        if choice == 'right':
            enemy1()
            loop = True
        elif choice == 'left':
            print("\nYou decide to listen to the lil fella and go left. You walk through a relaxing walkway in the forest.")
            print("You look around and see a gorgeous lake and beautiful animals sitting around it.")
            print("You peacfully walk around for a while, admiring all of the natures beauty and then eventually carry on.")
            choice_two()
            loop = True
        else:
            print("Thats not an option sorry ")

def enemy1():
    global attack
    global health
    global element
    en_attack = 20
    en_health = 100
    en_element = 'ice'
    print("\nYou go against your little companions advice and turn right.")
    print("As you walk through the forest you hear sounds coming out of the bushes.")
    print("An enemy with frost emitting from around his body slowly walks out...")
    print("You're little body comes upto you and tells you about the character.")
    print("THIS ICE GUY WEAK HAHA, HE STATS ARE:")
    print(f"Attack: {en_attack}, Health: {en_health}, Element: {en_element}")
    loop = False
    while loop == False:
        choice = input("\nWhat do you want to do? Attack, run or try talk it out? (attack, run or talk): ").lower()
        if choice == 'attack':
            print(f"You throw a {element} ball at him!")
            if element == 'fire':
                print("Your fire ball does extra damage!")
                print(f"\nEnemy Health: {en_health} - Your attack: {attack}")
                en_health = en_health - attack
                print(f"The enemy took an extra 10 damage beacuse of the element difference!")
                en_health = en_health - 10
                print(f"The enemys health is at {en_health}! He melts into a puddle as you carry on.")
                loop = True
                if en_health <= 0:
                    print("Enemy falls on the ground and dies.")
                    choice_two()
            if element == 'water':
                print("Your water ball does terribe damage!")
                print(f"\nEnemy Health: {en_health} - Your attack: {attack}")
                en_health = en_health - attack
                print(f"The enemy uses your water and hardens his armor back up a bit... Enemy adds 10 health back")
                en_health = en_health + 10
                print(f"The enemys health is at {en_health}!")
                if en_health <= 0:
                    print("Enemy falls on the ground and dies.")
                    choice_two()
                print(f"\nThe enemy wasn't happy that you just attacked him out of nowhere and attacks you!")
                health = health - en_attack - 10
                print(f"The enemy hardens the water around it and turns them into icyicles launching them at you doing {en_attack} damage!")
                print(f"The ice enemies attack does an extra 10 damage..")
                print(f"Your health is {health}")
            if element == 'ice':
                print("Your ice ball does neutral damage!")
                print(f"\nEnemy Health: {en_health} - Your attack: {attack}")
                en_health = en_health - attack
                print(f"The enemys health is at {en_health}!")
                if en_health <= 0:
                    print("Enemy falls on the ground and dies.")
                    choice_two()
                print(f"\nThe enemy wasn't happy that you just attacked him out of nowhere and attacks you!")
                health = health - en_attack
                print(f"The enemy hardens the water around it and turns them into icyicles launching them at you doing {en_attack} damage!")
                print(f"Your health is {health}")
            loop = False
        elif choice == 'run':
            print("\nYou runaway with your tail between your legs!")
            die("While running away you trip into a hole of acid. Shame.")
            loop = True
        elif choice == 'talk':
            print("\nWhen the beast gets close to you, you calmly ask for his name.")
            print("The beast tells you his name and then you guys chat for a while.")
            print("When you guys finishing talking about past lives he kindly lets you stroll on past.")
            choice_two()
            loop = True
        else:
            print("Sorry that's not an option.")
            loop = False

def choice_two():
    global health
    print(f"\n You're health is {health}")
    print("\n While you are strolling a mysterious blue box appears. You approach the box and start inspecting it.")
    print("Your little guide starts going on a bit how you have to guess a number to open it. But there's a catch... ")
    print("If you don't guess the number right you get inflicted with an uncurable poison. You get 3 guesses.")
    print("The little guy says guess between 1 and 8...")
    if element == 'water':
        print(f"\nAs you aproach the box it starts lighting up a bit.. It seems to connect with your element {element}.")
        print("Guess between 1 and 5.")
    box_number_notwater = random.randint(1, 8)
    box_number_water = random.randint(1, 5)
    guess_count = 0
    while guess_count <= 2:
        if element == 'fire' or 'ice':
            guess = int(input("Guess a number: "))
            if guess == box_number_notwater:
                print("You got it right!")
                exit()
            elif guess < box_number_notwater:
                print("Sorry! Try guessing higher!")
                guess_count += 1
            elif guess > box_number_notwater:
                print("Sorry! Try guessing lower!")
                guess_count += 1
        elif element == 'water':
            guess = int(input("Guess a number: "))
            if guess == box_number_water:
                print("You got it right!")
                exit()
            elif guess < box_number_water:
                print("Sorry! Try guessing higher!")
                guess_count += 1
            elif guess > box_number_water:
                print("Sorry! Try guessing lower!")
                guess_count += 1
    if guess_count == 3:
        die('A squirrel comes from the box and bites your finger. You start slowly dosing off and die.')





def die(reason):
    print("")
    print(f"{reason} Good Job!!")
    exit()


player = input("Would you like to play the game? ").lower()
if player == 'yes':
    character()
else:
    print("Goodbye!")
    exit()

welcome(attack, health, element)
which_way()
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I receive errors when testing your code. It appears that the paste is missing the import statements? Can you fix? Thanks. \$\endgroup\$
    – C. Harley
    Commented Sep 13, 2021 at 13:32
  • \$\begingroup\$ @C.Harley Just edited it, sorry I had forgot to paste in the import of random. \$\endgroup\$
    – NewWalker
    Commented Sep 13, 2021 at 23:54

4 Answers 4

7
\$\begingroup\$

This is a fun start to a game.

Decrease your reliance on globals, passing around variables in function parameters instead. Encapsulating the character stats in a class will make things easier.

You need to work on your spelling and grammar - for example, you're power is the to call -> your power is the ability to call.

Consider representing the element choice as an enumeration - among other advantages, to make iteration over its options easier.

You have some input validation - in character, consider expanding it by adding a loop that doesn't exit until the user enters valid input.

As a growing developer you need to develop a deep, burning hatred for copy-and-pasted code repetition such as that seen in print('Since you have chosen .... There are many ways to reduce this repetition, the easiest being to factor out the repeated code into functions.

Try to flatten out your call structure - instead of which_way calling into the enemy function, just return the choice and have the outer level call the next function.

You don't need to use separate variables for box_number_notwater and box_number_water - you can use one variable, initializing it conditionally based on player element.

I find it odd that you're asking the player whether they want to play the game? Presumably if they started the program, they already want to play the game, and if they somehow accidentally clicked on its icon, they could just close it without that prompt. I would delete it.

Suggested

This doesn't go far enough to eliminate e.g. your repeated code in the fight sequence, but it's a start and food for thought.

import enum
import random
from enum import Enum
from typing import ClassVar


@enum.unique
class Element(Enum):
    FIRE = 1
    WATER = 2
    ICE = 3


class Character:
    DESCRIPTION: ClassVar[str]
    ATTACK: ClassVar[int]
    START_HEALTH: ClassVar[int]
    ELEMENT: ClassVar[Element]

    def __init__(self):
        self.health = self.START_HEALTH

    @property
    def element_name(self) -> str:
        return self.ELEMENT.name.lower()


def choose_character() -> 'Character':
    prompt = (
        'Please choose your character.\n' +
        '\n'.join(
            f'{element.value}. {CHARACTERS[element].DESCRIPTION}'
            for element in Element
        )
    )
    while True:
        try:
            number = int(input(prompt))
            if 1 <= number <= len(Element):
                break
            print(f"Sorry, you can only choose a number between 1 and {len(Element)}.")
        except ValueError:
            print('Please enter a number.')

    player = CHARACTERS[Element(number)]()
    print(
        f"Since you have chosen {player.element_name}: "
        f"Attack = {player.ATTACK}, Health = {player.health}"
    )
    return player


class FireCharacter(Character):
    ELEMENT = Element.FIRE
    DESCRIPTION = 'You control the element of fire, and bend it to your will freely.'
    ATTACK = 100
    START_HEALTH = 50


class WaterCharacter(Character):
    ELEMENT = Element.WATER
    DESCRIPTION = 'You control the element of water; you feel the flow and control it elegantly.'
    ATTACK = 75
    START_HEALTH = 75


class IceCharacter(Character):
    ELEMENT = Element.ICE
    DESCRIPTION = 'You control the element of ice; your power is the ability to call on ice and use it lethally.'
    ATTACK = 50
    START_HEALTH = 100


CHARACTERS = {
    cls.ELEMENT: cls
    for cls in (FireCharacter, WaterCharacter, IceCharacter)
}


def welcome(player: Character) -> None:
    print(f"""
Welcome to the game! You have chosen the {player.element_name} element! Let's see how far you can get!
Best of luck, I hope the game ends up well!
    """)


def which_way(player: Character) -> str:
    print(
f"""With your newly discovered {player.element_name} powers, you mysteriously wake up in a whole new world.
You're in a crowded forest and there's noises coming from every direction.
A small furry animal approaches you and startles you!
'DO NOT WORRY SIR, I NO HARM YOU! I HEAR TO GUIDE YOU THROUGH NEW PLACE', the little creature says.
The creature then tells you he seems to have forgotten the safe way through... He says he has a foggy idea.
He will recommend which way to go he says.. but he says not to trust his memory...
The furry guy recommends going left.
"""
    )

    while True:
        choice = input("Are you going to go left or right? > ").lower()
        if choice in {'left', 'right'}:
            return choice
        print("That's not an option, sorry")


def forest() -> None:
    print(
"""\nYou decide to listen to the lil fella and go left. You walk through a relaxing walkway in the forest.
You look around and see a gorgeous lake and beautiful animals sitting around it.
You peacefully walk around for a while, admiring all of the natures beauty and then eventually carry on.
""")


def enemy1(player: Character) -> None:
    en_attack = 20
    en_health = 100
    en_element = Element.ICE

    print(
f"""\nYou go against your little companions advice and turn right.
As you walk through the forest you hear sounds coming out of the bushes.
An enemy with frost emitting from around his body slowly walks out...
You're little body comes upto you and tells you about the character.
THIS ICE GUY WEAK HAHA, HE STATS ARE:
Attack: {en_attack}, Health: {en_health}, Element: {en_element.name.lower()}
""")

    while True:
        choice = input("\nWhat do you want to do? Attack, run or try talk it out? (attack, run or talk): ").lower()
        if choice == 'run':
            print("\nYou run away with your tail between your legs!")
            die("While running away you trip into a hole of acid. Shame.")
        if choice == 'talk':
            print(
                "\nWhen the beast gets close to you, you calmly ask for his name."
                "\nThe beast tells you his name and then you guys chat for a while."
                "\nWhen you guys finishing talking about past lives he kindly lets you stroll on past."
            )
            return
        if choice != 'attack':
            print("Sorry that's not an option.")
            continue

        print(f"You throw a {player.element_name} ball at him!")
        if player.ELEMENT == Element.FIRE:
            print("Your fire ball does extra damage!")
            print(f"\nEnemy Health: {en_health} - Your attack: {player.ATTACK}")
            en_health -= player.ATTACK
            print(f"The enemy took an extra 10 damage because of the element difference!")
            en_health -= 10
            print(f"The enemy's health is at {en_health}! He melts into a puddle as you carry on.")
            if en_health <= 0:
                print("Enemy falls on the ground and dies.")
                return

        elif player.ELEMENT == Element.WATER:
            print("Your water ball does terrible damage!")
            print(f"\nEnemy Health: {en_health} - Your attack: {player.ATTACK}")
            en_health = en_health - player.ATTACK
            print(f"The enemy uses your water and hardens his armor back up a bit... Enemy adds 10 health back")
            en_health = en_health + 10
            print(f"The enemys health is at {en_health}!")
            if en_health <= 0:
                print("Enemy falls on the ground and dies.")
                return
            print(f"\nThe enemy wasn't happy that you just attacked him out of nowhere and attacks you!")
            player.health -= en_attack - 10
            print(f"The enemy hardens the water around it and turns them into icyicles launching them at you doing {en_attack} damage!")
            print(f"The ice enemies attack does an extra 10 damage..")
            print(f"Your health is {player.health}")

        if player.ELEMENT == Element.ICE:
            print("Your ice ball does neutral damage!")
            print(f"\nEnemy Health: {en_health} - Your attack: {player.ATTACK}")
            en_health = en_health - player.ATTACK
            print(f"The enemys health is at {en_health}!")
            if en_health <= 0:
                print("Enemy falls on the ground and dies.")
                return
            print(f"\nThe enemy wasn't happy that you just attacked him out of nowhere and attacks you!")
            player.health -= en_attack
            print(f"The enemy hardens the water around it and turns them into icyicles launching them at you doing {en_attack} damage!")
            print(f"Your health is {player.health}")


def guess_box(player: Character) -> None:
    n = 8

    print(
f"""\n Your health is {player.health}
While you are strolling a mysterious blue box appears. You approach the box and start inspecting it.
Your little guide starts going on a bit how you have to guess a number to open it. But there's a catch...
If you don't guess the number right you get inflicted with an uncurable poison. You get 3 guesses.
The little guy says guess between 1 and {n}...
""")

    if player.ELEMENT == 'water':
        print(f"\nAs you aproach the box it starts lighting up a bit.. It seems to connect with your element {player.element_name}.")
        n = 5
        print(f"Guess between 1 and {n}.")

    box_number = random.randint(1, n)

    for guess_count in range(3):
        guess = int(input("Guess a number: "))
        if guess < box_number:
            print("Sorry! Try guessing higher!")
        elif guess > box_number:
            print("Sorry! Try guessing lower!")
        else:
            print("You got it right!")
            return

    die('A squirrel comes from the box and bites your finger. You start slowly dosing off and die.')


def die(reason: str) -> None:
    print(f"\n{reason} Good Job!!")
    exit()


def main():
    should_play = input("Would you like to play the game? ").lower()
    if not should_play.startswith('y'):
        print("Goodbye!")
        exit()

    player = choose_character()
    welcome(player)
    if which_way(player) == 'right':
        enemy1(player)
    else:
        forest()

    guess_box(player)


if __name__ == '__main__':
    main()
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you for the time you put into this! I will make sure to have a good read and deep dive into all the new concepts you've presented me. Your review will help me in growing my coding skill. Thank you so much again! \$\endgroup\$
    – NewWalker
    Commented Sep 15, 2021 at 0:06
6
\$\begingroup\$

Welcome to CodeReview! Your code is very much "beginner" code, and I don't propose to change that too much. Instead, I'm going to point you at some things that you can do better in the same framework.

Global variables

You are using globals, which everyone is going to tell you is bad, bad, bad. It's horrible to use globals. Dogs and cats lying down together. Fire and brimstone falling from the skies. Whenever you use a global variable, an orphan dies of starvation. What kind of orphan-killing, brimstone-dropping, pet breeding monster are you?

Anyway.

Your use of globals is inconsistent. There are a few things you need to know:

  1. You only need to use the global keyword when you are changing the value of a global variable. In particular, not when you are changing an element or attribute of a global variable. Thus, you have to say global element if element is an integer or a string, but not if it's an object or a list or a dict (hint, hint!).

  2. You should be consistent in how you make changes to your globals. You can use += or =, but you shouldn't use both operators to accomplish the same task (in different places). You have

     def fire_character():
         global attack
         ...
         attack += 100
    
     def water_character():
         global attack
         ...
         attack = 75
    
  3. Use a naming convention to help distinguish globals. I'd suggest either using ALL_CAPS or Cap_snake for globals.

  4. Use helper functions for accessing globals. This will make it easier to change when you convert to an aggregate object like a dict or class. Write something like

     def set_attack(new_value: int): 
         global Attack
         Attack = new_value
    

This way you never have to write global attack again -- you can just call set_attack(75) and be done with it.

Function names

For "modular" code like this, try to put a verb into each function name, unless it's a pure function (like "sine" or "average") that only computes a result. For code with side effects, or "procedures", you definitely want a verb in there!

You have a function called character(). What does that do? Why not call it pick_character_type() instead?

Loop on input

Pretty much all user input needs to be validated. So any input code should be inside a loop. Look at your character() function - it prints an error if something goes wrong, but then it doesn't loop!

Fix that code! Add a loop to repeat the prompt until the user surrenders (or presses Ctrl+C).

Avoid redundancy

You've got a choice variable and an element variable that both tell what kind of character the user has chosen. One of those is redundant!

I'd suggest keeping the element variable and just comparing Element == 'water' instead of choice == 1 or whatever.

Even if you decide to keep both variables, the choice variable should be set in the element function, not in the character function -- just like all the other variables are set there.

Start looking for patterns

You've got two "location" functions written, and you can start to see patterns already.

You have repeatedly written code that prompts the user for input, checks the input is valid, and loops until it is valid.

You have location functions that print messages, ask for input, and do stuff.

Those are the kinds of patterns that you can turn into functions. If you find a pattern of action/behavior, then by all means make it more general! Code it into a function and pass the variable parts in as data or as parameters.

Learn about dictionaries

Python has dictionaries, which other languages call hashes or associative arrays. They are a fundamental data type, built in to the language and supported with special syntax, special opcodes in the VM, etc.

For what you're doing right now, you want to be able to group things by a key (the location). You could do that with lists, but it's more clear to do it with dictionaries:

places = {'icy cavern': ..., 'fiery desert': ...}

Once you start on this road, you'll immediately start nesting dictionaries inside other dictionaries, and look out, Zork!

places = {
    'icy cavern': {
        'enemy name': 'Dave the Dragon',
        'element': 'ice',
        'attack': 20,
        'health': 30,
     },
     'fiery desert': {
        'enemy name': 'Nick the Newt',
        'element': 'fire',
        'attack': 50,
        'health': 10,
     },
 }

That seems like enough to keep you going. Feel free to post a follow-up question (with a link back to this question) after you've got another 10 or 20 rooms done. Good luck!

\$\endgroup\$
4
  • \$\begingroup\$ Other languages call dictionaries hash maps, not hashes \$\endgroup\$
    – Reinderien
    Commented Sep 14, 2021 at 3:38
  • \$\begingroup\$ cough Perl cough \$\endgroup\$
    – aghast
    Commented Sep 14, 2021 at 17:06
  • \$\begingroup\$ TMYK. I'll instead say "most modern" languages call them hash maps, and calling it a "hash" is not a great idea as it conflates the name of the data structure with its key mechanism and they're not the same thing. \$\endgroup\$
    – Reinderien
    Commented Sep 14, 2021 at 17:10
  • \$\begingroup\$ Thank you! I knew the global variables felt really off.. Reading your description of them made me laugh, I will make sure to never been an orphan killing coder again. Thank you for the clarity in your review. I will start trying to cut down my code by using some of these methods. The time you took to write this is very appreciated! \$\endgroup\$
    – NewWalker
    Commented Sep 15, 2021 at 0:10
3
\$\begingroup\$

Thanks NewWalker for your post. I can see you have big ideas for your game, and that means you'll need to learn some new ideas for getting the game to completion.

The first thing that jumps out at me is it's one huge blob of code. Coding is about re-using code snippets, so we try to make functions where you give it a value (or not), it does some processing, and gives you back a result. Something like:

def title_message():
    return "Adventure World Quest!"

So we can run it to get the message on the screen: print(title_message()). Admittedly it's too simple a function, so instead you would just write print("Adventure World Quest!"). However, if you start using it in many places around your code, then it makes sense to put the statement into function, because D.R.Y. (Don't Repeat Yourself).

We use this process to keep our functions small, typically around 5 lines. Why 5 lines? Well, usually a function does only a single thing, we call this the Single Responsibility Principle. It's part of S.O.L.I.D - have a look at that concept and try to apply it to your code.
So, you can pretty much achieve anything in around 5 lines of code. If it's getting bigger than that, there's a good chance we can do some refactoring (rewriting code to simplify it or extract certain parts away into their own function).

Your blob of code looks like it would enjoy lots of refactoring :)

So, functions, D.R.Y., S.O.L.I.D, and refactoring. These are some ideas which evolved in the programming field over time. Let's look at how your character functions could be refactored:

def fire_character():
    global attack
    global health
    global element
    attack += 100
    health += 50
    element = 'fire'
    print(f"\nSince you have chosen fire: Attack = {attack}, Health = {health}")
    
def water_character():
    global attack
    global health
    global element
    attack = 75
    health = 75
    element = 'water'
    print(f"\nSince you have chosen water: Attack = {attack}, Health = {health}")

These two look pretty much the same, so let's use a programmatic approach (with different variables giving us different outcomes) to make their construction easier:

from enum import Enum

class Skill(Enum):
    Fire = 0
    Water = 1
    Ice = 2

class Character:
    def __init__(self, name, health, element):
        self.name = name
        self.health = health
        self.element = element
            

fire_char = Character("Fireball", 50, Skill.Fire)
water_char = Character("Waterboy", 75, Skill.Water)
ice_char = Character("Icy", 100, Skill.Ice)

What we've achieved here is instead of hard-coding the elements of a character, we make their creation in a programmatic fashion. Now, when we add a function attack(enemy) to the class of Character, any characters we create after that will automatically get the ability to attack. Something like:

    def attack(self, enemy):
        print(f"{self.name} attacks {enemy.name}")
        

Starting the battle fire_char.attack(water_char) would show the message Fireball attacks Waterboy. If we go for a more in-depth attack function, we could come up with something like:

class Dice(Enum):
    @staticmethod
    def d6():
        return randint(1, 6)
    

(adding to class Character)

def attack(self, enemy):
    dice = Dice.d6()

    print(f"{self.name} attacked {enemy.name} with {self.skill.name}")

    if self.skill == enemy.weakness and enemy.weakness != Skill.NoSkill:
        dice += 2
        print(f"-=< {self.name}'s power overwhelms {enemy.name} >=-")
    if self.skill == enemy.skill and enemy.weakness != Skill.NoSkill:
        dice -= 2
        print(f"___ {enemy.name} knows your skills ___")

    final_damage = dice - enemy.armor_class
    msg = f"{self.name} {Hit(final_damage).name} {enemy.name}"

    if final_damage > 0:
        msg += f" for {final_damage} hit point"
    if final_damage > 1:
        msg += "s"
    print(msg)
    return 0 if final_damage < 0 else final_damage
    

When attacking, if the player's skill is the enemy's weakness, we add 2 points of damage. If the player's skill is the same as the enemy, we subtract 2 points due to resistance. The "NoSkill" functionality is for players whom might have a skill that has no weakness. In fact, we should add "NoWeakness" to clarify that (and fix the if statement), but keep "NoSkill" as an option for potential characters who don't wish to use an element skill.

Looking at line msg = f"{self.name} {Hit(final_damage).name} {enemy.name}" what we're doing is giving a verb response based on the amount of damage inflicted. Here is the function:

class Hit(Enum):
    missed = 0
    grazed = 1
    knicked = 2
    hit = 3
    sliced = 4
    wounded = 5
    chopped = 6

Rather than hard-coding the various messages and attacks, such as:

        if element == 'fire': ...                
        if element == 'water': ...
        if element == 'ice': ...

        print("Your fire ball does extra damage!")
        print(f"\nEnemy Health: {en_health} - Your attack: {attack}")
        en_health = en_health - attack
        print(f"The enemy took an extra 10 damage beacuse of the element difference!")
        en_health = en_health - 10
        print(f"The enemys health is at {en_health}! He melts into a puddle as you carry on.")
    

We're using functions to do all those individual statements, and when we want to add another dice - such as a d8 - for a leveled-up character, we need to add more messages, say:

    smashed = 7
    pulverised = 8
    

to the Hit enum class. We can now add the dice to the Dice class use these messages:

    @staticmethod
    def d8():
        return randint(1, 8)

Now we can start to have Fighters, Clerics and Necromancers, oh my. So, if we take your code and write it how you will write it in a few years, we probably come up with something like the below. There is a lot to unpack from the code below - don't be daunted. It's probably easier to understand how it's put together by stepping through the code line by line in your debugger.

from random import randint
from enum import Enum
from itertools import cycle, permutations


class Dice(Enum):
    @staticmethod
    def d6():
        return randint(1, 6)

    @staticmethod
    def d8():
        return randint(1, 8)


class Skill(Enum):
    Fire = 0
    Water = 1
    Ice = 2
    Poison = 3
    Lightning = 4
    Wood = 5
    Necromancy = 6
    Healing = 7
    NoSkill = 8
    NoWeakness = 99

    def weakness(self):
        _weakness = {self.Fire: self.Water,
                     self.Water: self.Lightning,
                     self.Ice: self.Fire,
                     self.Poison: self.Healing,
                     self.Lightning: self.Wood,
                     self.Wood: self.Fire,
                     self.Healing: self.Necromancy,
                     self.Necromancy: self.Healing,
                     self.NoSkill: self.NoWeakness}

        return _weakness[self]


class Hit(Enum):
    missed = 0
    grazed = 1
    knicked = 2
    hit = 3
    sliced = 4
    wounded = 5
    chopped = 6
    smashed = 7
    pulverised = 8


class Character:
    def __init__(self, name: str, hit_points: int, skill: Skill, is_player=None):
        self.name = name
        if not isinstance(skill, Skill):
            print("Character creation invalid. Skill must be of type Skill.Fire, Skill.Water, etc.")
            return
        self.skill = skill
        self.weakness = skill.weakness()
        self._hit_points = hit_points
        self._is_player = is_player
        self._active_effects = []
        self._is_alive = True
        self._armor_class = 1
        char_type = "is a player" if is_player else "is not a player"
        # print(f"Character {self.name} is now alive. They have {hit_points} hit points and {char_type}")

    def attack(self, enemy):
        dice = Dice.d6()

        print(f"{self.name} attacked {enemy.name} with {self.skill.name}")

        if self.skill == enemy.weakness and enemy.weakness != Skill.NoSkill:
            dice += 2
            print(f"-=< {self.name}'s power overwhelms {enemy.name} >=-")
        if self.skill == enemy.skill and enemy.weakness != Skill.NoSkill:
            dice -= 2
            print(f"___ {enemy.name} knows your skills ___")

        final_damage = dice - enemy.armor_class
        final_damage = 0 if final_damage < 0 else final_damage

        msg = f"{self.name} {Hit(final_damage).name} {enemy.name}"

        if final_damage > 0:
            msg += f" for {final_damage} hit point"
        if final_damage > 1:
            msg += "s"
        print(msg)
        return final_damage

    @property
    def hit_points(self):
        return self._hit_points

    def takes_damage(self, value):
        self._hit_points -= value
        if self._hit_points <= 0:
            print(f"{self.name} died")
            self._is_alive = False

    @property
    def armor_class(self):
        return self._armor_class

    @armor_class.setter
    def armor_class(self, value):
        print(f"{self.name} puts on new armor.")
        self._armor_class = value + 1  # skin

    @property
    def is_dead(self):
        return self._is_alive == False


def get_user_choice(params, display_params=""):
    is_a_valid_choice = False
    if display_params == "":
        display_params = params
    while not is_a_valid_choice:
        choice = input(f"\nPlease select a choice ({display_params}): ")
        if choice.isalpha():
            choice = choice.lower()
        if choice.isnumeric():
            choice = int(choice)
        if choice in params:
            return choice

        print(f"That's unfortunately not a selection you can make. Please select one of these: {display_params}")


def begin_battle(combatants: list):
    """Given a list of combatants, begin the battle. List order determines the sequence of the actions"""

    phases = permutations(combatants)
    fighters = cycle(phases)
    combatants_alive = True
    while combatants_alive:
        act = next(fighters)
        attacker = act[0]
        defender = act[1]
        damage = attacker.attack(defender)
        defender.takes_damage(damage)
        if defender.is_dead:
            combatants_alive = False

    return combatants


def title_message():
    return "Adventure World Quest!"


def welcome_message():
    msg = "Welcome to the greatest experience since sliced bread was held over a fire!\n\n"
    msg += "You are about to embark on a perilous quest to save a dragon from a princess.\n\n"
    msg += "Are you ready?"
    return msg


def okay_bye():
    msgs = ["Thanks for trying!",
            "Well, *huff* I put all this together and you don't want to keep playing?",
            "Digital Characters were harmed in the making of this quest."]
    return msgs[randint(0, 2)]


def do_auto_character():
    return "Do you want to create your own character or just have a random character generated?"


def create_character():
    print("Welcome to the Unique Character Creation Process")
    print("Sorry, it's not ready, so here's a randomly generated character!")
    character = Character("Player", 10, Skill(randint(0, 6)), is_player=True)
    return character


if __name__ == "__main__":
    print(title_message())
    print(welcome_message())
    choices = get_user_choice(["y", "n", "yes", "no"], "y or n")
    player = Character("Player", 10, Skill.Fire, is_player=True)

    if choices[0].lower() == "n":
        print(okay_bye())
    else:

        print(do_auto_character())
        choices = get_user_choice(["y", "n", "yes", "no", "random"], "y or n/random")
        if choices[0].lower() == "y":
            player = create_character()

        enemy = Character("Ice Wizard", 7, Skill.Ice)

        player, enemy = begin_battle([player, enemy])
\$\endgroup\$
2
\$\begingroup\$

It’s an adventure game, why not seeing the code also from an adventurous perspective. Let’s imagine a character that traverses a sequence of scenes that each has:

a story:

With your newly discovered {element} powers, you mysteriously wake up in a whole new world. You're in a crowded forest and there's noises coming from every direction. A small furry animal approaches you and startles you! 'DO NOT WORRY SIR, I NO HARM YOU! I HEAR TO GUIDE YOU THROUGH NEW PLACE', the little creature says. The creature then tells you he seems to have forgotten the safe way through... He says he has a foggy idea. He will reccomend which way to go he says.. but he says not to trust his memory... The furry guy reccomends going left.

a dialog:

Are you going to go left or right?

an action:

while loop == False:
    choice = input("Are you going to go left or right? > ").lower()
    if choice == 'right':
        enemy1()
        loop = True
    elif choice == 'left':
        print("\nYou decide to listen to the lil fella and go left. You walk through a relaxing walkway in the forest.")
        print("You look around and see a gorgeous lake and beautiful animals sitting around it.")
        print("You peacfully walk around for a while, admiring all of the natures beauty and then eventually carry on.")
        choice_two()
        loop = True
    else:
        print(“Have a different option, try an available one this time ")

Now it is easy legible the solution could implement one of the behavioural patterns, namely State pattern that is an eficient, flexible and easy maintainable solution for this use case.

A possible implementation might model each scene in a state thus having welcome, crossroads, confrontation, forest and riddle states. Each state would have three methods action, dialog and story. The states would be stored in a list that will be iterated passing the control to each scene.

The rest is fairy tail. Have fun...

\$\endgroup\$

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.