10
\$\begingroup\$

I created a text-based RPG.

game.py

import random
import os
import items
from maps import mappa
import pickle


class Player:
    def __init__(self, name):
        self.name = name
        self.lvl = 1
        self.points = 5
        self.attributepoints = 2
        self.str = 5
        self.agy = 5
        self.vit = 1
        self.ene = 1
        self.exp = 1
        self.exptolvl = self.lvl * 2.5
        self.maxhp = 100 + (self.vit * 2)
        self.maxmp = 100 + (self.ene * 2)
        self.hp = self.maxhp
        self.mp = self.maxmp
        self.gold = 50
        self.primary = [items.Wood_Sword]
        self.secondary = [items.Wood_Shield]
        self.armor = [items.Wood_Armor]
        self.attack = self.str + self.primary[0].attack + self.agy
        self.defense = self.agy + (self.armor[0].attack/2) + (self.secondary[0].attack/2) + (self.vit / 1.5)
        self.magicattack = ((self.ene * 1.5) + self.primary[0].attack + self.agy)/2
        self.inventory = []


    def update(self):
        self.attack = self.str + self.primary[0].attack + self.agy
        self.defense = self.agy + (self.armor[0].attack/2) + (self.secondary[0].attack/2) + (self.vit / 1.5)
        self.magicattack = ((self.ene * 1.5) + self.primary[0].attack + self.agy) / 2


def main():
    clear()
    print("New Game")
    print("Load Game")
    print("Options")
    print("Exit")
    option = input("What do you want to do: ").lower()
    if option == "new game":
        start()
    elif option == "load game":
        loadgame()
    else:
        main()


def load(save):
    global playerig
    playerig = save
    input(f"Welcome back {playerig.name}, we missed you")
    game()


def start():
    print("Hello welcome to mu")
    name = input("What is your name ? \n")
    global playerig
    playerig = Player(name)
    game()


def game():
    os.system('cls')
    print(f"Explore             Level {round(playerig.lvl, 2)}  Health {round(playerig.hp, 2)}/{round(playerig.maxhp, 2)}   Mana {round(playerig.mp, 2)}/{round(playerig.maxmp, 2)}")
    print("Inventory")
    print("Character           Primary Weapon       Armor         Shield")
    print(f"Harald                {playerig.primary[0].name}       {playerig.armor[0].name}     {playerig.secondary[0].name}")
    print("Skills")
    print(f"Credits             Experience   Attack   Magic Attack   Defense")
    print(f"Save                  {playerig.exp}/{playerig.exptolvl}       {round(playerig.attack, 2)}        {round(playerig.magicattack, 2)}         {round(playerig.defense, 2)}")
    print("Exit")
    option = input("\nWhat do you want to do ?\n").lower()
    if option == "credits":
        input("Just you")
        game()
    elif option == "skills":
        skillupgrade()
    elif option == "harald":
        harald()
    elif option == "inventory":
        inventory()
    elif option == "explore":
        explore(mappa)
    elif option == "character":
        character()
    elif option == "exit":
        input("Cyaaa\n")
        os.system("exit")
    elif option == "save":
        savegame()
    else:
        game()


def explore(map):
    clear()
    print(f"You are in {map.curzone}")
    print("\nWrite travel north or travel south to explore new zones else write back to go back.")
    option = input("\nWhat do you want to do?\n").lower()
    if option == "travel north" or option == "travel south":
        clear()
        if map.loot == True:
            luck = random.randint(1, 30)
            if luck == 17:
                print(f"There is something on the ground")
                print(f"...")
                print(f"You found  {items.Great_Sword.name}")
                giveitem(items.Great_Sword)
                input("Press enter to continue")
        if option == "travel north":
            if map.walking < 40:
                playerig.hp += (playerig.maxhp * 0.10)
                if playerig.hp > playerig.maxhp:
                    playerig.hp = playerig.maxhp
                playerig.hp += (playerig.maxhp * 0.10)
                if playerig.mp > playerig.maxmp:
                    playerig.mp = playerig.maxmp
                map.walking += 1
                if map.walking == 5:
                    map.count = 1
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 10:
                    map.count = 2
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 20:
                    map.count = 3
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 40:
                    map.count = 4
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                print("You advance\n")
                print("Nothing to see around")
            elif map.walking >= 4:
                input("You can't go more north than this\n")
        elif option == "travel south":
            if map.walking > 0:
                playerig.hp += (playerig.maxhp * 0.10)
                if playerig.hp > playerig.maxhp:
                    playerig.hp = playerig.maxhp
                playerig.hp += (playerig.maxhp * 0.10)
                if playerig.mp > playerig.maxmp:
                    playerig.mp = playerig.maxmp
                map.walking -= 1
                if map.walking == 0:
                    map.count = 0
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 5:
                    map.count -= 1
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 10:
                    map.count -= 2
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                elif map.walking == 20:
                    map.count -= 3
                    map.curzone = map.zone[map.count]
                    print(f"You arrived in {map.curzone}\n")
                print("You advance\n")
                print("Nothing to see around")
            elif map.walking <= 0:
                input("You can't go more south than this\n")
        if map.fight == True:
            monsterlist = [items.Spider, items.Huge_Spider]
            monsterlist1 = [items.Baby_Dragon, items.Goblin]
            monsterlist2 = [items.Stone_Goblin, items.Cyclop]
            monsterlist3 = [items.Great_Dragon, items.Cyclop]
            monsterlist4 = [items.Great_Dragon, items.Great_Dragon, items.Great_Dragon, items.Golden_Dragon]
            luck = random.randint(1, 2)
            if luck == 2:
                if map.curzone == map.zone[0]:
                        luck1 = random.choice(monsterlist)
                        print(luck1.name)
                        fight(luck1)
                elif map.curzone == map.zone[1]:
                        luck1 = random.choice(monsterlist1)
                        fight(luck1)
                elif map.curzone == map.zone[2]:
                        luck1 = random.choice(monsterlist2)
                        fight(luck1)
                elif map.curzone == map.zone[3]:
                        luck1 = random.choice(monsterlist3)
                        fight(luck1)
                elif map.curzone == map.zone[4]:
                        luck1 = random.choice(monsterlist4)
                        fight(luck1)
            else:
                print("\nNo monsters around")
            input("\nPress enter to continue")
        explore(map)
    elif option == "back":
        game()
    else:
        input("\nPlease input a valid command")
        explore(map)


def fight(monster):
    clear()
    print(f"You are fighting {monster.name}")
    print(f"{playerig.name}'s Health {playerig.hp}      {monster.name}'s Health {monster.hp}")
    print(f"{playerig.name}'s Health {playerig.mp}")
    print("\nAttack")
    print("Skills")
    print("Run")
    if playerig.hp <= 0:
        dead()
    elif monster.hp <= 0:
        reward(monster)
    else:
        option = input("\nWhat do you want to do?\n").lower()
        if option == "attack":
            monster.hp -= playerig.attack
            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook
            input(f"You gave {playerig.attack} damage and took {damagetook} damage")
            fight(monster)
        elif option == "skills":
            skills()
            option1 = input('\nWhat skill do you want to use?\n').lower()
            if option1 == "twisting" or option1 == "ice storm":
                monster.hp -= useskill(option1)
                damagetook = random.randint(monster.minattack, monster.maxattack)
                playerig.hp -= damagetook
                input(f"You gave {useskill(option1)} damage and take {damagetook} damage")
                fight(monster)
            elif option1 == "regenerate":
                damagetook = random.randint(monster.minattack, monster.maxattack)
                playerig.hp -= damagetook
                input(f"You took {damagetook} damage and healed for {useskill(option1)} hp")
                fight(monster)
            else:
                input("Enter a valid skill name\n")
                fight(monster)
        elif option == "run":
            probability = random.randint(1, 2)
            if probability == 1:
                print("You ran away")
                input("Press enter to continue")
                explore(mappa)
            else:
                print("You couldn't run away")
                playerig.mp -= 5
                print("Your mana decrease by 5")
                monster.hp -= playerig.attack
                damagetook = random.randint(monster.minattack, monster.maxattack)
                playerig.hp -= damagetook
                input(f"You gave {damagetook} damage and took {monster.attack} damage")
                fight(monster)
        else:
            input("Enter a valid command\n")
            fight(monster)


def shop():
    print(f"Available items: {items.Great_Sword.name} and {items.Exe_Sword.name}")
    print("Buy")
    print("Back")
    option = input("What do you do: ").lower()
    if option == "back":
        game()
    elif option == "buy":
        option1 = input("What do you want to buy ?\n").lower()
        if option1 == "great sword":
            print("You bought the item")
            playerig.inventory.append(items.Great_Sword)
            game()
        elif option1 == "exe sword":
            print("You bought the item")
            playerig.inventory.append(items.Exe_Sword)
            game()
        else:
            print("Invalid input")
            game()


def inventory():
    clear()
    print("Inside the Inventory you have: ")
    c = 1
    for i in playerig.inventory:
        print(f"{c}.{i.name}")
        c += 1
    print(f"\nGold: {playerig.gold}")
    c = 1
    option = input("\nWrite back to go back else write wear to equip an item\n").lower()
    if option == "back":
        game()
    elif option == "wear":
        wearitem()
    else:
        inventory()


def wearitem():
    try:
        item = int(input("Write the number of the item you want to wear\n"))
        item -= 1
        if item in range(len(playerig.inventory)):
            if playerig.inventory[item].type == "Weapon":
                playerig.inventory.append(playerig.primary[0])
                playerig.primary.pop(0)
                print(f"You wore the item {playerig.inventory[item].name}")
                playerig.primary.append(playerig.inventory[item])
                playerig.inventory.pop(item)
                playerig.update()
                input("Press enter to continue")
                game()
            elif playerig.inventory[item].type == "Shield":
                playerig.inventory.append(playerig.secondary[0])
                playerig.secondary.pop(0)
                print(f"You wore the item {playerig.inventory[item].name}")
                playerig.secondary.append(playerig.inventory[item])
                playerig.inventory.pop(item)
                playerig.update()
                input("Press enter to continue")
                game()
            elif playerig.inventory[item].type == "Armor":
                playerig.inventory.append(playerig.armor[0])
                playerig.armor.pop(0)
                print(f"You wore the item {playerig.inventory[item].name}")
                playerig.armor.append(playerig.inventory[item])
                playerig.inventory.pop(item)
                playerig.update()
                input("Press enter to continue")
                inventory()
    except:
        input("Please chose a number from the inventory.")
        inventory()

def giveitem(item):
    playerig.inventory.append(item)


def character():
    clear()
    print(f"Available points {playerig.points}\n")
    print(f"Strength {playerig.str}")
    print(f"Agility {playerig.agy}")
    print(f"Vitality {playerig.vit}")
    print(f"Energy {playerig.ene}")
    print("\nTo add points write: Attribute press enter then chose the amount of points you want to add")
    print("Example:")
    print("Strength")
    print("3")
    print("----------------------------------------------------------------------------------------")
    print("To reset points write 'reset'")
    option = input("\nWhat do you want to do? Write back to go back.\n").lower()
    if option == "back":
        game()
    elif option == "strength" or option == "agility" or option == "vitality" or option == "energy":
        try:
            option1 = int(input("How many points you want to add ?"))
            if option1 <= playerig.points:
                playerig.points -= option1
                adder(option, option1)
            else:
                input("Not enough available points")
                character()
        except:
                input("Invalid amount of points")
                character()
    elif option == "reset":
        resetPoints()
    else:
        input("Invalid choice")
        character()


def skills():
    print(f"--Twisting-- Damage: {items.Twisting.damage}, Mana cost: {items.Twisting.manacost}, Description: {items.Twisting.description}, Level: {items.Twisting.lvl}")
    print(f"--Ice Storm-- Damage: {items.Ice_Storm.damage}, Mana cost: {items.Ice_Storm.manacost}, Description: {items.Ice_Storm.description}, Level: {items.Ice_Storm.lvl}")
    print(f"--Regenerate-- Damage: {items.Regenerate.damage}, Mana cost: {items.Regenerate.manacost}, Description: {items.Regenerate.description}, Level: {items.Regenerate.lvl}")


def skillupgrade():
    clear()
    skills()
    try:
        print(f"\nAvailable attribute points: {playerig.attributepoints}")
        option = input("\nWhat skill do you wanna upgrade? Or write back to go back\n").lower()
        if option == "twisting" and playerig.attributepoints > 0:
            items.Twisting.lvl += 1
            playerig.attributepoints -= 1
            items.Twisting.damage += 3
            items.Twisting.manacost += 3
            input(f"--Twisting-- Damage: {items.Twisting.damage}, Mana cost: {items.Twisting.manacost}, Description: {items.Twisting.description}, Level: {items.Twisting.lvl}")
            clear()
            skills()
        elif option == "ice storm" and playerig.attributepoints > 0:
            items.Ice_Storm.lvl += 1
            playerig.attributepoints -= 1
            items.Ice_Storm.damage += 4
            items.Ice_Storm.manacost += 8
            input(f"--Ice Storm-- Damage: {items.Ice_Storm.damage}, Mana cost: {items.Ice_Storm.manacost}, Description: {items.Ice_Storm.description}, Level: {items.Ice_Storm.lvl}")
            clear()
            skills()
        elif option == "Regenerate" and playerig.attributepoints > 0:
            items.Regenerate.lvl += 1
            playerig.attributepoints -= 1
            items.Regenerate.damage += 10
            items.Regenerate.manacost += 10
            input(f"--Regenerate-- Damage: {items.Regenerate.damage}, Mana cost: {items.Regenerate.manacost}, Description: {items.Regenerate.description}, Level: {items.Regenerate.lvl}")
            clear()
            skills()
        elif option == "back":
            game()
        else:
            input("Invalid command\n")
            skillupgrade()
    except:
        input("No skill with this name")
        skillupgrade()


def useskill(option):
    if option == "twisting":
        playerig.mp -= items.Twisting.manacost
        return items.Twisting.damage
    elif option == "ice storm":
        playerig.mp -= items.Ice_Storm.manacost
        return items.Ice_Storm.damage
    elif option == "regenerate":
        curhp = playerig.maxhp - playerig.hp
        playerig.mp -= items.Regenerate.manacost
        playerig.hp += items.Regenerate.damage
        if playerig.hp >= playerig.maxhp:
            playerig.hp = playerig.maxhp
            return curhp
        else:
            return items.Regenerate.damage
    else:
        input("Chose a valid skill")


def adder(option, option1):
    if option == "strength":
        playerig.str += option1
        input(f"You added {option1} points to {option}")
    elif option == "agility":
        playerig.agy += option1
        input(f"You added {option1} points to {option}")
    elif option == "stamina":
        playerig.vit += option1
        input(f"You added {option1} points to {option}")
    elif option == "energy":
        playerig.ene += option1
        input(f"You added {option1} points to {option}")
    playerig.update()
    character()


def reward(monster):
    print(f"\n{monster.name} died , you earn {monster.givegold} gold")
    playerig.gold += monster.givegold
    playerig.exp += monster.giveexp
    monster.hp = monster.maxhp
    if playerig.exp >= playerig.exptolvl:
        playerig.lvl += 1
        playerig.points += 5
        playerig.exptolvl = playerig.lvl * 2.5
        print(f"\nYou leveled up, you are level: {playerig.lvl}")
        print(f"You gained 5 points")
        print(f"You gained 2 attribute points\n")
    if monster.lvl == 1 or monster.lvl == 2:
        luck = random.randint(1, 2)
        if luck == 1:
            choice = random.choice(items.rewardbox)
            playerig.inventory.append(choice)
            print(f"You found {choice.name}")
    elif monster.lvl == 3 or monster.lvl == 4:
        luck = random.randint(1, 2)
        if luck == 1:
            choice = random.choice(items.rewardbox2)
            playerig.inventory.append(choice)
            print(f"You found {choice.name}")
    elif monster.lvl == 5 or monster.lvl == 6:
        luck = random.randint(1, 2)
        if luck == 1:
            choice = random.choice(items.rewardbox3)
            playerig.inventory.append(choice)
            print(f"You found {choice.name}")
    elif monster.lvl == 6:
        luck = random.randint(1, 2)
        if luck == 1:
            choice = random.choice(items.rewardbox4)
            playerig.inventory.append(choice)
            print(f"You found {choice.name}")
    elif monster.lvl == 7:
        luck = random.randint(1, 2)
        if luck == 1:
            choice = random.choice(items.rewardbox5)
            playerig.inventory.append(choice)
            print(f"You found {choice.name}")
    else:
        print(f"{monster.name} didn't dropped anything")
    input("\nPress enter to continue")
    explore(mappa)

def harald():
    clear()
    print("Inside the Inventory you have: ")
    c = 1
    for i in playerig.inventory:
        print(f"{c}.{i.name}")
        c += 1
    print(f"\nGold: {playerig.gold}")
    c = 1
    option = input("\nWrite back to go back else write sell to sell a item or recharge to heal/mana\n").lower()
    if option == "back":
        game()
    elif option == "sell":
        sellItem()
    elif option == "recharge":
        recharge()
    else:
        harald()


def recharge():
    clear()
    option = input("What do you want to recharge ? Health or Mana? back to go back\n").lower()
    if option == "health":
        if playerig.gold > 10:
            playerig.gold -= 10
            playerig.hp += playerig.maxhp
            if playerig.hp > playerig.maxhp:
                playerig.hp = playerig.maxhp
            input("Health refiled")
            game()
        else:
            input("Insufficient gold")
            game()
    elif option == "mana":
        if playerig.gold > 10:
            playerig.gold -= 10
            playerig.mp += playerig.maxmp
            if playerig.mp > playerig.maxmp:
                playerig.mp = playerig.maxmp
            input("Mana refiled")
            game()
        else:
            input("Insufficient gold")
            game()
    elif option == "back":
        game()
    else:
        input("Invalid command")
        game()

def sellItem():
    try:
        item = int(input("Write the number of the item you want to sell\n"))
        item -= 1
        if item in range(len(playerig.inventory)):
            playerig.gold += playerig.inventory[item].price
            print(f"You sold the item {playerig.inventory[item].name}")
            playerig.inventory.pop(item)
            input("Press enter to continue")
            harald()
    except:
        input("Please chose a number from the inventory.")
        inventory()


def dead():
    playerig.hp = playerig.maxhp
    playerig.mp = playerig.maxmp
    print("You died")
    print("...")
    input("Press enter enter to continue")
    game()

def resetPoints():
    option = input("Are you sure you want to reset all your points ? Yes or No?\n").lower()
    if option == "yes":
        if playerig.gold >= 100:
            playerig.gold -= 100
            playerig.points += (playerig.str + playerig.agy + playerig.vit + playerig.ene)-4
            playerig.str -= (playerig.str - 1)
            playerig.agy -= (playerig.agy - 1)
            playerig.vit -= (playerig.vit - 1)
            playerig.ene -= (playerig.ene - 1)
            input("You reset your points")
            clear()
            character()
        else:
            input("Insufficient gold, you need 100 gold")
            game()
    elif option == "no":
        input("You didn't reset your points")
        game()
    else:
        input("Please enter a valid command")
        resetPoints()


def savegame():
    with open("savegame.b", "wb") as save:
        pickle.dump(playerig, save)
        input(f"You saved your current status")
    game()


def loadgame():
    try:
        with open("savegame.b", "rb") as save:
            playerig = pickle.load(save)
            load(playerig)
    except:
        input("There is no saved game in the directory")
        main()


def clear():
    os.system('cls')


main()

items.py

import random
empty = [["Empty", 0, 0]]
primarys = [["Wood Sword", 2, 3, "Weapon"], ["Great Sword", 12, 9, "Weapon"], ["Exe Sword", 22, 14, "Weapon"], ["Dragon Sword", 45, 23, "Weapon"], ["Dragon Knight Sword", 50, 30, "Weapon"]]
secondarys = [["Wood Shield", 2, 3, "Shield"], ["Great Shield", 13, 11, "Shield"], ["Exe Shield", 23, 15, "Shield"], ["Dragon Shield", 50, 21, "Shield"], ["Dragon Knight Shield", 53, 28, "Shield"]]
armors = [["Wood Armor", 2, 3, "Armor"], ["Great Armor", 9, 6, "Armor"], ["Exe Armor", 21, 10, "Armor"], ["Dragon Armor", 48, 14, "Armor"], ["Dragon Knight Armor", 54, 22, "Armor"]]


skills = [["Twisting", 25, 10, "Turn around in a circular motion and deal damage all around you", "Attack", 1], ["Regenerate", 25, 10, "You regenerate some health", "Regen", 1], ["Ice Storm", 25, 10, "You start a small Ice Storm", "Attack", 1]]
monsters = [["Spider", 1, 30, 8, 13, 15], ["Huge Spider", 2, 45, 10, 15, 20], ["Baby Dragon", 2, 50, 12, 15, 20], ["Goblin", 3, 45, 15, 19, 25], ["Stone Goblin", 4, 55, 20, 26, 27], ["Cyclop", 5, 70, 30, 36, 33], ["Great Dragon", 6, 100, 40, 43, 40], ["Golden Dragon", 7, 200, 50, 100, 65]]


class Items:
    def __init__(self, name, price, attack, type):
        self.name = name
        self.price = price
        self.attack = attack
        self.type = type


class Skill:
    def __init__(self, name, damage, manacost, description, type, lvl):
        self.name = name
        self.damage = damage
        self.manacost = manacost
        self.description = description
        self.type = type
        self.lvl = lvl


class Monster:
    def __init__(self, name, lvl, maxhp, minattack, maxattack, defense):
        self.name = name
        self.lvl = lvl
        self.maxhp = maxhp
        self.hp = self.maxhp
        self.minattack = minattack
        self.maxattack = maxattack
        self.defense = defense
        self.givegold = random.randint(1, 4) * self.lvl
        self.giveexp = lvl * 1.5


Wood_Sword = Items(primarys[0][0], primarys[0][1], primarys[0][2], primarys[0][3])
Great_Sword = Items(primarys[1][0], primarys[1][1], primarys[1][2], primarys[1][3])
Exe_Sword = Items(primarys[2][0], primarys[2][1], primarys[2][2], primarys[2][3])
Dragon_Sword = Items(primarys[3][0], primarys[3][1], primarys[3][2], primarys[3][3])
Dragon_Knight_Sword = Items(primarys[4][0], primarys[4][1], primarys[4][2], primarys[4][3])

Wood_Shield = Items(secondarys[0][0], secondarys[0][1], secondarys[0][2], secondarys[0][3])
Great_Shield = Items(secondarys[1][0], secondarys[1][1], secondarys[1][2], secondarys[1][3])
Exe_Shield = Items(secondarys[2][0], secondarys[2][1], secondarys[2][2], secondarys[2][3])
Dragon_Shield = Items(secondarys[3][0], secondarys[3][1], secondarys[3][2], secondarys[3][3])
Dragon_Knight_Shield = Items(secondarys[4][0], secondarys[4][1], secondarys[4][2], secondarys[4][3])

Wood_Armor = Items(armors[0][0], armors[0][1], armors[0][2], armors[0][3])
Great_Armor = Items(armors[1][0], armors[1][1], armors[1][2], armors[1][3])
Exe_Armor = Items(armors[2][0], armors[2][1], armors[2][2], armors[2][3])
Dragon_Armor = Items(armors[3][0], armors[3][1], armors[3][2], armors[3][3])
Dragon_Knight_Armor = Items(armors[4][0], armors[4][1], armors[4][2], armors[4][3])

rewardbox = [Wood_Sword, Great_Sword, Wood_Shield, Great_Shield,  Wood_Armor, Great_Armor]
rewardbox2 = [Great_Sword, Exe_Sword, Great_Shield, Exe_Shield, Great_Armor, Exe_Armor]
rewardbox3 = [Exe_Sword, Dragon_Sword,Exe_Shield, Dragon_Shield,Exe_Armor, Dragon_Armor]
rewardbox4 = [Dragon_Sword, Dragon_Knight_Sword, Dragon_Shield, Dragon_Knight_Shield, Dragon_Armor, Dragon_Knight_Armor]
rewardbox5 = [Dragon_Knight_Sword, Dragon_Knight_Shield, Dragon_Knight_Armor]

Twisting = Skill(skills[0][0], skills[0][1], skills[0][2], skills[0][3], skills[0][4], skills[0][5])
Regenerate = Skill(skills[1][0], skills[1][1], skills[1][2], skills[1][3], skills[1][4], skills[0][5])
Ice_Storm = Skill(skills[2][0], skills[2][1], skills[2][2], skills[2][3], skills[2][4], skills[0][5])


Spider = Monster(monsters[0][0],monsters[0][1],monsters[0][2],monsters[0][3],monsters[0][4],monsters[0][5])
Huge_Spider = Monster(monsters[1][0],monsters[1][1],monsters[1][2],monsters[1][3],monsters[1][4],monsters[1][5])
Baby_Dragon = Monster(monsters[2][0],monsters[2][1],monsters[2][2],monsters[2][3],monsters[2][4],monsters[2][5])
Goblin = Monster(monsters[3][0],monsters[3][1],monsters[3][2],monsters[3][3],monsters[3][4],monsters[3][5])
Stone_Goblin = Monster(monsters[4][0],monsters[4][1],monsters[4][2],monsters[4][3],monsters[4][4],monsters[0][5])
Cyclop = Monster(monsters[5][0],monsters[5][1],monsters[5][2],monsters[5][3],monsters[5][4],monsters[5][5])
Great_Dragon = Monster(monsters[6][0],monsters[6][1],monsters[6][2],monsters[6][3],monsters[6][4],monsters[6][5])
Golden_Dragon = Monster(monsters[7][0],monsters[7][1],monsters[7][2],monsters[7][3],monsters[7][4],monsters[7][5])

maps.py

maps = [["Lorencia", "Lorencia is 1 of the oldest map of the ancient", True, True, [["Lorencia's safe zone"], ["Lorencia Woods"], ["Lorencia Sanctuary"], ["Lorencia Dungeon"], ["Lorencia Boss"]]]]


class Map:
    def __init__(self, name, desc, loot, fight, zone):
        self.name = name
        self.desc = desc
        self.loot = loot
        self.fight = fight
        self.zone = zone
        self.curzone = self.zone[0]
        self.count = 0
        self.walking = 0


mappa = Map(maps[0][0], maps[0][1], maps[0][2], maps[0][3], maps[0][4])
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for posting everything, this looks like a great subject for review \$\endgroup\$ Commented Oct 12, 2021 at 21:55

3 Answers 3

14
\$\begingroup\$

I assume this your first project of such size, and if so, that's a great work. It's really cool you've done all this.

Bad Recursion

This is the worst problem in your code. You see, calling the function is not free, it uses space in a call stack. The space is released on function return or program exit; but because you never return from functions like game(), harald(), skillupgrade() etc., it will consume more and more space. It will be real hard to make the program run out of stack space manually, but this is considered a very bad pattern in programming. Try rethinking the program using loops.

Repeating in ifs

Consider this code:

        if option1 == "twisting" or option1 == "ice storm":
            monster.hp -= useskill(option1)
            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook
            input(f"You gave {useskill(option1)} damage and take {damagetook} damage")
            fight(monster)
        elif option1 == "regenerate":
            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook
            input(f"You took {damagetook} damage and healed for {useskill(option1)} hp")
            fight(monster)
        else:
            input("Enter a valid skill name\n")
            fight(monster)

In any situation, the last line executed will be fight(monster). So you can just move it out of if-else:

        if option1 == "twisting" or option1 == "ice storm":
            monster.hp -= useskill(option1)
            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook
            input(f"You gave {useskill(option1)} damage and take {damagetook} damage")
        elif option1 == "regenerate":
            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook
            input(f"You took {damagetook} damage and healed for {useskill(option1)} hp")
        else:
            input("Enter a valid skill name\n")
        fight(monster)

There are several more places where you can use this technique.

Use more functions for repeating code

Lines

            damagetook = random.randint(monster.minattack, monster.maxattack)
            playerig.hp -= damagetook

are repeated many times in the code. You can move them into Monster method and write just something like

damagetook = monster.attack(playerig)

with

class Monster:
    ...
    def attack(self, player):
        damage = random.randint(self.minattack, self.maxattack)
        player.hp -= damage
        return damage

This will make the code much more compact and readable.

Global variables

Global variables are considered bad. You have only one such variable - playerig; but still you can gather all the functions using it in one class (like Scene or Game), make playerig a member of that class and create only one object of Scene in main() function.

Overindexing

Wood_Sword = Items(primarys[0][0], primarys[0][1], primarys[0][2], primarys[0][3])

can be shortened into

Wood_Sword = Items(*primarys[0])

* means unpack a list into arguments. You can also rewrite __init__ method to accept a list.

Hardcoding

Usually it's a good idea to separate data from the code. You can keep all the data (monsters, items, maps) in config files without need to edit code every time you want to change balance in the game. I'm not sure if you need this right now; probably use it as idea for next version.

Dataclasses

Probably you could use them here. Check out documentation.

I think that would be enough for a beginner. Keep your good work!


UPDATE

Use methods to access data

You have calculated data members (attack, defense), which are changed in update method. This is bad because you can forget to call update. Better way is to remove data members and use methods to get those values:

def get_attack(self):
    return self.str + self.primary[0].attack + self.agy

This way, you'll never forget to update them. Also, it's a good habit to access all data through methods (getters like get_attack to get data, setters like set_attack to set it), so you can always change the formulas like changing stats with items.


\$\endgroup\$
2
  • \$\begingroup\$ That is exactly what i wanted , thank you very much for your time! \$\endgroup\$
    – Niklaus
    Commented Oct 5, 2021 at 18:07
  • \$\begingroup\$ This is a great review, it's very skill-appropriate. Introduces a bunch of new ideas and style points the author will be able to use immediately, and explains why. And tries to focus on general tips instead of specifics, which I think is good for something this long. \$\endgroup\$ Commented Oct 12, 2021 at 21:57
5
\$\begingroup\$

Having programmed many similar games, I'll focus on one particular aspect pertaining to game development: the data structure that you use in items.py to represent items, skills, and monsters.

Right now, you use unstructured lists of lists for as representations:

empty = [["Empty", 0, 0]]
primarys = [["Wood Sword", 2, 3, "Weapon"], ["Great Sword", 12, 9, "Weapon"], ["Exe Sword", 22, 14, "Weapon"], ["Dragon Sword", 45, 23, "Weapon"], ["Dragon Knight Sword", 50, 30, "Weapon"]]
secondarys = [["Wood Shield", 2, 3, "Shield"], ["Great Shield", 13, 11, "Shield"], ["Exe Shield", 23, 15, "Shield"], ["Dragon Shield", 50, 21, "Shield"], ["Dragon Knight Shield", 53, 28, "Shield"]]
armors = [["Wood Armor", 2, 3, "Armor"], ["Great Armor", 9, 6, "Armor"], ["Exe Armor", 21, 10, "Armor"], ["Dragon Armor", 48, 14, "Armor"], ["Dragon Knight Armor", 54, 22, "Armor"]]

skills = [["Twisting", 25, 10, "Turn around in a circular motion and deal damage all around you", "Attack", 1], ["Regenerate", 25, 10, "You regenerate some health", "Regen", 1], ["Ice Storm", 25, 10, "You start a small Ice Storm", "Attack", 1]]
monsters = [["Spider", 1, 30, 8, 13, 15], ["Huge Spider", 2, 45, 10, 15, 20], ["Baby Dragon", 2, 50, 12, 15, 20], ["Goblin", 3, 45, 15, 19, 25], ["Stone Goblin", 4, 55, 20, 26, 27], ["Cyclop", 5, 70, 30, 36, 33], ["Great Dragon", 6, 100, 40, 43, 40], ["Golden Dragon", 7, 200, 50, 100, 65]]

This has several disadvantages. Perhaps the most obvious one is that you have to be very careful when adding new things or removing obsolete things. The list index may change, and you'll have to keep track of every place in your code where the index is used. For example, if you want to add a new goblin type, you'll probably want to define it next to the existing goblins, but that will change the index of the Cyclops (by the way, in English that creature name ends in s even in the singular) and all subsequent creatures. It's easy to introduce a bug here so that every Cyclops is now a Great Dragon. I'm going to focus below on monsters, but what I write applies similarly to items and skills, and it could also be adopted to levels.

Instead of using these lists of lists, I'd suggest to define an Enum that represents the different monster types, and then use this Enum as keys to a dictionary with the values representing the monster data. Perhaps like so:

from enum import Enum

class MonsterData(Enum):
    SPIDER          = ["Spider", 1, 30, 8, 13, 15]
    HUGE_SPIDER     = ["Huge Spider", 2, 45, 10, 15, 20]
    GOBLIN          = ["Goblin", 3, 45, 15, 19, 25]
    STONE_GOBLIN    = ["Stone Goblin", 4, 55, 20, 26, 27]
    CYCLOPS         = ["Cyclops", 5, 70, 30, 36, 33]
    BABY_DRAGON     = ["Baby Dragon", 2, 50, 12, 15, 20]
    GREAT_DRAGON    = ["Great Dragon", 6, 100, 40, 43, 40]
    GOLDEN_DRAGON   = ["Golden Dragon", 7, 200, 50, 100, 65]

The next change that I'd make would be to pass one of the members from the MonsterData as the sole argument of Monster.__init__(). The method will look up the right numbers from the values of the enum, like so:

class Monster:
    def __init__(self, monster_type):
        base_data = MonsterData(monster_type).value
        self.name = base_data[0]
        self.lvl = base_data[1]
        self.maxhp = base_data[2]
        self.minattack = base_data[3]
        self.maxattack = base_data[4]
        self.defense = base_data[5]

        self.hp = self.maxhp
        self.givegold = random.randint(1, 4) * self.lvl
        self.giveexp = lvl * 1.5

If you want to create a great dragon (or in technical terms, an instance of the Monster class that represents the enum member GREAT_DRAGON) you can do it like so:

Great_Dragon = Monster(MonsterData.GREAT_DRAGON)

instead of the following line from your code:

Great_Dragon = Monster(monsters[6][0],monsters[6][1],monsters[6][2],monsters[6][3],monsters[6][4],monsters[6][5])

Monster.__init__() still uses magic index numbers to represent the different monster properties, though. What happens if you want to add a new property, e.g. armor class? This property will be added to the list, and just like with adding a new monster, you'll have to make sure that you update every reference to the list index accordingly. Again, this is something that can introduce nasty errors. For example, it's easy to end up in a situation where the maxhp property becomes th minattack property if you're sloppy. In order to avoid this, you could create a NamedTuple class for each monster data entry, like so:

from typing import NamedTuple

class SpiderData(NamedTuple):
    name = "Spider"
    lvl = 1
    maxhp = 30
    minattack = 8
    maxattack = 13
    defense = 15

class HugeSpiderData(NamedTuple):
    name = "Huge Spider"
    lvl = 2
    maxhp = 45
    minattack = 10
    maxattack = 15
    defense = 20

... 

This will allow you to access the value of the tuples by the associated names, thus avoiding using list indices. Assuming that you create similar named tuples for each monster type, the enum MonsterData would then look like this:

class MonsterData(Enum):
    SPIDER          = SpiderData()
    HUGE_SPIDER     = HugeSpiderData()
    GOBLIN          = GoblinData()
    STONE_GOBLIN    = StoneGoblinData()
    CYCLOPS         = CyclopsData()
    BABY_DRAGON     = BabyDragpmData()
    GREAT_DRAGON    = GreatDragonData()
    GOLDEN_DRAGON   = GoldenDragonData()

And you'd also incorporate this change into Monster.__init__:

class Monster:
    def __init__(self, monster_type):
        base_data = MonsterData(monster_type).value
        self.name = base_data.name
        self.lvl = base_data.lvl
        self.maxhp = base_data.maxhp
        self.minattack = base_data.minattack
        self.maxattack = base_data.maxattack
        self.defense = base_data.defense

        self.hp = self.maxhp
        self.givegold = random.randint(1, 4) * self.lvl
        self.giveexp = lvl * 1.5

This way you have a data structure that is very robust against bugs that may creep up if you expand either what monsters can do or which monsters exist. But this security comes at a price: there is much more code overhead. Instead of a single list in which each list element represents a list of monster properties, you create a NamedTuple class for each monster, and add these classes explicitly to an enum. This avoids depending on magic index numbers, but it's adding a level of design complexity.

When @pavlo-slavynskyy talks about separating data from code in their Hardcoding section, they're explicitly advising against restructuring the code like this. If Python was a compiled programming language, I'd agree more wholeheartedly. But given that your external config files would be structured anyway (e.g. JSON or XML), the advantage to editing the Python code directly diminishes. For example, a monster config file in JSON might look like this:

{"SpiderData": {
    "name": "Spider",
    "lvl": 1,
    "maxhp": 30,
    "minattack": 8,
    "maxattack": 13,
    "defense": 15},
 "HugeSpiderData": {
    "name": "Huge Spider",
    "lvl": 2,
    "maxhp": 45,
    "minattack": 10,
    "maxattack": 15,
    "defense": 20}
}

This is so close to creating named tuples for each monster type in Python that personally, I wouldn't really bother. But your mileage may vary.

However, there's a huge bug in your program anyway. As it is, you create the instance Great_Dragon only once in item.py. You use this instance in explore() from fight.py when you create the different lists of monsters that you encounter on each map. So, Great_Dragon occurs once in monsterlist3, and three times in monsterlist4. The problem is that all these entries in the monster list all refer to the same instance – the same representation of the same great dragon in your game world. Once the hit points of this instance are changed, they will be changed for all future encounters of this instance. In other words, once the player defeats the Great Dragon, all future encounters with the first Great Dragon will end in an instant win because it has been already defeated. And the player will receive exactly the same amount of gold during each encounter.

To fix this bug, you need to change your monster lists. Instead of e.g.

monsterlist3 = [items.Great_Dragon, items.Cyclop]
monsterlist4 = [items.Great_Dragon, items.Great_Dragon, items.Great_Dragon, items.Golden_Dragon]
        

you should have the following (make sure that you import Monster and MonsterData from the items module so that you don't need to replicate the items. prefix):

monsterlist3 = [Monster(MonsterData.GREAT_DRAGON), 
                Monster(MonsterData.CYCLOPS)]
monsterlist4 = [Monster(MonsterData.GREAT_DRAGON), 
                Monster(MonsterData.GREAT_DRAGON), 
                Monster(MonsterData.GREAT_DRAGON), 
                Monster(MonsterData.GOLDEN_DRAGON)]

The lines in items.py in which you create the instances of the monsters would be obsolete now.

To sum up: Currently, you use lists of lists to represent items, monsters, and skills. This data structure has the disadvantage of depending on magic index numbers. The result is intransparent code (you probably won't remember if I ask you in six months what monsters[7][2] corresponds to), and if you expand your game in the future, modifying these lists can easily introduce bugs. Instead, you should use data structures that allow you to access the elements you're interested in by name. Enum is one of these data structures, and NamedTuple is another.

Your code also contains a bug in which every future encounter with a monster of the same type will result in a creature that is dead on arrival.

\$\endgroup\$
1
  • \$\begingroup\$ Just amazing, thank you very much for all the details \$\endgroup\$
    – Niklaus
    Commented Oct 12, 2021 at 21:23
2
\$\begingroup\$

This is a lot of code. It's good that you've shared it all, but I'll probably have to stop short of offering a complete refactor.

  • You should not be storing derived values as variables; this risks the source and destination value getting out of sync. Instead of
self.vit = 1
self.maxhp = 100 + (self.vit * 2)

you'll want something like

self.vit = 1
# ...

@property
def max_hp(self) -> int:
    return 100 + 2*self.vit

Related to the above, don't have an update at all; just have @property functions that return those derived values calculated on the fly.

  • Decorate your function signatures with type hints; for instance def __init__(self, name: str) -> None.

  • Why is primary a list? Can the player hold an arbitrary number of weapons at one time? If not, then this should just be a single reference to an item.

  • Due to operator precedence, self.agy + (self.armor[0].attack/2) does not need parens

  • magicattack should be magic_attack by PEP8, and similar for other variables and function names

  • You have a non-portable implementation of clear. It is fairly easy to make this program portable to other operating systems so long as you either

    • don't clear at all;
    • write your own OS switching logic to call the appropriate clear routine; or
    • call into a third-party portable Python library that does TUI graphics for you.
  • Consider capturing this comparison chain:

    if option == "credits":
        input("Just you")
        game()
    elif option == "skills":
        skillupgrade()
    elif option == "harald":
        harald()
    elif option == "inventory":
        inventory()
    elif option == "explore":
        explore(mappa)
    elif option == "character":
        character()
    elif option == "exit":
        input("Cyaaa\n")
        os.system("exit")
    elif option == "save":
        savegame()
    else:
        game()

in a dictionary of function references.

  • Don't if map.loot == True; just if map.loot
  • Repeated comparisons such as
monster.lvl == 3 or monster.lvl == 4

can be abbreviated to

monster.lvl in {3, 4}

or probably better, since this comparison should support continuous values,

3 <= monster.lvl <= 4
  • If harald is a person it should be Harold; or maybe you meant herald.
  • Are you sure that this:
playerig.str -= (playerig.str - 1)

is what you meant? This is going to always set the strength to 1. More likely you meant

playerig.str = playerig.str - 1

which is equivalent to

playerig.str -= 1
  • game should definitely not be called from savegame.
  • You need an if __name__ == '__main__': before your main().
  • Your map initializer needs some work. The outer list is not needed, and the inner list is a fragile and difficult-to-read way to initialize a class. Instead,
mappa = Map(
    name='Lorencia',
    desc='Lorencia is 1 of the oldest map of the ancient',
    loot=True,
    fight=True,
    zones=[
        "Lorencia's safe zone",
        "Lorencia Woods", 
        "Lorencia Sanctuary", 
        "Lorencia Dungeon",
        "Lorencia Boss"
    ],
)

Note that zones should be a list of strings and not a list of lists of strings.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you very much for the tips. \$\endgroup\$
    – Niklaus
    Commented Oct 7, 2021 at 17:01
  • \$\begingroup\$ Just wanted to explain downvote. I think this is a great answer but some parts (ex. dictionary of function references) are clearly not aimed at the right skill level, and things like spelling are not the highest-impact. So overall I think the other answers focusing on basics are a little nicer, even though this is good too. \$\endgroup\$ Commented Oct 12, 2021 at 21:53
  • \$\begingroup\$ @ZacharyVance thank you for the explanation. For what it's worth, my review philosophy is that 1. There's no issue too small to mention, and 2. Treat posters like adults. I must assume that their goal is to get to a professional level of coding, and so the feedback should be concomitant. I'm happy to offer more explanation of these concepts to beginners, but I don't think they should be withheld. \$\endgroup\$
    – Reinderien
    Commented Oct 13, 2021 at 2:27

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.