21
\$\begingroup\$

I am embarking on the daunting journey of learning to program on my own two feet and have created a turn-based battle simulator (think Pokémon). The idea came from here.

It's very crude and could probably use a lot of refinement, so I'm willing to listen to any criticism and advice on improvements.

    # Turn Based Battle Simulator

# Player and computer take turns to attack each other with different moves
# until one is defeated.

import random


def main():
    """Main function that will welcome the player to the game."""

    print("\tWelcome to Battle Sim! This is a turn based combat simulator where")
    print("\tthere can only be one winner.")

    print("\nHow to play.\n\nPlayers take turn to choose a move. Moves can either deal moderate damage")
    print("with a low range, deal high damage but over a wide")
    print("range, or they can heal. (Note: Moves can miss, including Heal!)")

    print("\nEach player starts with 100 health, and the first")
    print("player to reduce their opponent to 0 is the winner.")

    print("\nThat's it! Good luck")

    play_again = True

    # Set up the play again loop
    while play_again:
        winner = None
        player_health = 100
        computer_health = 100

        # determine whose turn it is
        turn = random.randint(1,2) # heads or tails
        if turn == 1:
            player_turn = True
            computer_turn = False
            print("\nPlayer will go first.")
        else:
            player_turn = False
            computer_turn = True
            print("\nComputer will go first.")


        print("\nPlayer health: ", player_health, "Computer health: ", computer_health)

        # set up the main game loop
        while (player_health != 0 or computer_health != 0):

            heal_up = False # determine if heal has been used by the player. Resets false each loop.
            miss = False # determine if the chosen move will miss.

            # create a dictionary of the possible moves and randomly select the damage it does when selected
            moves = {"Punch": random.randint(18, 25),
                     "Mega Punch": random.randint(10, 35),
                     "Heal": random.randint(20, 25)}

            if player_turn:
                print("\nPlease select a move:\n1. Punch (Deal damage between 18-25)\n2. Mega Punch (Deal damage between 10-35)\n3. Heal (Restore between 20-25 health)\n")

                player_move = input("> ").lower()

                move_miss = random.randint(1,5) # 20% of missing
                if move_miss == 1:
                    miss = True
                else:
                    miss = False

                if miss:
                    player_move = 0 # player misses and deals no damage
                    print("You missed!")
                else:
                    if player_move in ("1", "punch"):
                        player_move = moves["Punch"]
                        print("\nYou used Punch. It dealt ", player_move, " damage.")
                    elif player_move in ("2", "mega punch"):
                        player_move = moves["Mega Punch"]
                        print("\nYou used Mega Punch. It dealt ", player_move, " damage.")
                    elif player_move in ("3", "heal"):
                        heal_up = True # heal activated
                        player_move = moves["Heal"]
                        print("\nYou used Heal. It healed for ", player_move, " health.")
                    else:
                        print("\nThat is not a valid move. Please try again.")
                        continue

            else: # computer turn

                move_miss = random.randint(1,5)
                if move_miss == 1:
                    miss = True
                else:
                    miss = False

                if miss:
                    computer_move = 0 # the computer misses and deals no damage
                    print("The computer missed!")
                else:
                    if computer_health > 30: 
                        if player_health > 75:
                            computer_move = moves["Punch"]
                            print("\nThe computer used Punch. It dealt ", computer_move, " damage.")
                        elif player_health > 35 and player_health <= 75: # computer decides whether to go big or play it safe
                            imoves = ["Punch", "Mega Punch"]
                            imoves = random.choice(imoves)
                            computer_move = moves[imoves]
                            print("\nThe computer used ", imoves, ". It dealt ", computer_move, " damage.")
                        elif player_health <= 35:
                            computer_move = moves["Mega Punch"] # FINISH HIM!
                            print("\nThe computer used Mega Punch. It dealt ", computer_move, " damage.")                       
                    else: # if the computer has less than 30 health, there is a 50% chance they will heal
                        heal_or_fight = random.randint(1,2) 
                        if heal_or_fight == 1:
                            heal_up = True
                            computer_move = moves["Heal"]
                            print("\nThe computer used Heal. It healed for ", computer_move, " health.")
                        else:
                            if player_health > 75:
                                computer_move = moves["Punch"]
                                print("\nThe computer used Punch. It dealt ", computer_move, " damage.")
                            elif player_health > 35 and player_health <= 75:
                                imoves = ["Punch", "Mega Punch"]
                                imoves = random.choice(imoves)
                                computer_move = moves[imoves]
                                print("\nThe computer used ", imoves, ". It dealt ", computer_move, " damage.")
                            elif player_health <= 35:
                                computer_move = moves["Mega Punch"] # FINISH HIM!
                                print("\nThe computer used Mega Punch. It dealt ", computer_move, " damage.")

            if heal_up:
                if player_turn:
                    player_health += player_move
                    if player_health > 100:
                        player_health = 100 # cap max health at 100. No over healing!
                else:
                    computer_health += computer_move
                    if computer_health > 100:
                        computer_health = 100
            else:
                if player_turn:
                    computer_health -= player_move
                    if computer_health < 0:
                        computer_health = 0 # cap minimum health at 0
                        winner = "Player"
                        break
                else:
                    player_health -= computer_move
                    if player_health < 0:
                        player_health = 0
                        winner = "Computer"
                        break

            print("\nPlayer health: ", player_health, "Computer health: ", computer_health)

            # switch turns
            player_turn = not player_turn
            computer_turn = not computer_turn

        # once main game while loop breaks, determine winner and congratulate

        if winner == "Player":
            print("\nPlayer health: ", player_health, "Computer health: ", computer_health)
            print("\nCongratulations! You have won. You're an animal!!")
        else:
            print("\nPlayer health: ", player_health, "Computer health: ", computer_health)
            print("\nSorry, but your opponent wiped the floor with you. Better luck next time.")

        print("\nWould you like to play again?")

        answer = input("> ").lower()
        if answer not in ("yes", "y"):
            play_again = False

main()
\$\endgroup\$
0

2 Answers 2

20
\$\begingroup\$

Style

You've styled your code very well overall, I just have a few small nitpicky tips and such.

  • First off, when running main, you need run it under an if __name__ == "__main__": block. See this Stack Overflow answer for more information on this.
  • The comment at the top of your code file should be a docstring. It should look something like the below example, with a description of the file and it's contents.

    """
    filename.py
    
    Put a description of your file here.
    """
    

Design

Your design is not as great as your coding style, so I'm going to go more in-depth here on what can be improved.

  • At the top of main you have a lot of print statements on one line. I'd reccomend defining a function to easily print a lot of lines without needing to repeat print statements.

    def print_lines(*lines):
        """
        A helpful function for printing many
        separate strings on separate lines.
        """
        print("\n".join([line for line in lines]))
    
    
    # EXAMPLE USAGE
    print_lines(
        "Hello.",
        "Goodbye"
    )
    
  • Your current design using variables to manage different statistics and attributes about a player and an enemy begs for Object-Oriented-Programming. I'd recommend setting up something like this, a Character class with methods like, do_damage, or take_damage.

    class Character:
        """
        Base class used to create playable
        characters
        """
        def __init__(self, health: int, damage: int, name: str):
            self.health = health
            self.damage = damage
            self.name = name
    
        ...
    
  • Another thing I'd recommend is using dictionaries for getting certain inputs, rather than creating if/elif/else chains. Here's an example.

    CHOICES = {
        "a choice": a_function,
        ...
    }
    
    user_input = raw_input("> ")
    if user_input in CHOICES:
        CHOICES[user_input]()
    
  • Finally, I'd recommend trimming of useless whitespace from user input, in addition to lowering the input. This makes user input more forgiving.

Anyways, that's about all I can come up with. if there's anything else that you want me to cover, just mention it in the comments, and I'll see what I can do. I hope this helps!

\$\endgroup\$
7
  • \$\begingroup\$ @EthanBierlein What does the health: int do in the constructor params? I tried it on Python 3.4.2, and while not a syntax error, it doesn't seem to do anything noticeable. \$\endgroup\$
    – Kroltan
    Commented Jun 19, 2015 at 23:47
  • 1
    \$\begingroup\$ @Kroltan It's called a function annotation. They allow you to declare parameter types and the return types of functions. For example: def power(a: float, b: float) -> float: return a ** b \$\endgroup\$ Commented Jun 19, 2015 at 23:57
  • \$\begingroup\$ @EthanBierlein Interesting, didn't know about that! But that doesn't enforce the types, right? \$\endgroup\$
    – Kroltan
    Commented Jun 19, 2015 at 23:59
  • \$\begingroup\$ @Kroltan But it does. If you try to pass in the incorrect type it'll raise a TypeError. \$\endgroup\$ Commented Jun 20, 2015 at 0:00
  • 5
    \$\begingroup\$ @Kroltan No, it doesn't enforce types. You have to use a library for this. Python 3.5 will provide such a library. See PEP 484. \$\endgroup\$
    – Joschua
    Commented Jun 20, 2015 at 12:04
11
\$\begingroup\$

str.format

Over the years of Python, there has mostly been two ways to join stings and variables. And now we have the new format way. We will use the following constants in the examples.

>>> a = 'You'
>>> b = 'Mega Punch'
>>> c = 50

The first way

>>> a + ' used ' + b + '. It dealt ' + str(c) + ' damage.'
'You used Mega Punch. It dealt 50 damage.'

This is annoying to type, and is unsafe if a or b change to an int. I didn't use the , operator, as it will make a tuple if you assign it to a variable.

The second way

>>> '%s used %s. It dealt %s damage.' % (a, b, c)
'You used Mega Punch. It dealt 50 damage.'

This is quite a lot like the new .format method. However there are arguments against it, as stated in the documentation for it:

The formatting operations described here exhibit a variety of quirks that lead to a number of common errors (such as failing to display tuples and dictionaries correctly). Using the newer str.format() interface helps avoid these errors, and also provides a generally more powerful, flexible and extensible approach to formatting text.

The str.format way

>>> '{} used {}. It dealt {} damage.'.format(a, b, c)
'You used Mega Punch. It dealt 50 damage.'
>>> '{1} used {0}. It dealt {2} damage.'.format(a, b, c)
'Mega Punch used You. It dealt 50 damage.'
>>> '{0} used {1}. {0} dealt {2} damage.'.format(a, b, c)
'You used Mega Punch. You dealt 50 damage.'
#Every console app that makes a UI needs this!
>>> '{}{:>15}'.format('Pika', '23/100')
'Pika         23/100'

This is just to show a few ways to use the str.format method. It also allows you to change the type of inputted variable, the amount of decimal places shown and manipulate lists. But most of all it just rocks!

About the * and ** operators.

There are plenty of questions about these operators. And the documentation for them is quite bare.

>>> a = random.randint(1, 9)
>>> a
9
>>> a
9
# It didn't change? That's 'cause you are not calling the function,
# you're just asking for the same output again. So if we could...
>>> l = [1, 9]
>>> random.randint(l)
TypeError: randint() takes exactly 3 arguments (2 given)
# And this is why we have the `*` operator
>>> random.randint(*l)
4
>>> random.randint(*l)
2

But I would recommend not using it. As you will come up with things like:

def print_lines(*lines):
    print("\n".join([line for line in lines]))
# This is confusing. And `print` is a function in python3!
# If we ignore how good print is we get
def print_lines(*lines):
    print('\n'.join(lines))

# But how do we use it?
print_lines('hello', 'world')
print_lines(*['hello', 'world'])

# "There should be one-- and preferably only one --obvious way to do it."
print('hello', 'world', sep='\n')
print(['hello', 'world'], sep='\n')

Print is a function now! And the quote is from The Zen of Python.

List Comprehension

I use this later in the Class example, and I bashed the other persons use of it. They allow you to reduce the amount of code you write. And you can simply change them so that they are generators. Generators are good for large amounts of data, as they only put the information into memory when needed.

# You have a list of strings, and you want them to be all lowercase.
>>> lst = [ ... ]
>>> lst2 = []
>>> for i in lst:
        lst2.append(i.lower())
# That's a lot of writing for such a simple task.
# And so we will use list comprehension.
>>> lst3 = [i.lower() for i in lst]
# And they are the same.
>>> lst2 == lst3
True

Classes

You want to have code re-use. And that is the brain dead response for what classes are! The main function of your code implement misses, moves, and checks to see if one of the Pokemon have fainted. When I was introduced to classes, I was told to use super, inheritance and a lot of things that I struggled with. And so I will give you a 'small' example of a simple way to make a class. (As you didn't use one I am assuming you don't like/understand/know them.)

moves={'Punch': [18, 25],
       'Mega Punch': [10, 35],
       'Heal': [-25, -20]
       }

moves_list=list(moves)
moves_list_lower=[move.lower() for move in moves_list]

move_names='\n'+'\n'.join(
    "{0}. {1} (Deal damage between '{2[0]}' - '{2[1]}')".format(
        i,
        move,
        moves[move]
    )
    for i, move in enumerate(moves_list)
)

class Pokemon:
    def __init__(self, title):
        self.title = title

    def select_move(self)
        move = input(move_names + '\n> ').lower()
        try:
            return moves_list[int(move)]
        except ValueError:
            return moves_list[moves_list_lower.index(move)]
        except IndexError:
            print('That is not a valid move. Please try again.')

    def use_move(self, other, move):
        # 20% of missing
        if random.randint(1,5):
            print('{} missed!'.format(self.title.capitalize()))
        else:
            # Works as shown earlier.
            magnitude = random.randint(*moves[move])
            if moves[move][0] < 0:
                # A simple self.health += magnitude
                self.heal(magnitude)
                desc = 'healed for {} health.'
            else:
                # A simple self.health -= magnitude
                other.deal(magnitude)
                desc = 'dealt {} damage.'
            print(('{} used {}. It' + desc).format(
                   self.title.capitalize(),
                   move,
                   magnitude
                   ))
  • The bits in global name-space are to make all the information about the moves.

    • We keep your simple dictionary of moves, as it stores all the information we need.
    • moves_list is a a copy of all the keys of the dictionary, so that we can index easier.
    • moves_list_lower is a lowercase copy of moves_list. This is so we can use it to see if the person entered a valid move.
    • move_names will output roughly the same as the print statement at the start of each players turn. However it will change every time you load the game, due to how dictionary's work. You can sort moves_list so that it becomes fixed if you wanted to 'fix' this. It also outputs healing as negative damage. I'm sure you can figure a way to fix that.

    All of it should however be wrapped in a if __name__ == "__main__": as @EthanBierlein said.

  • I left out some key points in this program, as it's just meant to show you how you could implement it. And allows you to do thinking on your own. I personally hate, 'learn classes, here's how __init__ works'.

  • I also used a try except statement. I don't know if it's fully functional, but it shows you how you can remove some clutter. For more information on these 'handling exceptions'.

  • If I were to continue making this program I would personally use inheritance and use method overriding to make it so the computer has a diffrent way to select_move.

  • If you want to know what the best way to style Python there is PEP8 and PEP257.


This answer was re-written so that it has a better layout, and presentation of information, rather than being a mess. I re-wrote this and hope that it is as good or better than the previous version. If not, revert it. -Author.

\$\endgroup\$
0

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.