4
\$\begingroup\$

This is my first leap into Python and I had a lot of fun writing this - it is not complete yet.

I'm interested in adding a perk system next, instead of generically increasing stats I'd like the player to be able to choose specific perks. General feedback and criticism is welcomed!

"""A Python3 'game' that generates enemies that scale according to the players specific attributes and level. Further features to be implemented. Wait for the screens to refresh before typing in new commands - unless you like losing."""

import random
import os
import time


class clearScreen(): # Simply so I don't have to type os.system('cls||clear')
# Progammers are lazy, right?
    def clearTheScreen():
        try:
            os.system('cls||clear')
        except:
            pass


class Character():
    def __init__(self, health, mana, defence, attack, level, experience, criticalChance, gold):
        self.health = health
        self.mana = mana
        self.defence = defence
        self.attack = attack
        self.level = level
        self.experience = experience
        self.criticalChance = criticalChance
        self.gold = gold


class Mage(Character):
    def __init__(self):
        super().__init__(health = 150,
                        mana = 100,
                        defence = 3,
                        attack = 16, # 12, 35 for debug
                        level = 1,
                        experience = 0,
                        criticalChance = 5, # 5 or 50 for testing
                        gold = 0)
    name = input("What is your name? ").upper()
    maxHealth = 150
    maxMana = 100 
    statUpPoints = 0 # Basically level up points
    inTheBattle = False # Checks battle state for enemy scaling 
    playerClass = "MAGE" # To determine which UI to print


class enemyWarrior(Character):
    def __init__(self):
        super().__init__(health = 1,
                        mana = 0,
                        defence = 0,
                        attack = 0,
                        level = 1,
                        experience = 0,
                        criticalChance = 0,
                        gold = 0)

    name = random.choice(["Skeleton Warrior", "Warrior of the Light", "Summoned Warrior"])
    maxHealth = 1
    maxMana = -20 # Stats can be increased or reduced to change the scale


class enemyMage(Character):
    def __init__(self):
        super().__init__(health = 1,
                        mana = 50,
                        defence = -2,
                        attack = 5,
                        level = 1,
                        experience = 0,
                        criticalChance = 0,
                        gold = 0)

    name = random.choice(["Ethereal Mage of Frost", "Wisp of Lightning", "Dark Wizard"])



def choosePlayer(): # Will expand when functionality is more complete

    choosePlayerCommand = input("Mage(1), 2, 3 ")

    if choosePlayerCommand == "1":
        player = Mage()
        return player

    else:
        player = Mage()
        return player


def enemySpawner():

    if player.playerClass == "MAGE":
        if player.level < 10:
            enemy = enemyWarrior()
            return enemy
        elif player.level < 20:
            enemy = enemyMage()
            return enemy


def levelUp():

    global experienceNeeded, menuBool # Eek globals, are these necesarry or could I deal with them in a different way?

    experienceNeeded = [] # Holds the sequence of numbers that determine how much xp is needed to level up
    menuBool = False # Important for printing proper menus

    maxLevel = 60; # Max Level Achievable by a Character
    a = 0; # Initialization
    b = 1; # Initialization
    # Determines how much experience is needed for each level
    for c in range(0 , maxLevel):
        c = a + b;
        a = b;
        b = c;
        experienceNeeded.append(c)
        # ^ Fibonacci, [1,1,2,3,5,8,13,21,34,55,89, etc...]

    while player.experience > experienceNeeded[player.level]:
        # ^ Checks if player has leveled up
        if player.level % 5 == 0: # Every 5 levels
            player.criticalChance += 2 # add 2% every 5 levels to critical chance, needs balancing

        player.level += 1 # Add one to players level
        player.health = player.maxHealth # Reset players health
        player.mana = player.maxMana # Reset players mana
        player.statUpPoints += 1 # Give 1 stat point per level up
        menuBool = True # Used to disengage the regular interface
        player.inTheBattle = False
        userInterface.printPerkMenu() # Opens up the Perk Menu


class scale(): # Scale those enemies, needs some balancing

    def scaleEnemies():
        # Quickly reset the stats so enemy scaling remains constant, I'm not sure how else to tackle this
        enemy.level = 0
        enemy.maxHealth = 0
        enemy.maxMana = 0
        enemy.attack = 0
        enemy.defence = 0
        enemy.experience = 0

        enemy.level += round(player.level + random.choice(range(1,5)))
        #enemy.level += round(abs((player.attack + player.defence) / 7) + (enemy.health / 25) + random.choice(range(1,3)))

        enemy.maxHealth += round(enemy.level * 10) + random.choice(range(35, 100))

        enemy.health = enemy.maxHealth

        enemy.maxMana += round(enemy.level * 5) + random.choice(range(25, 70))

        enemy.mana = enemy.maxMana

        enemy.attack += round((player.level + enemy.level) + (player.defence / 5) + random.choice(range(0,enemy.level)) + 2)

        enemy.defence += round((player.level + enemy.level) + (player.attack / 5) + random.choice(range(0,enemy.level)) + 2)

        enemy.experience += round(enemy.level * 50) + random.choice(range(player.level, player.maxHealth))

        #enemy.gold += round((enemy.level * 2) + random.choice(range(enemy.level, 35)))

        enemy.criticalChance += round(2 + (enemy.level * 0.10))

        player.inTheBattle = True


class userInterface(): # Our user interface is just text

    def printUI():

        print("\n",
        "[", player.name, "the", player.playerClass, "]", "\n",
        "Level: ", player.level, "| Experience: {}/{}".format(player.experience,experienceNeeded[player.level]), "\n",
        "Health: ", player.health, "/", player.maxHealth, "\n",
        "Mana: ", player.mana, "/", player.maxMana, "\n",
        "Defence: ", player.defence, "\n",
        "Attack: ", player.attack, "\n",
        "Critical Chance: ",player.criticalChance, "%", "\n")
        print(
        "[", enemy.name, "]", "\n",
        "Difficulty: ", enemy.level, "\n",
        "Health: ", enemy.health, "/", enemy.maxHealth, "\n",
        "Mana: ", enemy.mana, "/", enemy.maxMana, "\n",
        "Defence: ", enemy.defence, "\n",
        "Attack: ", enemy.attack, "\n",
        "Critical Chance: ",enemy.criticalChance, "%", "\n",
        "Experience: ",enemy.experience, "\n")

    def printPerkMenu(): # Menu for leveling up our stats

        statUp = (player.level + 1)
        statUpHealth = 25 + (player.level * 2)
        statUpMana = 10 + (player.level * 2)

        clearScreen.clearTheScreen() # Clear the screen!

        print("Level Up! Choose a Stat to Increase:", "\n",
        "Attack + {} [1]".format(statUp), "\n",
        "Defence + {} [2]".format(statUp), "\n",
        "Health + {} [3]".format(statUpHealth), "\n",
        "Mana + {} [4]".format(statUpMana), "\n")

        while player.statUpPoints > 0:

            if player.statUpPoints == 0:
                menuBool = False
                # ^ When no longer leveling up, display regular UI

            command = input("Level {}! >> ".format(player.level))

            if command == "1":
                player.attack += statUp
                player.statUpPoints -= 1
            elif command == "2":
                player.defence += statUp
                player.statUpPoints -= 1
            elif command == "3":
                player.health += statUpHealth
                player.maxHealth += statUpHealth
                player.statUpPoints -= 1
            elif command == "4":
                player.mana += statUpMana
                player.maxMana += statUpMana
                player.statUpPoints -= 1

            time.sleep(0.2)


    def playerAttackCommands(): # Temporary, will make seperate command functionality for each different player you can choose and will likely move everything except for the print statements outside of the userInterface class.

        damageDealtByPlayer = [0] # Weird way to store damage for printing
        playerCriticalHit = player.attack * 2 # Determines critical damage
        playerCriticalHitBool = False # Weird way to check for criticals
        playerHeal = round((player.level * player.maxHealth) / 20)
        playerHealCost = round((player.level * player.maxHealth) / 50)

        if player.playerClass == "MAGE":
            print("\n", "*"*40, "\n",
            "1.) Attack using your staffs power, has chance to critical.", "\n",
            "2.) Attack with magic for high damage! (Costs 25 Mana)", "\n",
            "3.) Heal yourself for {} health. (Costs {} Mana)".format(playerHeal, playerHealCost), "\n",
            "4.) Choice 4", "\n",
            "5.) Run Away!", "\n")

        chooseAttack = input(">> ")

        if chooseAttack == "1":

            damageDealtByPlayer[0] = round(player.attack - (0.25 * enemy.defence))

            if damageDealtByPlayer[0] > 0: # Checks if player was able to bypass enemies armor, if so determine if they critically hit
                if random.choice(range(100)) <= player.criticalChance:
                    playerCriticalHitBool = True # Used for printing mostly
                    damageDealtByPlayer[0] = (1 + playerCriticalHit) - (0.25 * enemy.defence) # Calculates critical hit
                    enemy.health -= damageDealtByPlayer[0] # Subtracts hit from enemies health points

                else:
                    enemy.health -= damageDealtByPlayer[0] # If player did not critically hit, calculate damage normally
            else:
                damageDealtByPlayer[0] = 1
                enemy.health -= 1

        if chooseAttack == "2": # Magic attack, needs balancing
            if player.mana >= 25:
                player.mana -= 25
                damageDealtByPlayer[0] = round(player.attack * 1.75) - (0.25 * enemy.defence)

                if damageDealtByPlayer[0] > 0:
                    enemy.health -= damageDealtByPlayer[0]
                else:
                    damageDealtByPlayer[0] = 1
                    enemy.health -= 1
            else:
                damageDealtByPlayer[0] = "NOMANA"


        if chooseAttack == "3": # Healing needs to be balanced
            if player.mana >= playerHealCost:
                player.mana -= playerHealCost
                player.health += playerHeal
                damageDealtByPlayer[0] = "HEAL"
            else:
                damageDealtByPlayer[0] = "NOMANA"

        if chooseAttack == "4":
            time.sleep(1)
            print("Debug")

        if chooseAttack == "5":

            player.health -= enemy.attack / 1.5
            damageDealtByPlayer[0] = "RUN"
            enemy.experience = 0
            enemy.health = 0

        try: # Clear the screen then prints damage dealt by player, will not always be under a try statement... this is temporary
            clearScreen.clearTheScreen()
            print("Calculating....")

            time.sleep(0.3)
            if damageDealtByPlayer[0] == "HEAL":
                print("You heal yourself for {} hit points.".format(playerHeal))

            if damageDealtByPlayer[0] == "NOMANA":
                print("Not enough mana!")

            if damageDealtByPlayer[0] == "RUN":
                print("You run away but take {} damage in return.".format(enemy.attack / 1.5))

            else:
                if playerCriticalHitBool == True:
                    print("You've Critically Hit For {} Damage".format(damageDealtByPlayer[0]))
                    playerCriticalHitBool = False

                else:
                    print("You've Hit For {} Damage".format(damageDealtByPlayer[0]))

            time.sleep(0.8)
            damageDealtByPlayer[0] = 0 # Reset damage so it does not stack

        except ValueError:
            pass


    def enemyAttackCommands(): # 'AI' for attacks dealt by enemies, this will also be seperated from the userInterface class except for the print statements

        enemyCriticalHit = 2 * enemy.attack
        enemyCriticalHitBool = False
        damageDealtByEnemy = [0]
        enemyHeal = round((enemy.level * enemy.maxHealth) / 20)
        enemyHealCost = round((enemy.level * enemy.maxHealth) / 55)
        damageDealtByEnemy[0] = round(enemy.attack - (0.25 * enemy.defence))

        if enemy.health < (0.40 * enemy.maxHealth) and enemy.mana >= enemyHealCost:

            enemy.mana -= enemyHealCost
            enemy.health += enemyHeal
            damageDealtByEnemy[0] = "HEAL"

        elif damageDealtByEnemy[0] > 0:

            if random.choice(range(100)) <= enemy.criticalChance:

                enemyCriticalHitBool = True
                damageDealtByEnemy[0] = 1 + enemyCriticalHit
                player.health -= damageDealtByEnemy[0]

            else:

                player.health -= damageDealtByEnemy[0]

        else:

            damageDealtByEnemy[0] = 1
            player.health -= damageDealtByEnemy[0]


        try: # Will not always be a try statement, this is temporary

            if damageDealtByEnemy[0] == "HEAL":

                print("The enemy heals itself for {} hit points.".format(enemyHeal))

            if enemyCriticalHitBool == True:

                print("You've Been Critically Hit For {} Damage".format(damageDealtByEnemy[0]))
                enemyCriticalHitBool = False

            else:

                print("You've Been Hit For {} Damage".format(damageDealtByEnemy[0]))


            time.sleep(1)
            damageDealtByEnemy[0] = 0


        except:
            pass



player = choosePlayer() # Initialize player
enemy = enemySpawner() # Initialize enemy


# Game Loop
while True:

    if player.health > player.maxHealth: # players health cannot exceed maximum
        player.health = player.maxHealth

    if player.mana > player.maxMana: # Players mana cannot exceed maximum
        player.mana = player.maxMana

    if enemy.health > enemy.maxHealth: # Enemies health cannot exceed maximum
        enemy.health = enemy.maxHealth

    if enemy.mana > enemy.maxMana: # Enemies mana cannot exceed maximum
        enemy.mana = enemy.maxMana


    if enemy.health <= 0: # Check if enemy has died
        print("You've slain the {} and gained {} experience!".format(enemy.name,enemy.experience))
        player.inTheBattle = False
        player.experience += enemy.experience # Add experience
        #player.gold += enemy.gold # Gold is useless as of right now
        enemy = enemySpawner() # Spawn new enemy
        time.sleep(3) # Pause screen for ~ 3 seconds

    if player.health <= 0: # Check if player has died
        break # DEAD

    if player.inTheBattle == False: # Check if player is battling an enemy otherwise the enemy will constantly be respawned with different stats and never lose health.
        scale.scaleEnemies() # Class for scaling enemies stats and levels

    levelUp() # Used for some calculations and pre-battle prep

    if menuBool == False: # If leveling up, don't bother printing anything else

        userInterface.printUI() # Prints user interface
        if player.health > 0: # Check if player is still alive
            userInterface.playerAttackCommands() # Start combat for player

        if enemy.health > 0: # Check if enemy is alive
            userInterface.enemyAttackCommands() # Starts enemy battle 'AI'
\$\endgroup\$
  • \$\begingroup\$ If player enters anything except 1-5, no action happens, but still the program will print "Calculating..." and "You've Hit For {} Damage" despite not actually using any default attack. \$\endgroup\$ – Gnudiff Jan 12 '18 at 6:21
2
\$\begingroup\$

Some standard criticisms apply:

  1. Objects should manage their own data.

  2. User interface logic should not be part of model objects.

  3. For model objects, pass in data as parameters. Especially data from the user, like name.

With that out of the way, there are some things I think you might be fuzzy on:

  • Why is ClearScreen a class? Why isn't this simply a user interface function?

  • Everything has a name. But name isn't part of the Character class. Why not?

  • Do you realize that setting variables at "class level" makes them class variables, instead of instance variables? That is, when you do this:

    class enemyWarrior(Character):
        def __init__(self):
            ...
    
        name = random.choice(["Skeleton Warrior", "Warrior of the Light", "Summoned Warrior"])
    

    You are setting the name of all enemyWarrior objects to that value, rather than having each enemy warrior randomly pick a name. (This might be what you intend, I can't tell.)

  • For operations like choosePlayer ask yourself, "What am I doing? What data am I using? What is this a part of?" Specifically, choosePlayer seems like a user-interface method, since it interacts with the user. (You might break it into two methods, one for talking to the player and one for creating a Mage, but I don't think that's necessary at this point.)

  • The difficulty scaling is a good idea. But think about who should be doing that. It seems to me that might be a method of the enemy classes, with an input of the player, or the player's level. Or it might be a user interface function, or a function of the "game board" if there is such a thing.

\$\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.