Skip to content

Latest commit

 

History

History
executable file
·
240 lines (150 loc) · 18.2 KB

goblindice003.rst

File metadata and controls

executable file
·
240 lines (150 loc) · 18.2 KB

unlimited damage

about

This page demonstrate some more sophisticated random functions to model a more detailed combat model and introduce the use of a recursion. Also shows how to organize functions in a module and access them from other python programs to provide output inside an :term:`GUI` Graphical user interface.

Note

Please help improving this tutorial!

Do you found a typo or failure ? Do you have ideas to improve this tutorial? Please edit/comment this file directly at https://github.com/horstjens/ThePythonGameBook/blob/master/goblindice003.rst (you need an account at github.com to do so).

idea

Allow unlimited damage by using the recursive re_roll function that can call itself. Also, packing the whole combat calculating inside a strike function that can either produce text ouptut or returns a text string to an external python program to be displayed inside a scrollbox GUI widget.

Flow Chart for goblindice001

less boring combat

By using random.randint() to calculate the attack and defense values, the outcome is limited to a finite number of possibilites. In the pen-and-paper era, a dice throw using a 20-sided die can simulate 20 different outcomes. But does a real fighter has only 20 different ways to act ? Using the calculating power of a computer, it's easy to simulate an (near) infinte numbers of possible outcomes by using -for example- the random.random() function instead of random.randint():

Random.random() generates a random float (decimal point) number bewteen 0.0 and 1.0, like 0.423156879. While precision or number of post decimal positions is not infinite but depends of your computers CPU power, it is in practice high enough to simulate endless different outcomes. The distribution of numbers generated by random.random is equal: The probability to make an extreme powerful attack (like >0.9) is the same as the probability to make an extreme unsuccessful attack (like <0.1).

attack values generated by random.random()

To the right is a graphic (generated with :ref:csvmaker.py and openoffice) that shows the distribution of 1000 attack values generated with the``random.random())`` function. While all values are between 0.0 and 1.0, there is no clear prediction possible about an most popular number. Note that to save place on the x-axis, all values are grouped at the second decimal position. If a million attack values would be calculated instead of just 1000, the bars would have more equal hight, according to the law of large numbers.

Another fine effect of using floats instead of integers is that you can read the output generated by random.random() direct as an probabilty or percent chance: an attack score of 0.5 means a "fair" chance of 50% to hit an opponent, while an attack score of 0.99 means "perfect" chance (99%).

To simulate differnt attack skill of players, simply a player's base attack skill to the random.random() value, much like in the previous examples:

# add a random float from 0.0 to 1.0 to the attack value
attack = base_attack + random.random()

more sophisticated combat rules

standard deviation (wikipedia)

Long etablished role-playing systems use like the D&D series (Dungeons & Dragons) use for their combat calculation different kind of dice: traditional dice with 6 faces (d6) as well as less usual dice with 20 faces (d20) as well as other, even more obscure dice.

Having different kind of dice is not only because of vanity and nerd fashion. There is a mathematical difference of calculating an e.g. defense value ranging from 1 to 20 points with either one 20-sided die (1d20) or with 4 6-sided dice (4d6). Both ways produce values between 1 and 20 (after subtracting 4 from the 4d6 result). The difference is that with the 1d20 die, each outcome has the same probability; by using 4d6, the distribution of values favor the middle values and makes extreme values at both ends less likely - a Gaussian distribution (see graphic).

Using python, it is not necessary to emulate multiple dice throws - python provides the random.gauss(m,s) function to create Gaussian distributed float values.

The random.gauss(m,s) function requires two arguments: m and s. The first argument, m (greek letter: mu ) is the :term:`median` or the middle value while s (greek letter: sigma) is the :term:`standard deviation`: how far the random values are distanced from m on average. Wikipedia explains it best:

At the graphic (right) dark blue is one standard deviation on either side of the mean. For the normal distribution, this accounts for 68.27 percent of the set; while two standard deviations from the mean (medium and dark blue) account for 95.45 percent; three standard deviations (light, medium, and dark blue) account for 99.73 percent; and four standard deviations account for 99.994 percent. The two points of the curve that are one standard deviation from the mean are also the inflection points.

In this example, random.gauss(0.5,0.2) is used to model the defence values:

# add a random float ( 0.5 +- something) to the defense value
defense = base_defense + random.gauss(0.5,0.2)
defense values generated by random.gauss(0.5,0.2)

Different defense skill could be simulated by different base_defense values as well as by differnt values for mu (the higher, the better). An experinced fighter would also have a lower sigma, reflecting that he is able to defend with the same level of quality each time, without wide variation. Note that this random.gauss(0.5,0.2) function produces a few negative defense values and sometimes defense values higher than 1:

At the right is a graphic (generated with :ref:csvmaker.py and openoffice) that shows the distribution of 1000 defense values generated with the random.gauss(0.5,0.2)) function. Most -but not all!- values are between 0.0 and 1.0. Unlike the attack values, here is a clear prediction possible about a `most popular`number: it's (around) 0.5, the mean or mu value. Note that to save place on the x-axis, all values are grouped at the second decimal position. If a million attack values would be calculated instead of just 1000, the bars resemble more a perfect Gaussian distribution.

dice throw with re-roll

It is no big deal for a combat sim to produce sometimes negative attack or defense values (simulating a very lucky or unlucky attack) by using the random.gauss() function. Not so for the damage calculation: A negative damage would mean that an opponent get more hitpoints if he is hit!

One way to make sure that the random.gauss() function returns only useful values is to pack it inside an if elif else construct or using the in-built min() and max() functions of python:

def limited_gauss(m=0.5, s=0.2, upper_limit = 1.0, lower_limit=0):
    """returns an gauss random value inside limits"""
    gauss_value = random.gauss(m,s)
    return min(upper_limit, max(gauss_value,lower_limit))
defense values generated by random.gauss(0.5,0.2)

If you prefer to use integers instead of floats for damage calculation take a look at this recursive re_roll() function below to calculate unlimited damage. It simulates throwing dice with the rule that you can sometimes throw agein (re-roll): if the highest possible number (a 6 on a six-sided die) is rolled, 1 is subtracted from the actual rolled number (6-1=5) and a re-roll is allowed and it's number is added. If the second (re-roll) throw is also a 6, this procedere is repeated: 6-1 + 6-1 + 6-1 until a number lesser than 6 is rolled.

The -1 rule is added so that it becomes possible to roll a natural 6: 6-1 + 1 = 6. Witout this rule, only 5 and 7 would be possible, but never 6.

The re_roll function can deliver very high numbers with a very low probability. See this graphic at the right of 1000 damage values calculated using re_roll(6). Note that the first 5 numbers have nearly equal (high) probability, while the next 5 numbers have a much lower, but also nearly equal probability, and so on. If a million values are calculated instead just 1000, the graphic would resembele a staircase with several steps.

This is a function that can call itself, a recursive function or :term:`recursion`. In the flow-chart above, the recursion is symbolized by the dashed arcs pointing from the end of the re_roll function to it's top. A function that calls itself and return it's return values to itself, to be used as parameters - for itself.

Note

warning

Keep in mind that a bad programmed recursion can become an endless loop (the computer "hangs") and that the maximum number of "recursion depth" is depended on the main memory of the computer.

Here is the code:

def re_roll(faces=6, start=0):
    """open ended die throw, can re-roll at highest face)"""
    while True:
       roll = random.randint(1, faces)
       if roll != faces:
          return roll + start
       return re_roll(faces, roll-1+start )

Notice the equal side inside the round brackets of the function's first line: (faces=6, start=0) are :term: default values meaning that if the caller of the function provides no parameters, those default values will be used. Note that you non-default values must come before default values. A caller can provide all, some or no parameters to overwrite the default-values.

organising code with functions

It makes sense to organise your code into functions and to organise your functions and code in python modules. Python's random module alone provides access to over 20 random-specific functions. Each pyton file can be imported by another any other python module as long as both are in the same folder or can be found via the :term: python path.

To avoid automatic execution of code when a python program is imported, there is usually not much code on modul level (non-idented) of python programs that will be imported by other programs. Instead, a special if constroct is used to determine if a python program is started directly or imported as a module from another python program:

In the first case, python set the internal variable __name__ to the value of '__main__'. It this is the case, usually a special function or code block is executed (or simply nothing is done). In the case of goblindice003.py, the return value of the function combatsim is printed to the screen.

Otherwise, if another program, like scrollbox.py uses import to import goblindice003.py, nothing is done by goblindice003.py itself - just all it's functions are provided to the calling program.

graphical gui with tkinter

The inner details of the program scrollbox.py will be explained in another tutorial. At the moment, it is just important that scrollbox.py uses import combatsim003 to import goblindice003.py and calls the combatsim() function to produce output in a scrollable gui widget.

In the next chapter, a more simple to use module -easygui- will be used as graphical gui. The reason for using scrollbox.py is that tkinter is part of the standard python library, while easygui is not.

Code

This code consist of 2 seperate code blocks.

prerequesites

  • necessary:
    • python3 is installed
    • both files, ´´goblindice003.py´´ and ´´scrollbox.py´´ are in the same folder
    • tkinter is correctly installed (usually automatic togehter with python or idle)
  • recommended:
    • python-friendly IDE like IDLE, Geany etc.

source code

The goblindice003.py can run stand-alone but will only produce text ouput:

.. literalinclude:: /python/goblindice/goblindice003.py
   :language: python
   :linenos:

scrollbox output

example output:

*** Round: 28 *** Grunty has 3 hitpoints, Stinky has 67 hitpoints
Smack! Stinky hits Grunty with a most skilled attack: 1.64>1.33
...and inflicts 2 damage!
Smack! Grunty hits Stinky with a most skilled attack: 1.37>0.20
...and inflicts 6 damage!
*** Round: 29 *** Grunty has 1 hitpoints, Stinky has 61 hitpoints
Oh no! Stinky does not even hit Grunty 0.96 < 1.40
Oh no! Grunty does not even hit Stinky 0.59 < 0.87
*** Round: 30 *** Grunty has 1 hitpoints, Stinky has 61 hitpoints
Smack! Stinky hits Grunty with a most skilled attack: 1.79>1.48
...and inflicts 4 damage!
- - - - - - - - - - - - - - - - - - - -
Victory for Stinky after 30 rounds

This small python program displays the text output of goblindice003.py inside a cute, scrollable text widget:

.. literalinclude:: /python/goblindice/scrollbox.py
   :language: python
   :linenos:


code discussion

code discussion goblindice003.py:

line number term explanation
1 - 17 :term:`docstring` Docstring insinde triple-quotes """.
18 import random This code is on module level.
20 - 21 re_roll function re_roll function with standard values and docstring. Note that a minus is by python interpreded as an mathematical operator and not allowed inside names. Thus re-roll as name would produce an error. Pytho would try to subtract roll from re.
22 endless loop A while True loop is always dangerous (endless loop) and should be terminated by an break - or in this case, by return.
31 float attack value The attack values are even distributed floats by using random.random()).
33 float defense value The defense values are Gaussian distributed floats by using th random.gauss(0.5,0.2)
39 + 46 format mini language for float It is possible to limit the decimal places of float numbers shown inside string with the .format() mini language. The number inside curly brackets after colon and dot is the desired amount of decimal places. Note that python use the full precision for calculating and only show less decimal places.
49 combatsim function The code inside the combatsim function was on module level in goblindice002.py and is now packed inside a function to organize the code better.
93 if __name__ == '__main__': This is a very common sight in many python programs. Note the double underscore (indicating special python variables) and that you need quotes around '__main__'. The double equal sign is a test to compare strings for equality.

code discussion scrollbox.py:

Tkinter is a gui library included into python, but there exist many others (often better looking) guis. How to code tkinter is topic of another tutorial. Here only some eays to topics are discussed.

line number term explanation
11 import x as y to avoid typing too much, it is possible to give alias names to imported modules.
16 hexadecimal color-code '#808000' is a string of three hexadecimal numbers (after the #) representing a color (background color of the widged in this case). Hex-numbers range from 00 (=0) to FF (=255). The first pair is the red part, the second pair the green part and the third part is the blue part of a color.
21 - 22 dimensions height and width is notated in lines / chars. Change this values to see a different scrollbox shape.
25 calling combatsim In this line, the function combatsim() of the module goblindice003 is called inside the command to fill the widget with text. Try to append some static text to it using the ´´+´´ operator.