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()
3 Answers 3
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.
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.
-
\$\begingroup\$
__slots__
is not very useful, often.pathlib
is part of the standard library. \$\endgroup\$Daniel– Daniel2017年12月30日 13:09:58 +00:00Commented Dec 30, 2017 at 13:09
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.