10
\$\begingroup\$

Start Program

Comments

In Linux you can install a program called fortune. This program gives you a random wisdom each time you execute it. I thought about making a game, where you have to do something before you can read some wise words.

This is "the biggest" program I have written so far. I tried to practise object oriented programming by developing an idea and model the individual structures by using OOP-concepts. So I would like to read comments how to improve the structure of the code.

This game also was written by me in a simplified form in Java and in C, but now I also wanted to try Python. So if you can tell me where there is a better way, a "Python way to do something" for elements of my program, i would be thankful.

I tested the program until I have not found bugs anymore. I hope you will also don't find any bugs.

Game Description

You have to solve mathmatical exercises. You gain Experience points, rank points and gold. You can buy attribute upgrades for speed up the reward gaining and unlock new wisdoms from wise persons.

#!/usr/bin/env python3

from pathlib import Path
from random import randint
import os
import time

# this code is divided into the following parts:
#   helpful functions
#   the actual parts of the program (classes, functions)
#   the starting part

### helpful functions

def saveIntInput(string):
    try:
        number = int(input(string))
        return number
    except ValueError:
        print("Please enter a number.")
        return 0
        
### parts of the program

class Player:
    rankString = ["Pupil", "Teacher's Favorite", "Freshman", "Junior", "Graduate", "College Student", 
                  "Bachelor of Science", "Master of Science", "Doctor of Mathematics", "Professor", 
                  "Researcher", "Nobel Laureate", "Superman", "Chuck Norris", "Better than Chuck Norris", 
                  "Nearly the best rank", "Very good", "Very very good", "Nearly the best", "The Best", 
                  "A true Mathematician"]
    
    def __init__(self):
        self.save = Path(".save/player")
        if self.save.is_file():
            self.save = open(".save/player", "r")
            self.save.readline() # skip version detection (maybe needed later)
            
            self.level = int(self.save.readline())
            self.experience = int(self.save.readline())
            self.experienceNeeded = int(self.save.readline())
            
            self.rank = int(self.save.readline())
            self.rankPoints = int(self.save.readline())
            self.rankPointsNeeded = int(self.save.readline())
            
            self.attributePoints = int(self.save.readline())
            self.gold = int(self.save.readline())
            
            # attributes
            self.luck = int(self.save.readline()) # improves gold gaining
            self.knowledge = int(self.save.readline()) # improves experience gaining
            self.wisdom = int(self.save.readline()) # improves wisdom gaining

            self.save.close()
        else:
            self.level = 1
            self.experience = 0
            self.experienceNeeded = 10
            
            self.rank = 1
            self.rankPoints = 0
            self.rankPointsNeeded = 10
            
            self.attributePoints = 0
            self.gold = 0
            
            # attributes
            self.luck = 1
            self.knowledge = 1
            self.wisdom = 1
            
        self.trainer = Trainer(self)
        self.wiseWords = WiseWords(self)
            
            
    def __str__(self):
        string =  "Level            : " + str(self.level) + "\n"
        string += "Experience       : " + str(self.experience) + " / " + str(self.experienceNeeded) + "\n\n"
        
        if self.rank < 20:
            string += "Rank             : " + self.rankString[self.rank - 1] + "\n"
        else:
            string += "Rank             : " + self.rankString[19] + "\n"
        string += "Rank points      : " + str(self.rankPoints) + " / " + str(self.rankPointsNeeded) + "\n\n"
        
        string += "Gold:            : " + str(self.gold) + "\n"
        string += "Attribute Points : " + str(self.attributePoints) + "\n\n"
        
        string += "Luck Level       : " + str(self.luck) + "\n"
        string += "Knowledge Level  : " + str(self.knowledge) + "\n"
        string += "Wisdom Level     : " + str(self.wisdom)
        return string
        
    def saveToFile(self):
        self.save = open(".save/player", "w")
        self.save.write("v1\n") # version of program
        # is needed maybe later to convert save files from old versions to new version,
        # if old version is detected
        self.save.write(str(self.level) + "\n")
        self.save.write(str(self.experience) + "\n")
        self.save.write(str(self.experienceNeeded) + "\n")
        
        self.save.write(str(self.rank) + "\n")
        self.save.write(str(self.rankPoints) + "\n")
        self.save.write(str(self.rankPointsNeeded) + "\n")
        
        self.save.write(str(self.attributePoints) + "\n")
        self.save.write(str(self.gold) + "\n")
        
        self.save.write(str(self.luck) + "\n")
        self.save.write(str(self.knowledge) + "\n")
        self.save.write(str(self.wisdom) + "\n")
        self.save.close()
        
    def reward(self, value):
        if value:
            goldReward = int((1 + (0.1 * self.rank)) * self.luck)
            experienceReward = int((1 + (0.1 * self.rank)) * self.knowledge)
            rankPointsReward = int((1 + (0.1 * self.rank)) * self.wisdom)
            
            self.gold += goldReward
            self.experience += experienceReward
            self.rankPoints += rankPointsReward
            
            print("Gold +", goldReward)
            print("Exp  +", experienceReward)
            print("RP   +", rankPointsReward)
            self.increaseLevel()
            self.increaseRank()
            self.saveToFile()
        else:
            print("You get no reward")
    
    def increaseLevel(self):
        while(self.experience >= self.experienceNeeded):
            self.level += 1
            self.attributePoints += 2
            print("\nLevel Up!\n")
            time.sleep(1)
            self.experience -= self.experienceNeeded
            self.experienceNeeded = 5 * self.level ** 2 + 5 * self.level
            self.wiseWords.increaseLimit(2)
            
    def increaseRank(self):
        while(self.rankPoints >= self.rankPointsNeeded):
            self.rank += 1
            print("\nYou have a new Rank!")
            self.rankPoints -= self.rankPointsNeeded
            self.rankPointsNeeded *= 2  
            time.sleep(1)
            
class Trainer:
    def __init__(self, player):
        self.save = Path(".save/trainer")
        
        if self.save.is_file():
            self.save = open(".save/trainer", "r")
            self.luckExpense = int(self.save.readline())
            self.knowledgeExpense = int(self.save.readline())
            self.wisdomExpense = int(self.save.readline())
            self.luckGoldLevel = int(self.save.readline())
            self.knowledgeGoldLevel = int(self.save.readline())
            self.wisdomGoldLevel = int(self.save.readline())
            self.save.close()
        else:
            self.luckExpense = 10 # refers to gold
            self.knowledgeExpense = 10
            self.wisdomExpense = 10
            # neccessary for calculating new price
            self.luckGoldLevel = 1 
            self.knowledgeGoldLevel = 1
            self.wisdomGoldLevel = 1
        
        self.player = player
        
    def saveToFile(self):
        self.save = open(".save/trainer", "w")
        self.save.write(str(self.luckExpense) + "\n")
        self.save.write(str(self.knowledgeExpense) + "\n")
        self.save.write(str(self.wisdomExpense) + "\n")
        self.save.write(str(self.luckGoldLevel) + "\n")
        self.save.write(str(self.knowledgeGoldLevel) + "\n")
        self.save.write(str(self.wisdomGoldLevel) + "\n")
        self.save.close()
        
    def train(self): 
        print("Gold has to be payed only if there are no attribute points remained")
        print("[0] Leave")
        while True:
            print("Gold:", self.player.gold, "  AP:", self.player.attributePoints)
            print("[1] Luck (costs", self.luckExpense, "Gold)")
            print("[2] Knowledge (costs", self.knowledgeExpense, "Gold)")
            print("[3] Wisdom (costs", self.wisdomExpense, "Gold)")
            command = input("> ")
            print()
            
            # following code looks redundant, is there a good way to change that without reduce readability?
            
            if command == "1":
                if self.player.attributePoints > 0:
                    self.player.luck += 1
                    print("Luck increased!")
                    self.player.attributePoints -= 1
                    print("One attribute point was used.")
                elif self.player.gold >= self.luckExpense:
                    self.player.luck += 1
                    print("Luck increased!")
                    self.player.gold -= self.luckExpense
                    print(self.luckExpense, "gold was payed.")
                    self.luckGoldLevel += 1
                    self.luckExpense = 5 * self.luckGoldLevel ** 2 + 5 * self.luckGoldLevel
                else:
                    print("You can not affort that")
                time.sleep(1)
            elif command == "2":
                if self.player.attributePoints > 0:
                    self.player.knowledge += 1
                    print("Knowledge increased!")
                    self.player.attributePoints -= 1
                    print("One attribute point was used.")
                elif self.player.gold >= self.knowledgeExpense:
                    self.player.knowledge += 1
                    print("Knowledge increased!")
                    self.player.gold -= self.knowledgeExpense
                    print(self.knowledgeExpense, "gold was payed.")
                    self.knowledgeGoldLevel += 1
                    self.knowledgeExpense = 5 * self.knowledgeGoldLevel ** 2 + 5 * self.knowledgeGoldLevel
                else:
                    print("You can not affort that")
                time.sleep(1)
            elif command == "3":
                if self.player.attributePoints > 0:
                    self.player.wisdom += 1
                    print("Wisdom increased!")
                    self.player.attributePoints -= 1
                    print("One attribute point was used.")
                elif self.player.gold >= self.wisdomExpense:
                    self.player.wisdom += 1
                    print("Wisdom increased!")
                    self.player.gold -= self.wisdomExpense
                    print(self.wisdomExpense, "gold was payed.")
                    self.wisdomGoldLevel += 1
                    self.wisdomExpense = 5 * self.wisdomGoldLevel ** 2 + 5 * self.wisdomGoldLevel
                else:
                    print("You can not affort that")
                time.sleep(1)
                    
            elif command == "0":
                break
            self.saveToFile()
            print()
            
class WiseWords:
    # encrypted with a super-secret algorithm
    wiseWords = [
    "ehteoG nov .W .J - .emag gninniw a trats yam yeht tub ,netaeb eb yam yehT .drawrof devom nemssehc ekil era saedi gniraD",
    "sdlavroT suniL - .smargorp wen etirw tonnac ohw elpoep rof si ytilibatroP",
    "ehteoG nov .W .J - ?sruoy fo ti si ssenisub tahw ,uoy evol I fI",
    "sdlavroT suniL - edoc eht em wohS .paehc si klaT",
    "sdlavroT suniL - .elpoep druH eht ekil pu dne t'now uoy ebyam dna ,SGURD OT ON yas tsuj :trohs nI",
    "namllatS drahciR - .dlrow eht tpecca t'nod I !tihslluB .dlrow eht tpecca dluohs I dias elpoeP",
    ")POO fo rotnevnI( yaK nalA - .SOD-SM ecnis gnitupmoc tih ot gniht gnissertsid tsom eht si avaJ",
    "yaK nalA - .dnim ni ++C evah ton did I uoy llet nac I dna detneirO-tcejbO mret eht detnevni I",
    "namllatS drahciR - .ecirp ton ,ytrebil fo rettam a si erawtfos eerF",
    "ehteoG nov .W .J - .yawa nur d'I ,flesym wenk I fI ?flesyht wonKmlehliW resiaK - .dlrow eht reuqnoc lliw I dna reeb sevol ylurt ohw namow a em eviG",
    "setaG lliB - enoyreve rof hguone eb lliw etyboliK 046",
    "eM - .ynnuf era setouq-avaJ-itna eht tub ,avaJ ekil",
    "nwonknU - evil uoy erehw swonk ohw htapohcysp tneloiv a eb lliw edoc ruoy gniniatniam pu sdne ohw yug eht fi sa edoc syawlA",
    ".margorp avaJ a etirw yllautneve lliw meht fo eno ,sdraobyek noillim a ta syeknom noillim a tup uoy fI",
    "nwonknU - .yrotcaFmelborP a evah I woN .avaj htiw melborP a evlos ot deirt I",
    "nwonknU - .tib a yb ffo ylno era uoy ,gnorw era uoy fi neve si naeloob a tuoba gniht tseb ehT",
    "latkO - .gnitsil yrotcerid xinU a ni pu wohs t’ndluow ti os teN. deman tfosorciM kniht I",
    "redniL guoD - .teerts yaw-eno a gnissorc erofeb syaw htob skool syawla ohw enoemos si remmargorp doog A",
    "gnireenignE erawtfoS fo waL s’rehsoM - boj a fo tuo eb d’uoy ,did gnihtyreve fI .thgir krow t’nseod ti fi yrrow t’noD",
    ".margorp avaJ a etirw yllautneve lliw meht fo eno ,sdraobyek noillim a ta syeknom noillim a tup uoy fI"]
    amount = len(wiseWords)
    
    def __init__(self, player):
        self.save = Path(".save/wisdom")
        if self.save.is_file():
            self.save = open(".save/wisdom", "r")
            self.limit = int(self.save.readline())
            self.unlocked = int(self.save.readline())
            self.expenses = int(self.save.readline())
            self.save.close()
        else:
            self.limit = 2 # limit of wisdom you can unlock
            self.unlocked = 1 # number of unlocked wisdoms
            self.expenses = 10 # for buying a new wisdom
        
        self.player = player
        
    def saveToFile(self):
        self.save = open(".save/wisdom", "w")
        self.save.write(str(self.limit) + "\n")
        self.save.write(str(self.unlocked) + "\n")
        self.save.write(str(self.expenses) + "\n")
        self.save.close()
        
    def overview(self):
        print("[0] Leave")
        while(True):
            print("Wisdoms unlocked: ", self.unlocked, "/", self.limit)
            print("[1] Buy new Wisdom (costs", self.expenses, "gold)")
            print("[2] Give me a random Wisdom")
            print("[3] Show all wisdoms")
            command = input("> ")
            print()
            
            if command == "1":
                self.buyWisdom()
                time.sleep(1)
            elif command == "2":
                self.showRandomWisdom()
                time.sleep(1)
            elif command == "3":
                self.showAllWisdoms()
                time.sleep(1)
            elif command == "0":
                break
            print()

    def buyWisdom(self):
        if self.player.gold >= self.expenses and self.unlocked < 20:
            self.unlocked += 1
            print(self.wiseWords[self.unlocked - 1][::-1])
            self.player.gold -= self.expenses
            self.expenses = 5 * self.unlocked ** 2 + 5 * self.unlocked
            self.saveToFile()
        elif self.player.gold < self.expenses:
            print("You have not enough gold.")
        else:
            print("You allready have unlocked all wisdoms")
    
    def showRandomWisdom(self):
        print(self.wiseWords[randint(0, self.unlocked - 1)][::-1])
    
    def showAllWisdoms(self):
        for i in range(self.unlocked):
            print(self.wiseWords[i][::-1])
    
    def increaseLimit(self, value):
        if self.limit + value <= self.amount:
            self.limit += 2
            print("You can buy", value, "new wisdoms")
            self.saveToFile()
            time.sleep(1)
    
            
class Exercise:
    def __init__(self, number1, number2, solution, string):
        self.number1 = number1
        self.number2 = number2
        self.solution = solution
        self.string =  string
            
    def getRandomExercise():
        type = randint(1, 4) # + - * /
        if type == 1:
            n1 = randint(5, 25); n2 = randint(5, 25)
            return Exercise(n1, n2, n1 + n2, str(n1) + " + " + str(n2) + " = ")
        if type == 2:
            n1 = randint(5, 25); n2 = randint(5, 25)
            return Exercise(n1, n2, n1 - n2, str(n1) + " - " + str(n2) + " = ")
        if type == 3:
            n1 = randint(5, 15); n2 = randint(5, 15)
            return Exercise(n1, n2, n1 * n2, str(n1) + " * " + str(n2) + " = ")
        if type == 4:
            n1 = randint(10, 100); n2 = randint(2, 15)
            return Exercise(n1, n2, int(n1 / n2), str(n1) + " / " + str(n2) + " = ")
            
    def calculate(self):
        guess = saveIntInput(self.string)
        print()
        if guess == self.solution:
            return True
        else:
            return False
            
class Game:
    def __init__(self):
        self.player = Player()
    
    def mainLoop(self):
        print("Division Exercises have to be solved without rest.")
        print("[0] Leave")
        while(True):
            print("[1] Show Character")
            print("[2] Calculate")
            print("[3] Improve Attributes")
            print("[4] Give me a Wisdom")
            command = input("> ")
            print()
            
            if command == "1":
                print(self.player, "\n")
            elif command == "2":
                iterations = saveIntInput("How much times: ")
                print()
                for i in range(iterations):
                    exercise = Exercise.getRandomExercise()
                    self.player.reward(exercise.calculate())
                    print()
            elif command == "3":
                self.player.trainer.train()
            elif command == "4":
                self.player.wiseWords.overview()
                
            elif command == "0":
                break
            
### starting
            
if not os.path.exists(".save"):
    os.makedirs(".save")
            
game = Game()
game.mainLoop()
\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

Naming conventions

The PEP 8 style guide says that functions/methods should be snake_case and not camelCase. So for example saveIntInput should be save_int_input.

Some better naming

For something like:

string =  "Level            : " + str(self.level) + "\n"

string is the name of a library and is also undescriptive. I would change this to something like stats.

JSON?

self.save = open(".save/player", "r")
self.save.readline() # skip version detection (maybe needed later)

self.level = int(self.save.readline())
self.experience = int(self.save.readline())
self.experienceNeeded = int(self.save.readline())

self.rank = int(self.save.readline())
self.rankPoints = int(self.save.readline())
self.rankPointsNeeded = int(self.save.readline())

self.attributePoints = int(self.save.readline())
self.gold = int(self.save.readline())

This could be vastly simplified using JSON and the json Python library. tl;dr JSON is a convenient way of storing data (in file) that roughly resembles a Python dictionary. (There are also alternatives to JSON like XML, but I prefer JSON). You can also refactor saveToFile using JSON.

Place stats into a dictionary.

I would put the player stats into a dictionary so you can make access stats like:

self.stats["level"]

DRY

            if self.player.attributePoints > 0:
                self.player.luck += 1
                print("Luck increased!")
                self.player.attributePoints -= 1
                print("One attribute point was used.")
            elif self.player.gold >= self.luckExpense:
                self.player.luck += 1
                print("Luck increased!")
                self.player.gold -= self.luckExpense
                print(self.luckExpense, "gold was payed.")
                self.luckGoldLevel += 1
                self.luckExpense = 5 * self.luckGoldLevel ** 2 + 5 * self.luckGoldLevel
            else:
                print("You can not affort that")
            time.sleep(1)

And:

            if self.player.attributePoints > 0:
                self.player.knowledge += 1
                print("Knowledge increased!")
                self.player.attributePoints -= 1
                print("One attribute point was used.")
            elif self.player.gold >= self.knowledgeExpense:
                self.player.knowledge += 1
                print("Knowledge increased!")
                self.player.gold -= self.knowledgeExpense
                print(self.knowledgeExpense, "gold was payed.")
                self.knowledgeGoldLevel += 1
                self.knowledgeExpense = 5 * self.knowledgeGoldLevel ** 2 + 5 * self.knowledgeGoldLevel
            else:
                print("You can not affort that")
            time.sleep(1)

Are extremely similar and should be placed into a function. This is doable if you put stuff like self.knowledgeGoldLevel into an approprate dictionary (maybe stats?)

if-else chains

Instead of:

if command == "1":
    ...
elif command == "2":
    ...

You could make a dictionary (list?) that calls the correct function, so:

commands = {
    "1" : lambda params: update_stats(params)
    "2" : lambda params: update_stats(params)
    ...
}

About wiseWords (wise_words)

I guess if you really want to "encode" it fine. I wouldn't really bother, but ok. But wise_words along with rank_string don't appear to mutate so I would use a tuple (change the [ -> ( and ] -> )) to ensure that they don't mutate.

\$\endgroup\$
4
\$\begingroup\$

That's a lot of codes. Yikes!

Lazy execution

Right now if I wanted to use your game as part my game for example, the simple act of importing your game module, will start your game. However, what if I don't want this? Make it importable as a module. Here is how you do that:

if __name__ == '__main__':
    ### starting

    if not os.path.exists(".save"):
        os.makedirs(".save")

    game = Game()
    game.mainLoop()

Now I can do from wisdom_game import Game withou game t worrying that I will start playing the game.

Use __slots__

Define the properties of your objects by specifying them in a __slots__ array. For example, the Player class would have:

__slots__ = ('save', 'level', 'experience', 'experienceNeeded', 'rank', 'rankPoints', etc)

The reason for this is to keep the memory footprint of the objects down, so that when you create many of them, the memory usage is not too high.

Serialization

Consider storing the objects as JSON rather than line-by-line. Storing as JSON offers the following benefits:

  • python has a readily available json module, so you are not replying on any third-party libs to do this.
  • You just need to create a single method which can take a file name and will read the object from that file as json, then in the class you simply do self.__dict__.update(my_json_object).
  • If you decide to make this game playable over the network, JSON is already a popular method of sending structured messages over the network, so your model will work well in that case
  • You will not mistakenly read the wrong property into the wrong field. JSON format will map each attribute to it's value, so there will be no guessing if you decide to read some attribute before the other.

os.path.isfile

You don't need a third-party library to do this in python. The os module has got you covered.

That's all for now.

\$\endgroup\$
1
  • \$\begingroup\$ __slots__ is not very useful, often. pathlib is part of the standard library. \$\endgroup\$
    – Daniel
    Commented Dec 30, 2017 at 13:09
1
\$\begingroup\$

In saveIntInput you return a magic number, if the user enters no number. This is unexpected. The print suggests a repeated input, otherwise raise an exception.

self.save is not attribute, remove the self.. If you use Path, also use its open-method, with the with-statement.

Use string formatting.

while is not a function. No need for parentheses.

Game is not a class, write it as function.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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