5
\$\begingroup\$

I have recently started to learn python, and I have made my first fighting duel game. I'm planning on adding more stuff in the future like shops and items so you get a way to spend gold, but for now the current features are:

  • Character creation (with saving and loading),
  • Upgrading Str/Def,
  • Keeping Score
  • Battling AI or Battling 1v1.
  • (you also get rewards for winning)

But for now I'm happy with my progress. Please review my code!

import random
import json
import time
r = random
#INSTRUCTIONS FOR FIRST TIME USE
#MAKE 3 COMPUTER ACCOUNTS NAMED "Computer_Easy", "Computer_Med" and "Computer_Hard" AND SET THEIR STATS HOWEVER YOU LIKE! OTHERWISE UNABLE TO PLAY VS AI
class Character():   
#Change value of default if you want people to start with higher str/def when they make their account.
    def __init__(self, name, default=1, health=None, strength=None,
                 defence=None, gold=0, exp=0, wins=0, losses=0,
                ):
        self.name = name
        self.health = 100
        self.strength = default if strength is None else strength 
        self.defence = default if defence is None else defence 
        self.gold = 0
        self.exp = 0
        self.wins = 0
        self.losses = 0

    def make_account(self, name): #Create a new user to /accounts folder with default stats as listed above
        save_name = name
        path = 'accounts/{0}.json'.format(save_name)
        data = {
        "health": self.health,
         "strength": self.strength, 
         "defence": self.defence, 
         "gold": self.gold, 
         "exp": self.exp, 
         "wins": self.wins, 
         "losses": self.losses, 
        }
        with open(path, 'w+') as f:
            json.dump(data, f)

    def load_account(self, name):
        load_name = name
        path_two = 'accounts/{0}.json'.format(load_name)
        with open(path_two, 'r') as f:
            data = json.load(f)
            return data

    def upgrade_stat(self, name): #Upgrade either defence or strength
        target = raw_input("What stat do you want to upgrade?")
        if target in {"health", "gold", "exp", "wins", "losses"}:
            print("you cannot do that!")
            return
        load_name = name
        path_two = 'accounts/{0}.json'.format(load_name)
        with open(path_two, 'r') as f:
            data = json.load(f)
        if data["exp"] < 100:
            print("You don't have enough xp!")
        else:
            data["exp"] -= 100
            data[target] += 1
            with open (path_two, 'w+') as g:
                json.dump(data, g)


    def calculate_damage(self, damage_amount, attacker, defender):
        attacker_stats = self.load_account(name=attacker)
        defender_stats = self.load_account(name=defender)

        #You get damage boost, depending on attacker strength level and defender defence level. 
        damage_amount_boost = (attacker_stats["strength"] * damage_amount / (attacker_stats["strength"] + defender_stats["defence"])) + damage_amount
        if (damage_amount_boost > self.health):
            overkill = abs(self.health - damage_amount_boost)
            self.health = 0
            if (overkill > 0):
                print("{0} takes fatal damage from {1}, with {2} overkill!"
                      .format(self.name.capitalize(), attacker, overkill))
            else:
                print("{0} takes fatal damage from {1}!"
                      .format(self.name.capitalize(), attacker))
        else:
            self.health -= damage_amount_boost
            print("{0} takes {1} damage from {2}!"
                  .format(self.name.capitalize(), damage_amount_boost, attacker))

    def calculate_heal(self, heal_amount):
        if (heal_amount + self.health > 100):
            self.health = 100
            print("{0} heals back to full health!"
                  .format(self.name.capitalize()))
        else:
            self.health += heal_amount
            print("{0} heals for {1}!"
                  .format(self.name.capitalize(), heal_amount))

def parse_int(input):
    try:
        int(input)
        return True
    except ValueError:
        return False

def get_selection():
    valid_input = False
    while (valid_input is False):
        print()
        choice = input("Select an attack: ")
        if (parse_int(choice) is True):
            return int(choice)
        else:
            print("The input was invalid. Please try again.")

def get_computer_selection(health):
    sleep_time = random.randrange(2, 5)
    print("Computer thinking...")
    time.sleep(sleep_time)

    if (health <= 35):
        # Have the computer heal ~50% of its turns when <= 35
        result = random.randint(1, 6)
        if (result % 2 == 0):
            return 3
        else:
            return random.randint(1, 2)
    elif (health == 100):
        return random.randint(1, 2)
    else:
        return random.randint(1, 3)

def play_round_ai(computer, human):
    game_in_progress = True
    current_player = computer
    movesprint = '1. Mild, 2. Hard, 3. Heal'

    while game_in_progress:
        if (current_player == computer):
            current_player = human
        else:
            current_player = computer
        print(
            "You have {0} health remaining and the "
            "computer has {1} health remaining."
            .format(human.health, computer.health))
        if (current_player == human):
            print(movesprint)
            move = get_selection()
        else:
            move = get_computer_selection(computer.health)

        if (move == 1):
            damage = random.randrange(12, 22)
            if (current_player == human):
                computer.calculate_damage(damage, human.name, computer.name)
            else:
                human.calculate_damage(damage, computer.name, human.name)
        elif (move == 2):
            damage = random.randrange(5, 35)
            if (current_player == human):
                computer.calculate_damage(damage, human.name, computer.name)
            else:
                human.calculate_damage(damage, computer.name, human.name)
        elif (move == 3):
            heal = random.randrange(15, 28)
            current_player.calculate_heal(heal)
        else:
            print ("The input was not valid. Please select a choice again.")

        if (human.health == 0):
            print("Sorry, you lose!")
            computer.wins += 1
            load_names = human.name
            paths = 'accounts/{0}.json'.format(load_names)
            with open(paths, 'r') as f:
                data = json.load(f)
            data["losses"] += 1
            with open(paths, 'w+') as g:
                json.dump(data, g)
            game_in_progress = False

        if (computer.health == 0):
            print("Congratulations, you beat the computer!")
            human.wins += 1
            load_names = human.name
            paths = 'accounts/{0}.json'.format(load_names)
            with open(paths, 'r') as f:
                data = json.load(f)
            data["exp"] += 25
            data["gold"] += 100
            data["wins"] += 1
            with open(paths, 'w+') as g:
                json.dump(data, g)
            game_in_progress = False

def play_round_multiplayer(player2, player1):
    game_in_progress = True
    current_player = player2
    movesprint = '1. Mild, 2. Hard, 3. Heal'

    while game_in_progress:
        if (current_player == player2):
            current_player = player1
        else:
            current_player = player2
        #print()
        print(
            "You have {0} health remaining and the "
            "player2 has {1} health remaining."
            .format(player1.health, player2.health))
        #print()
        if (current_player == player1):
            print(movesprint)
            move = get_selection()
        else:
            print(movesprint)
            move = get_selection()

        if (move == 1):
            damage = random.randrange(12, 22)
            if (current_player == player1):
                player2.calculate_damage(damage, player1.name, player2.name)
            else:
                player1.calculate_damage(damage, player2.name, player1.name)
        elif (move == 2):
            damage = random.randrange(5, 35)
            if (current_player == player1):
                player2.calculate_damage(damage, player1.name, player2.name)
            else:
                player1.calculate_damage(damage, player2.name, player1.name)
        elif (move == 3):
            heal = random.randrange(15, 28)
            current_player.calculate_heal(heal)
        else:
            print ("The input was not valid. Please select a choice again.")

        if (player1.health == 0):
            print("{0} wins".format(player2.name))
            player2.wins += 1
            winner_name = player2.name
            loser_name = player1.name
            winner_path = 'accounts/{0}.json'.format(winner_name)
            loser_path = 'accounts/{0}.json'.format(loser_name)
            with open(winner_path, 'r') as f:
                data = json.load(f)
            data["exp"] += 25
            data["gold"] += 100
            data["wins"] += 1
            with open(winner_path, 'w+') as g:
                json.dump(data, g)
            with open(loser_path, 'r') as f:
                data = json.load(f)
            data["losses"] += 1
            with open(loser_path, 'w+') as g:
                json.dump(data, g)
            game_in_progress = False

        if (player2.health == 0):
            print("{0} wins".format(player1.name))
            player1.wins += 1
            winner_name = player1.name
            loser_name = player2.name
            winner_path = 'accounts/{0}.json'.format(winner_name)
            loser_path = 'accounts/{0}.json'.format(loser_name)
            with open(winner_path, 'r') as f:
                data = json.load(f)
            data["exp"] += 25
            data["gold"] += 100
            data["wins"] += 1
            with open(winner_path, 'w+') as g:
                json.dump(data, g)
            with open(loser_path, 'r') as f:
                data = json.load(f)
            data["losses"] += 1
            with open(loser_path, 'w+') as g:
                json.dump(data, g)
            game_in_progress = False

def start_game_ai(name): #Play vs Ai, which you select your difficulty.
    target = raw_input("Easy, med or hard?")
    if target == "easy":
        computer = Character("Computer_Easy") #Make sure you have made all of these accounts in folder Accounts
    if target == "med":                         #Set stats however you like
        computer = Character("Computer_Med")
    if target == "hard":
        computer = Character("Computer_Hard")
    human = Character(name)

    keep_playing = True

    while (keep_playing is True):
        print("Current Score:")
        print("You - {0}".format(human.wins))
        print("Computer - {0}".format(computer.wins))

        computer.health = 100
        human.health = 100

        play_round_ai(computer, human)
        print()
        response = raw_input("Play another round?(Y/N)")
        if (response.lower() == "n"):
            break

def start_game_multiplayer(name, target): #Play a local 1v1 game.
    player2 = Character(target) 
    player1 = Character(name) 

    keep_playing = True

    while (keep_playing is True):
        print("Current Score:")
        print("Player1 - {0}".format(player1.wins))
        print("Player2 - {0}".format(player2.wins))
        player2.health = 100
        player1.health = 100
        play_round_multiplayer(player2, player1)
        #print()
        response = raw_input("Play another 1v1? (y/n)")
        if (response == "y"):
            continue
        else:
            break

def options(): #Main menu
    name = raw_input("Username to load or create?") 
    print("What do you want to do? If this is your first time, type 5 to create your new account!")
    move = raw_input("1. Battle AI | 2. List stats | 3. Play 1v1 | 4. Upgrade str/def | 5. Make new user")
    if move == "1":
        print("Make sure you already have made all Computer accounts! (Computer_Easy, Computer_Med, Computer_Hard)")
        start_game_ai(name)
    if move == "2":
        stats = Character(name).load_account(name)
        print stats
        return
    if move == "3":
        target = raw_input("Who do you want to fight?") 
        start_game_multiplayer(name, target)
    if move == "4":
        Character(name).upgrade_stat(name)
        stats = Character(name).load_account(name)
        print stats
    if move == "5":
        Character(name).make_account(name)
    else:
        print("Exiting..")
        return

options()
\$\endgroup\$
5
\$\begingroup\$

Currently your code is a bit all over the place. You have a Character class that can keep track of how much e.g. exp a character has. But when it comes to adding exp, you instead use a data dictionary.

Also, instead of saving the Character to file, you save that dictionary instead. The only thing where you actually use that class is to get the characters name.

So you have two options:

  1. Get rid of the class and always use the data dictionary.
  2. Make all of those things something the class handles.

I would choose option 2 here.

Here is a start on that road:

import random
import json
import time
r = random
# INSTRUCTIONS FOR FIRST TIME USE
# MAKE 3 COMPUTER ACCOUNTS NAMED "Computer_Easy", "Computer_Med" and
# "Computer_Hard" AND SET THEIR STATS HOWEVER YOU LIKE! OTHERWISE UNABLE
# TO PLAY VS AI


class CharacterDoesNotExist(Exception):
    pass


class Character(object):
    # Change value of default if you want people to start with higher str/def
    # when they make their account.
    path = 'accounts/{}.json'
    mild_attack = 12, 22
    hard_attack = 5, 35
    heal_amount = 15, 28
    default = 1

    def __init__(self, name, health=None, strength=None,
                 defence=None, gold=0, exp=0, wins=0, losses=0):
        self.name = name
        self.health = 100 if health is None else health
        self.strength = self.default if strength is None else strength
        self.defence = self.default if defence is None else defence
        self.gold = 0 if gold is None else gold
        self.exp = 0 if exp is None else exp
        self.wins = 0 if wins is None else wins
        self.losses = 0 if losses is None else wins

    @classmethod
    def load(cls, name):
        path = cls.path.format(name)
        try:
            with open(path) as f:
                return cls(**json.load(f))
        except Exception:
            raise CharacterDoesNotExist(name)

    def save(self):
        path = self.path.format(self.name)
        with open(path, 'w+') as f:
            json.dump(self.__dict__, f)

    def __str__(self):
        return self.name.capitalize()

    def upgrade_stat(self):  # Upgrade either defence or strength
        if self.exp < 100:
            print("You don't have enough exp!")
            return
        target = raw_input("What stat do you want to upgrade?")
        if target not in ("strength", "defence"):
            print("You cannot do that!")
        else:
            self.exp -= 100
            setattr(self, target, getattr(self, target) + 1)
            self.save()

    def attack(self, defender, damage_amount):
        # You get damage boost, depending on attacker strength level and
        # defender defence level.
        damage_amount_boost = (self.strength * damage_amount /
                               (self.strength + defender.defence)) + damage_amount
        if damage_amount_boost > defender.health:
            overkill = abs(self.health - damage_amount_boost)
            defender.health = 0
            if overkill > 0:
                print("{} takes fatal damage from {}, with {} overkill!".format(
                    defender, self, overkill))
            else:
                print("{} takes fatal damage from {}!".format(defender, self))
        else:
            defender.health -= damage_amount_boost
            print("{} takes {} damage from {}!".format(
                defender, damage_amount_boost, self))

    def heal(self, heal_amount):
        if (heal_amount + self.health > 100):
            self.health = 100
            print("{} heals back to full health!".format(self))
        else:
            self.health += heal_amount
            print("{} heals for {}!".format(self, heal_amount))

    def get_selection(self):
        print 'Avalaible moves: 1. Mild Attack, 2. Hard Attack, 3. Heal'
        while True:
            try:
                choice = int(input("Select a move: "))
                if not 1 <= choice <= 3:
                    continue
                return choice
            except ValueError:
                print("The input was invalid. Please try again.")

    def make_move(self, player2):
        choice = self.get_selection()
        if choice == 1:
            # mild attack
            damage = random.randint(*self.mild_attack)
            self.attack(player2, damage)
        elif choice == 2:
            # mild attack
            damage = random.randint(*self.hard_attack)
            self.attack(player2, damage)
        elif choice == 3:
            # mild attack
            heal = random.randint(*self.heal_amount)
            self.heal(heal)

    def win(self):
        self.wins += 1
        self.exp += 25
        self.gold += 100
        self.save()

    def loose(self):
        self.losses += 1
        self.save()


class Computer(Character):

    def get_selection(self):
        print("Computer thinking...")
        time.sleep(random.randrange(2, 5))

        if self.health <= 35:
            # Have the computer heal ~50% of its turns when <= 35
            if random.random() >= 0.5:
                return 3
            return random.randint(1, 2)
        elif self.health == 100:
            return random.randint(1, 2)
        return random.randint(1, 3)

    def win(self):
        self.wins += 1

    def loose(self):
        self.losses += 1


def play_round(player1, player2):
    player1.health = player2.health = 100
    while True:
        print("\n{} has {} health remaining and {} has {} health remaining.".format(
            player1, player1.health, player2, player2.health))
        for _ in range(2):
            player1.make_move(player2)
            if player2.health <= 0:
                print("{} looses!".format(player2))
                player1.win()
                player2.loose()
                return
            # swap the players
            player1, player2 = player2, player1


# Play vs Ai, which you select your difficulty.
def play_vs_ai(human, computer):
    while True:
        print "Current Score:"
        print "You - {}".format(human.wins)
        print "Computer - {}".format(computer.wins)
        play_round(computer, human)
        print
        if raw_input("Play another round?(Y/N)").lower() == "n":
            break


def play_1_vs_1(player1, player2):  # Play a local 1v1 game.
    while True:
        print "Current Score:"
        print "Player1 - {}".format(player1.wins)
        print "Player2 - {}".format(player2.wins)
        play_round(player1, player1)
        print
        if raw_input("Play another 1v1? (y/n)").lower() != "y":
            break


def main_menu():  # Main menu
    name = raw_input("Username to load or create?")
    try:
        player1 = Character.load(name)
    except CharacterDoesNotExist:
        player1 = Character(name)
        player1.save()
    while True:
        print("What do you want to do?")
        move = raw_input(
            "1. Battle AI | 2. List stats | 3. Play 1v1 | 4. Upgrade str/def")
        if move == "1":
            target = raw_input("Easy, med or hard?")
            if target.lower() == "easy":
                computer = Computer("Computer_Easy")
            elif target.lower() == "med":  # Set stats however you like
                computer = Computer("Computer_Med", strength=2, defence=3)
            elif target.lower() == "hard":
                computer = Computer("Computer_Hard", strength=5, defence=4)
            play_vs_ai(player1, computer)
        elif move == "2":
            print player1.__dict__
        elif move == "3":
            target = raw_input("Who do you want to fight?")
            try:
                player2 = Character.load(target)
            except CharacterDoesNotExist:
                player2 = Character(target)
                player2.save()
            play_1_vs_1(player1, player2)
        elif move == "4":
            while True:
                player = raw_input("Which player: {} or {}")
                if player == player1.name:
                    player1.upgrade_stat()
                    break
                # elif player == player2.name:
                #    player2.upgrade_stat()
                #    break
        else:
            print("Exiting..")
            return
if __name__ == "__main__":
    main_menu()

Some notable changes:

  • The Character class inherits from object, making it a new-style class.
  • This uses a Computer class that inherits from Character and overrides some methods to allow the computer to make its decisions and prevent it from saving its wins and losses.
  • It has a classmethod load that loads a character from a file and a save method to save it.
  • The __str__ magic method makes sure that when we print a character that we see their name
  • Instead of disallowing some stats (like wins) to be upgraded, explicitly allow only strength and defence to be upgraded (whitelist vs blacklist).
  • A Character can attack and heal. By how much is encoded as a class variable.
  • Some of the attributes can be reached like for a dict, to ease the upgrading of a stat.
  • There is a make_move function that performs a characters move, given a choice of moves.
  • The play_round function is generic enough that it does not care if both players are humans or even both are computers.
  • The only thing that does not currently work is upgrading the stats of player 2, if they are human.
  • You don't need to specify the position in str.format strings, if you use them in the order you pass it in. In other words "{0} {1}".format(1, 2) and "{} {}".format(1, 2) are equivalent.
  • It uses a if __name__ == "__main__": guard to allow importing from this module.
  • A new character is automatically created if the loading fails.
  • The AI players have default values now. Whether or not they make sense, I don't know (I managed to consistently beat the easy AI...).
  • Consistently use print as a statement, as it was in Python 2.

Some things which you should still change:

\$\endgroup\$
  • 2
    \$\begingroup\$ Instead of printing a __dict__ prefer using vars. Also, if defining __getitem__ and __setitem__ is only useful for self[target] += 1 in upgrade_stat you shoul favor getattr and/or hasattr instead and handle a failure the same as a restricted attribute. Besides, you may want to check for the XP amount before asking what to upgrade. \$\endgroup\$ – Mathias Ettinger Jul 12 '18 at 7:12
  • \$\begingroup\$ @MathiasEttinger: Agree, when I was writing it I thought I might need to set and get more attributes. Removed those methods entirely and replaced them with a call to setattr and getattr. Btw, I am checking that the user has enough exp. \$\endgroup\$ – Graipher Jul 12 '18 at 8:57
  • 1
    \$\begingroup\$ But the check comes after asking the question ;) \$\endgroup\$ – Mathias Ettinger Jul 12 '18 at 9:00
4
\$\begingroup\$
  1. parse_int() has no reason to be a function, as it doesn't simplify anything. get_selection() could look like this:

    def get_selection():
        valid_input = False
        while (valid_input is False):
            print()
            choice = input("Select an attack: ")
            try:
                return int(choice)
            except ValueError:
                print("The input was invalid. Please try again.")
    
  2. For comparing object identity (i.e. current_player == player2 or current_player == computer) use is. this checks if the objects are the same, rather than if they have the same values. In your case, because you did not override == they should currently act the same.

  3. You can abstract out a single human or computer turn as a function, so that the different combinations of human computer play are all the same code. This will also make it easier to do AI vs. AI duels.

  4. Try to separate the game code from the output code. What if you or someone else wanted to make a different user interface for your game? First try to remove printing from the Character class, then try to make a quiet turn function that handles dueling using arguments and return values.

  5. Instead of r = random you can do import random as r. In my opinion however, it is not a good idea to change the name of a module as it makes it harder for readers to figure out which module you are using.

\$\endgroup\$
2
\$\begingroup\$

Try to eliminate repetitions. For example, you have a lot of “generate filename, open, read/write” statements. Instead, create a separate class which could use along the lines of

storage = FileStorage(name)  # reads "accounts/{name}.json" into storage.data
storage.data["something"] = whatever
storage.save()

Where else are you repeating yourself a lot? You have

if (current_player == human):
    computer.calculate_damage(damage, human.name, computer.name)
else:
    human.calculate_damage(damage, computer.name, human.name)

But you can simplify this to

active_player, non_active_player = human, computer

...

active_player.calculate_damage(damage, active_player.name, non_active_player.name)

...

# and when the turn ends:
active_player, non_active_player = non_active_player, active_player

And so on.

Repetition is bad because whenever you want to make a change you have to do it in multiple places, and you risk failing to update all the places or updating some of them differently from others.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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