Table of Contents
Step 003 - class and menu
How about having a (dynamic!) menu with user input, an option to view and modify the attributes of both goblins and an option to enter the number of fights before a series of battles start ?
The github sourcecode below does exactly that. And finally the to date useless intro variable is now meaningful: The introduction text is visible from the menu.
There is lot of new stuff packed into this code example. Don't worry if you can not understand or reproduce all of it …yet.
Look at the docstrings (remember, the multi-line strings after the function names) of unknown functions to get an idea about their purpose. Later, you will maybe replace those functions with your own, more specialized functions. Or maybe you will busy coding other stuff and ending up using those functions unmodified, without ever fully understanding how they work. As long as you know how to use them this is not much of a problem.
Flowchart
Download Flowchart as pdf This Flowchart should help you to visualize the github sourcecode below. All functions handling combat are stuffed inside an orange box (note the recursive firststrike() function!). The goblin class (green cloud) has itself 3 different functions. The game logic is handled by the menu() function and some helper functions (yellow container).
direct mode
You need to learn some core python concepts before the big github source code below start to make sense. Open idle or any other python direct mode ( like opening a terminal and typing python ) to access the direct mode. To learn new python commands and play around with them the direct mode is great:
iterating
Remember the concept of lists in python and how you can iterate over the items of a list ? Well, lists are not the only thing you can iterate over.
There are 3 important data types in python:
| tuple | list | dictionary | |
|---|---|---|---|
| symbol: | round brackets () | square brackets [] | curled brackets {} |
| advantage: | fast, low memory usage | it is easy to replace a single item of a list | stores pairs of key:item, seperated by an colon :. You can iterate over keys or items or both. Fast access of item if you know the corresponding key |
| disadvantage: | Immutable. If you want to change one item you have to change the whole tuple | More memory usage than tuples | More memory usage than tuples |
| example: | (“a”,”b”,-55,3.14) | [“a”,”b”,-55,3.14] | {“a”:1, “b”:3.14, “c”:-55} |
You can iterate over all three of those data types using a for loop. Let's take a closer look at the third data type, dictionaries:
>>>mydict = {"abc": 123, "bbb": 200, "cece": 3.14} >>>mydict["bbb"] 200 >>>mydict.keys() ["abc","bbb","cece"] >>>mydict.values() [123,200,3.14] >>>mydict.keys()[1] "bbb" >>>for x in mydict.keys(): print x, mydict[x] "abc" 123 "bbb" 200 "cece" 3.14 >>>
As you see you can loop over the keys of a dictionary and because the command keys() generates a list of keys. And you can iterate over all items of a list with the for loop.
Note that if you just iterate over a dictionary, you iterate in fact over the keys() of this dictionary:
>>>for x in mydict): print x "abc" "bbb" "cece"
Also interesting, you can loop over the lines of text of a multi-line string:
>>> mytext = """hello word how are you ? I am fine""" >>> mytext 'hello word\nhow are you ?\nI am fine' >>> lines = mytext.splitlines() >>> lines ['hello word', 'how are you ?', 'I am fine'] >>>
Note that a newline sign is symbolized by an \n. The splitlines() functions generates a list of srings (one string for each line) out of an multi-line string.
Classes
If you look at the previous source code example, you will notice that we need a bunch of variables (stinky_hitpoints, grunty_hitpoints etc.) for each goblin. That is not so much of a problem if we simulate only one or two goblins, but what if we want to simulate an army of goblins ? We would soon run out of names for the variables. And we would get lost in this mass of different variables. Good thing we have an computer to handle those variables for us.
The concept of a class() in python means that you define once all attributes and functions of a python class, like the Goblin class.
Let's do it in the direct mode:
>>>class Goblin(object): def __init__(self): self.hitpoints = 50 self.min_damage = 3 >>>
Once done, you can create as many individual instances of this class (goblins) as you want and the computer will take care of managing variables (like hitpoints) for each individual class instance (goblin). All we need to care for is to assign each goblin to a different variable.
stinky = Goblin()
In the example below the variables (lowercase!) are called stinky and grunty. Here in the directmode the goblin containing variable has the name mygoblin. To refer to the hitpoints of a single goblin we write the name of the variable containing this goblin, a dot and the attribute:
>>>stinky.hitpoints >>>50 >>>stinky.hitpoints = 100 >>>stinky.hitpoints 100
In Python, you can read and edit the attributes of each class instance (each Goblin) from inside the class as well as from outside the class. Let us create another goblin called grunty and give him twice the hitpoints of stinky:
>>>grunty = Goblin() >>>grunty.hitpoints = 2 * stinky.hitpoints >>>grunty.hitpoints 200
With only hitpoints and min_damage this example of a goblin class is rather sad… see the full github sourcecode below for a “real” goblin class with many attributes for each goblin. A class can also have it's own functions, those are called methods. So far you have only seen the __init__() method. This method is automatically called as soon as a new goblin is created. The word self inside the round brackets is necessary1) and refers to this single goblin. It is possible to pass parameters at the creation:
>>>class Goblin(object): def __init__(self, full_name): self.full_name = full_name self.hitpoints = 50 self.min_damage = 3 def speak(self) print "I'm a goblin and my full name is {0}".format(self.full_name) >>> >>>bigmouth = Goblin("Braxx the unwashed") >>>bigmouth.hitpoins 30 >>>bigmouth.speak() I'm a goblin and my full name is Braxx the unwashed
All the attributes of a class instance are stored into a dictionary. You can access this dictionary with __dict__. And because it's an dictionary you can iterate over it:
>>>bigmouth.__dict__ {'full_name': 'Braxx the unwashed', 'hitpoints': 30, 'min_damage': 3 } >>>for prop in bigmouth.__dict__: print prop, bigmouth.__dict__[prop] full_name 'Braxx the unwashed' hitpoints 30 min_damage 3
To find out what kind of class an class instance belongs to, you can access the dictionary of the class itself. This makes no sense yet because we know that bigmouth is a Goblin() instance. We have not created any other classes. Yet. But it may become useful in later steps of this tutorial when we create a lot of different classes:
>>>bigmouth.__class__.__name__ 'Goblin'
Text input and pretty text output
Iterating over the attributes of an goblin is good and fine, but to produce a really cute status report feature it would be nice to print the attributes vertical aligned like in a table. Using the {format} in combination with {:>10} to right-align output 10 characters like in this example:
>>> x = ["x", 5, 2000, 22] >>> y = ["y", -30000, 255, 899] >>> z = ["z", -314, 222, 0] >>> for a in range(len(x)): print "{0:>10}{1:>10}{2:>10}".format(x[a],y[a],z[a]) x y z 5 -30000 -314 2000 255 222 22 899 0
The source github source code example below has an option to modify the attributes of a single goblin. Python provides the input() and the raw_input() function: The input() function provides a prompt and expect the user to enter a number (decimal point and minus sign is allowed) and to press the Enter key:
>>> age = input("how old are you?") how old are you?15 >>> age 15
Because input() only accept numbers things get ugly if the user enters anything other than a number:
>>>age = input("how old are you?")
how old are you?i dunno
Traceback (most recent call last):
File "<pyshell#56>", line 1, in <module>
age = input("how old are you?")
File "<string>", line 1
i dunno
^
SyntaxError: unexpected EOF while parsing
Therefore, it's better to use raw_input() because raw_input() accept all forms of text input, including numbers. Raw_input() always returns a string. To transform this string back into a number, you test it first using the isdigit() function and use the int() function to get an integer value. (use float() if you need decimal numbers):
>>> age = raw_input("how old are you?") how old are you?99 >>> age '99' >>> age.isdigit() True >>> real_age = int(age) >>> real_age 99
You should now have all necessary information to understand the github source code below. It's quite a piece of code, so let's analyze it:
Code discussion
import
Let me break up the big github code below into chunks. The fist few lines are nothing new:
#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2011 Horst JENS <horst.jens@spielend-programmieren.at> # part of http://ThePythonGameBook.com # licence: gpl, see http://www.gnu.org/licenses/gpl-3.0.txt import random # this example uses classes to store the propertys of the goblins, # introduces the __main__ function and handles user-input # with the raw_input() function
class
Now comes the class definition for the Goblin() class.
class Goblin(object): """goblin with name, and other individual values""" def __init__(self): """standard out-of-the-cave goblin, alter values after creation""" self.name = "Dummy" self.hitpoints = 50 self.wins = 0 self.min_damage = 1 self.max_damage = 6 self.min_speed = 1 self.max_speed = 7 def report(self, description=True): """print out all attributes with or without description (column header) right-align all for 20 chars.""" if description: msg = "{0:>15} : {1:>15} \n".format("class",self.__class__.__name__ ) else: msg = "{0:>15} \n".format(self.__class__.__name__) for x in self.__dict__.keys(): if description: msg+= "{0:>15} : {1:>15} \n".format(x, self.__dict__[x]) else: msg+= "{0:>15} \n".format(self.__dict__[x]) return msg def modify(self): """allow the user to modify all values of this Goblin""" newName = False print self.report() for x in self.__dict__.keys(): new = raw_input("{0}: {1} (Enter=accept)>".format(x, self.__dict__[x])) if new != "": if x == "name": # accept everything as name self.__dict__[x] = new print "name changed into {0}".format(new) newName = True # modify menu else: # accept only numbers if new.isdigit(): self.__dict__[x] = new print "new value accepted" else: print "new value rejected because not a number" print "--- i print now the new values" print self.report() if newName: return self.name else: return None
As you already know, class names start with an uppercase letter. The Goblin() class has no parent class (more on that in later steps) so you write a generic object into the round brackets.
A part from the necessary __init__() function, this class has 2 other methods: the report and the modify method. Both methods are a bit hard to read.
I recommend that you start the whole example and try to modify some goblins so that you see in practise what those methods do.
Both methods iterate over all attribute of a single goblin. The modify method assumes that only the attribute name is a string and all other attributes are integer variables. Each attribute and it's value is printed on the screen and the user can either accept the current value by pressing Enter or modify the value.
The report method is even more complicated, as it has 2 modes (depending of the value of the description parameter). In the direct mode text above, there is a short example about the bigmouth goblin and his speak() method.
That is basically what the report() method does. It iterates over and returns all attributes of a goblin, either with the description (like “hitpoints:”) or without. This is necessary for the compare() function below to get the attributes of 2 goblins printed side-by-side in a little tabel.
variables
intro = """ ---- Introduction ------- Two goblins, Grunty and Stinky, play the traditional game of Goblin Dice Duel The rules a very simple. Each goblin throws a die and is allowed to hit the other goblin on the head with a club as often as the number of eyes on his throwed die. This is called damage. As each goblins has an individual number of hitpoints (how much damage he can suffer) the last gobling standing is the winner. Note that dice in the goblin cave are made out of bones and are not six-sided as the dice you may know. Each dice has a minimal value (number of eyes) and a maximal value. To find out if weak and smart goblins beat dumb and strong goblins more often than not, it is necessary to observe thousands of games """ stinky = Goblin() # create stinky with default values stinky.name = "Stinky" # adapt some values of Stinky the Goblin stinky.min_damage = 3 stinky.max_damage = 4 stinky.min_speed = 4 stinky.max_speed = 9 grunty = Goblin() # create Grunty with default values grunty.name = "Grunty" # adapt some values of Grunty grunty.max_damage = 7 grunty.max_speed = 7 grunty.hitpoints = 60 menuitems = [] # empty list menuitems.append("read introduction") # 0 menuitems.append("view and compare goblins") # 1 menuitems.append("modify Stinky") # 2 menuitems.append("modify Grunty") # 3 menuitems.append("make many fights") # 4 menuitems.append("quit") # 5
Here comes the part where some variables will be assigned. Note that both goblins get their default values for hitpoints, min_damage etc. as soon as they are created, but those values are later overwritten to make sure the two goblins have different attributes.
Also a list of menu items for the main menu is created here. The .append() command simply adds an item to an existing list. The whole menu is dynamic: If you later change Stinky's name into “BigBoss” (using the modify menu), the third item (item number 2) will be displayed as “modify BigBoss”.
compare function
The compare function is small and simple. By calling the report() method of both goblins, a big multi-line text-string is generated and returned.
def compare(leftGoblin, rightGoblin): # make a list of lines with description and values: leftLines = leftGoblin.report(True).splitlines() # make a list of lines with only values, no description rightLines = rightGoblin.report(False).splitlines() msg = "" for lineNumber in range(len(leftLines)): msg += "{0} vs. {1} \n ".format(leftLines[lineNumber], rightLines[lineNumber] ) return msg
menu
The menu function include several loops. Basically all menuitems are printed, the user is asked for a menu number to select, this answer is tested for correctness and then the corresponding function(s) are called.
def menu(menuitems): while True: # endless menu loop for item in menuitems: print menuitems.index(item), item while True: # endless user input loop wish = raw_input("your choice (and Enter):") if wish.isdigit(): if int(wish) >= 0 and int(wish) < len(menuitems): break # break the endless user input loop print "your wish was %i: %s" % (int(wish), menuitems[int(wish)]) if int(wish) == 0: print intro elif int(wish) == 1: # compare goblins msg = compare(stinky, grunty) print msg elif int(wish) == 2: # modify Stinky (and menu entry) newname = stinky.modify() if newname != None: menuitems[2] = "modify " + newname elif int(wish) == 3: # modify Grunty (and menu entry) newname = grunty.modify() if newname != None: menuitems[3] = "modify " + newname elif int(wish) == 4: # fight ! while True: #endless user input loop amount = raw_input("how many fights (Enter)") if amount.isdigit(): if int(amount) >0: break # correct answer many_games(stinky, grunty, int(amount)) elif int(wish)==5: break # break out of the menu loop
battle functions
Four different functions are involved to let 2 goblins fight each other:
- strike() # calculate damage and reduce hitpoints
- first_strike() # This is a recursive function to see who get the first strike. This function can actually call itself (!) as often as necessary.
- combat() # combat calculates a single duel between 2 goblins, calling first_strike() and strike() until one goblin has no hitpoints left
- many_games() # allows a big number of single combats. To better test out the effect of attributes.
def firstStrike(leftGoblin, rightGoblin): """this recursive function computes the faster of 2 Goblins""" leftSpeed = random.randint(leftGoblin.min_speed, leftGoblin.max_speed) rightSpeed = random.randint(rightGoblin.min_speed, rightGoblin.max_speed) print "both Goblins try to strike each other..." if leftSpeed == rightSpeed: print "but both are equal fast {0}:{1}".format(leftSpeed, rightSpeed) return firstStrike(leftGoblin, rightGoblin) # recursion ! elif leftSpeed > rightSpeed: print "{0} is faster ({1}:{2}) and strikes first !".format(leftGoblin.name, leftSpeed, rightSpeed) return leftGoblin else: print "{0} is faster ({1}:{2}) and strikes first !".format(rightGoblin.name, rightSpeed, leftSpeed) return rightGoblin def strike(attacker, defender): """the attacker strikes against the defender""" damage = random.randint(attacker.min_damage, attacker.max_damage) defender.hitpoints -= damage print "{0} strikes against {1} and causes {2} damage." \ "({3} hitpoints left)".format(attacker.name, defender.name, damage, defender.hitpoints) def combat(leftGoblin, rightGoblin): """a function that takes 2 goblins (class instances) let them fight and returns the winning goblin""" combatround = 0 # the word "round" is a reserved keyword in python print "saving hitpoints..." original_hp_left = leftGoblin.hitpoints original_hp_right = rightGoblin.hitpoints while leftGoblin.hitpoints > 0 and rightGoblin.hitpoints > 0: combatround += 1 # increase the combat round counter print " ----- combat round %i -------" % combatround # who attacks first ? firstStriker = firstStrike(leftGoblin, rightGoblin) if firstStriker == leftGoblin: # leftGoblin strikes first strike(leftGoblin, rightGoblin) if rightGoblin.hitpoints <= 0: break else: print "{0} strikes back !".format(rightGoblin.name) strike(rightGoblin, leftGoblin) elif firstStriker == rightGoblin: # rightGoblin strikes first strike(rightGoblin, leftGoblin) if leftGoblin.hitpoints <= 0: break else: print "{0} strikes back !".format(leftGoblin.name) strike(leftGoblin, rightGoblin) else: print "no first strike ??" #----- end of loop ---- print "==================================" if leftGoblin.hitpoints > 0: print "{0} is the winner !".format(leftGoblin.name) leftGoblin.wins += 1 print "restoring original hitpoints" leftGoblin.hitpoints = original_hp_left rightGoblin.hitpoints = original_hp_right return leftGoblin else: print "{0} is the winner !".format(rightGoblin.name) rightGoblin.wins += 1 print "restoring original hitpoints" leftGoblin.hitpoints = original_hp_left rightGoblin.hitpoints = original_hp_right return rightGoblin def many_games(leftGoblin, rightGoblin, number_of_fights=1000): """calls the combat function 1000 times""" print "setting wins to zero" leftGoblin.wins = 0 rightGoblin.wins = 0 for fight in range(number_of_fights): print "fight number %i" % fight winner = combat(leftGoblin, rightGoblin) print "===============================" print " * * * end results * * * " print "===============================" print "{0} wins: {1} vs. {2} wins: {3}".format(leftGoblin.name, leftGoblin.wins, rightGoblin.name, rightGoblin.wins)
main()
Those to lines are a common sight in many python programs:
if __name__ == "__main__": menu(menuitems)
Each python module can -a bit like functions- be called (imported) from another python module. Remember the random.randint() function ? There are many functions inside the random module, but in this example only the random.randint() function is used.
At a later point in time, you may want to use the menu() or combat() function from this example but not the other functions. At this time, however, you want the program to start as it is with the menu() funcion. By testing the internal variable __name__ we can find out if this program is called from another python module or started directly. In the latter case, the value of __name__ is __main__.
ideas
You have now a pretty, working goblin duel simulator. Not much of a game yet but a handy tool to balace out values. Some ideas you can try to implement:
- randomize goblin values. create a method in the goblin class or modify the __init__() method using random.randint(). Ideally, create a new menu item to randomize a goblin.
- Do more menu items. Append them in the variable section and modify the menu() function to accept the user input. Start with something simple like an help text. See the intro menu point.
- Improve the combat mechanics. How about adding attack and defense values, values for armor and dodging, not to speak of chances for critical damage (=damage multiplier) ?
- add a betting system. Let the computer generate a sum value of all goblin attributes (except “name” of course). The higher this value, the better is the goblin. Use the difference in the sum values of 2 goblins to calculate the win quota. Try to bet on a underdog. Increase the number of games.
Source Code on Github
To start this example you need:
| file | in folder | download | comment |
|---|---|---|---|
| 003_goblindice.py | python | Download the whole Archive with all files from Github: https://github.com/horstjens/ThePythonGameBook/archives/master | version for python2.x |
python 2.x
View/Edit/Download the file directly in Github: https://github.com/horstjens/ThePythonGameBook/blob/master/python/003_goblindice.py
click reload in your browser if you see no code here:
Comment this Page








