Creating a dice game in Python (part 3)

In this post we continue developing a dice game in Python.

  • In Iteration 1 we coded a single round of the dice game for one player.
  • In Iteration 2 we added an additional loop so that multiple rounds were completed until the target score is reached.

Iteration 3

For this iteration we add a second player to the game. A high-level design for the revised game is shown below.

set target
set score1 to 0
set score2 to 0

while score1 < target and score2 < target
   player1 completes their turn
   update score1
   player2 completes their turn
   update score2
end while

output winner and scores

In the algorithm above, if player 1 reaches the target score, then player 2 will also get a turn before the loop terminates. However, the game should terminate as soon as one player reaches the target score. Another issue that we need to consider is ensuring that the amount of repeated code is minimised. A literal interpretation of the pseudocode above can lead to repeated code which we should avoid.

We improve the algorithm by adding a current player counter to keep track of who the current player is.

set target
set score1 to 0
set score2 to 0
set current_player to 1

while score1 < target and score2 < target
   current_player completes their turn
   if current_player = 1
   then
      update score1
      set current_player to 2
   else
      update score2
      set current_player to 1
   end if
end while

output winner and scores

After refining the algorithm to reduce the amount of repeated code we implement the algorithm in Python.
The first part of the code, as shown below, includes the necessary package imports together with variable initialisations. The variable current_player is used to track which player has the current turn. Initially this has a value of 1 (for player one) and will alternate between 1 and 2 until the game is complete.

import random
import time

target = 200
score1 = 0
score2 = 0
current_player = 1

The next block of code contains the main control loop and code for running the current player's round. The condition for the control loop (highlighted in the code listing) checks whether both scores are below the target score. The next line of code provides an indication of who the current player is.

while score1 < target and score2 < target:
    print("Player", current_player, "turn")
    round_score = 0
    valid = True
    while valid:
        roll1 = random.randint(1, 6)
        roll2 = random.randint(1, 6)
        total = roll1 + roll2
        if total == 7:
            valid = False
            print(total)
        else:
            round_score = round_score + total
            print(total, end=", ")
    print("You scored", round_score, "this round")

The block of code starting at line 23 is responsible for printing the total score of the current player and then updating the value of the current player before the while loop runs again. The code includes a conditional statement that checks who the current player is. Note that the code in the two branches of the condition block are very similar. This provides further opportunities for improving (refactoring) the code to reduce repetition. This is explore in the next part of the tutorial where we generalise to any number of players.

    if current_player == 1:
        score1 = score1 + round_score
        print("Total score for player 1 is", score1)
        current_player = 2
    else:
        score2 = score2 + round_score
        print("Total score for player 2 is", score2)
        current_player = 1
    time.sleep(1)

The final block of code outputs the winner of the game together with the final score. The winner is determined by comparing the scores of the two players within an if statement condition.

if score1 > score2:
    print("Player 1 wins", score1, "to", score2)
else:
    print("Player 2 wins", score2, "to", score1)

Example output that is generated when the code is run is shown below. Note that for brevity the output has been elided with only the first three and last round shown.

Player 1 turn
9, 10, 11, 6, 7
You scored 36 this round
Total score for player 1 is 36
Player 2 turn
2, 8, 10, 7
You scored 20 this round
Total score for player 2 is 20
Player 1 turn
6, 5, 9, 6, 9, 11, 10, 6, 9, 7
You scored 71 this round
...
Player 2 turn
11, 8, 11, 4, 6, 6, 5, 10, 5, 9, 7
You scored 75 this round
Total score for player 2 is 234
Player 2 wins 234 to 154

Creating a dice game in Python (part 2)

Iteration 2

In the second iteration of the development of Seven Out we add an outer loop so that the game will continue until the player’s total score equals or exceeds the target score – in this case 200. In the pseudocode shown below the outer loop is modelled as a while loop with a condition to check whether the target score has been met. The algorithm used inside of the outer loop is the same as that used in iteration 1 except a new variable is used to keep track of the score for the current round.


set target to 200
set score to 0
while score < target
   set round_score to 0
   loop
      set total to sum of two random integers between 1 and 6
      if total = 7
      then
         exit loop
      else
         add total to round_score
      end if
    end loop
    output round_score
end while
output score

The code written for this iteration is similar to that from iteration 1. New lines of code are highlighted in the listing below. Line 6 contains a while loop with a condition that checks that the score is less than the target. The while loop will continue while
this condition is true. In particular this while loop will always run at least once. Line 7 creates a new variable to represent the score for the current round - the value for this variable is reset to zero each time the outer loop is repeated. Line 17 updates the round score by adding the current dice total. Note that when a total of 7 is rolled this is not added to the round score.

import random

target = 200
score = 0

while score < target:
    round_score = 0
    valid = True
    while valid:
        roll1 = random.randint(1, 6)
        roll2 = random.randint(1, 6)
        total = roll1 + roll2
        if total == 7:
            valid = False
            print(total)
        else:
            round_score = round_score + total
            print(total, end=", ")
    score = score + round_score
    print("You scored", round_score, "this round")
    print("Total score is", score)

print("Well done. You got to", target)

Line 18 of the code uses an option within the print statement to alter its behaviour. The end option will by default print a newline character at the end of the output, meaning that subsequent output will appear on a new line. In this case we replace the default behaviour and instead end with a comma followed by a space. This means all of the totals for a round will be printed out on one line.

Output generated by running the game is shown below. For each round a list of comma-separated dice totals are shown, followed by the total for that round and then the total score. At the end of the game the player's total is shown.

8, 2, 3, 8, 2, 4, 9, 7
You scored 36 this round
Total score is 36
9, 5, 8, 10, 7
You scored 32 this round
Total score is 68
7
You scored 0 this round
Total score is 68
2, 9, 5, 11, 4, 4, 10, 6, 11, 11, 3, 4, 7
You scored 80 this round
Total score is 148
3, 6, 9, 7
You scored 18 this round
Total score is 166
6, 5, 5, 4, 7
You scored 20 this round
Total score is 186
8, 6, 4, 3, 6, 6, 7
You scored 33 this round
Total score is 219
Well done. You got to 200

Creating a dice game in Python (part 1)

Introduction

In this series of posts we will look at how to code a simple dice game in Python, starting with the initial concept, all the way through to the coding and adding extension features.


The game that we will develop is a simple two dice game called Sevens out. The game rules from lovetoknow.com are as follows:

Game rules

  1. The players decide upon a winning score, this could be 500 or 1,000.
  2. Each player, in turn, throws the dice.
  3. Each player rolls and continues to roll the dice until they throw a 7.
  4. They score the sum of all the numbers thrown before the 7.
  5. Players announce their tally after each throw and once they have thrown a 7, their score is noted on the score sheet.
  6. Doubles score double the value. For example, if a player who throws two 4s making eight, their score is 16.
  7. The player reaches the pre-arranged total first is the winner.

Iteration 1

Rather than attempting to develop the entire game in one pass, we will develop the game in multiple stages using an iterative development approach.
For the first iteration we will get one round of dice rolling completed for a single player.

Before writing any code we will develop a high-level design using pseudocode. This allows us to consider the key logical parts of the algorithmic solution without having to get bogged downed in the finer details of exactly how to code this in Python.

The pseudocode for our proposed first iteration is shown below. The most important part of the pseudocode is that it conveys the intended logic of the problem we are solving. For example the third and fourth lines of pseudocode indicates that a random number between 1 and 6 needs to be generated. How this is done does not matter at this stage – indeed the solution is dependent on the coding language being used, whereas pseudocode should be coding language independent.


set score to 0
loop
   set roll1 to random number between 1 and 6
   set roll2 to random number between 1 and 6
   set total to roll1 + roll2
   if total = 7
   then
      exit the loop
   else
      set score to score + total
   end if
end loop
output score

The resulting code in Python is almost a line for line translation of the pseudocode. One noteworthy change is the addition of a Boolean-valued variable, valid, which is used to indicate whether all rolls to date are valid (do not add up to 7). The while loop will continue whilst this variable is true. When a total of 7 is rolled the value of the variable becomes false. The use of this loop termination variable is highlighted in the code below. The relationship between the remainder of the code and the pseudocode should hopefully be evident.

import random

valid = True
score = 0

while valid:
    roll1 = random.randint(1, 6)
    roll2 = random.randint(1, 6)
    total = roll1 + roll2
    if total == 7:
        valid = False
    else:
        score = score + total

print("Score is", score)

The screenshot below shows the code running in the Pycharm IDE. The code runs until a total of 7 is rolled and then prints the resulting score.

Developing a text-based adventure game (Part 4)

Developing a battle system

In this tutorial we will look at the development of a simple battle system. We restrict our attention to a one-on-one battle.
We will implement a Dungeons and Dragons like battle system, where the player and the monster they are facing have a number of hitpoints. The player and monster will take it in turns to attack the other entity. Each attack causes damage that reduces the hitpoints of the entity that was hit. The damage caused by an attack will be randomly determined, but must lie between a predetermined minimum and maximum damage. The battle ends when the hitpoints of either the player or monster are reduced to zero or below.

Initial implementation

The battle mechanics will be encapsulated within the battle function.
The function takes seven inputs. The first three inputs are used for player information, describing the number of hitpoints that they had at the beginning of the combat, the minimum damage that one of their attacks does, and the maximum damage done by their attack. The remaining inputs are used for the title/name of the monster, the monster’s hitpoints and the minimum and maximum damage done by their attack.

The code for the battle mechanics is shown below. The battle function is tested by calling the function at the end of the code listing.

import random

def battle(hitpoints, minDamage, maxDamage,
           monsterTitle, monsterHitpoints,
           monsterMinDamage, monsterMaxDamage):
    playerAttack=True
    while (hitpoints>0 and monsterHitpoints>0):
        if (playerAttack):
            damage=random.randint(minDamage, maxDamage)
            monsterHitpoints=monsterHitpoints-damage
            print("You did {0:d} hitpoints damage".format(damage))
            if (monsterHitpoints <= 0):
                print("The {0} is dead".format(monsterTitle))
            playerAttack=False
        else:
            damage=random.randint(monsterMinDamage, monsterMaxDamage)
            hitpoints=hitpoints-damage
            print("You took {0:d} hitpoints damage".format(damage))
            if (hitpoints <= 0):
                print("You have been killed".format(monsterTitle))
            playerAttack=True

battle(20, 1, 8, "Ogre", 25, 1, 12)

Download source code [Battle mechanics (version 1)].

To randomise the amount of damage done we need to import the random module. From this module we use the randint function, which generates a random integer between the lower and upper bounds which are passed as inputs to the function. To keep track of whether it is the player’s turn or the monster’s turn we use a boolean-valued variable called playerAttack (line 6). This variable can be set to either true or false. A while loop is used to provide a sequence of attacks that continue until either the player or monster is reduced to zero or less hitpoints (line 7). Lines 8-14 of the code implement the player’s attack. The randint function is used to determine the amount of damage done, this is then subtracted from the monster’s hitpoints. If the monster’s hitpoints are reduced below zero, then the monster is declared dead. Lines 15-21 use a similar sequence of code to implement the monster’s attack. The boolean-valued playerAttack variable is updated after each attack to ensure that the player and monster have alternate attacks.

Determining if attacks hit

In the first version of the battle mechanics code all attacks hit. However it is more realistic that only some attacks hit – the attacker may have just swung wildly and missed their target, or the defender may have managed to dodge the attack, or perhaps even parry the attack with their shield. The improve the battle mechanics we adapt the code so that only some of the attacks hit their target. In the code shown below we assume that the player has a 50% chance of hitting their opponent, while the monster only has a 25% chance of hitting.

def battle(hitpoints, minDamage, maxDamage,
           monsterTitle, monsterHitpoints,
           monsterMinDamage, monsterMaxDamage):
    playerAttack=True
    while (hitpoints>0 and monsterHitpoints>0):
        if (playerAttack):
            hitRoll=random.randint(1,20)
            if (hitRoll>=11):
                damage=random.randint(minDamage, maxDamage)
                monsterHitpoints=monsterHitpoints-damage
                print("You hit!")
                print("You did {0:d} hitpoints damage".format(damage))
                if (monsterHitpoints <= 0):
                    print("The {0} is dead".format(monsterTitle))
            else:
                print("You missed")
            playerAttack=False
        else:
            hitRoll=random.randint(1,20)
            if (hitRoll>=16):
                damage=random.randint(monsterMinDamage, monsterMaxDamage)
                hitpoints=hitpoints-damage
                print("{0} hit".format(monsterTitle))
                print("You took {0:d} hitpoints damage".format(damage))
                if (hitpoints <= 0):
                    print("You have been killed".format(monsterTitle))
            else:
                print("{0} missed".format(monsterTitle))
            playerAttack=True

Download source code [Battle mechanics (version 2)].

Lines 7 and 8 of the above code are used to determine whether or not the player hits the monster. A random integer between 1 and 20 is generated and if this number if 11 or larger the player hits the monster, otherwise the attack results in a miss. Similar code is given on lines 19-20 to determine whether or not the monster hits.

Improvements

We improve the code by firstly generalising the hit chances of the player and monster by adding these as variables. We also add a return statement at the end of the code showing how many hitpoints the player has at the end of the battle. This will be used when we call the function in the game to keep track of the ongoing health of the player as well as to determine whether or not the game continues.

def battle(hitpoints, minDamage, maxDamage, toHit,
           monsterTitle, monsterHitpoints,
           monsterMinDamage, monsterMaxDamage, monsterToHit):
    playerAttack=True
    while (hitpoints>0 and monsterHitpoints>0):
        if (playerAttack):
            hitRoll=random.randint(1,20)
            if (hitRoll>=toHit):
                damage=random.randint(minDamage, maxDamage)
                monsterHitpoints=monsterHitpoints-damage
                print("You hit!")
                print("You did {0:d} hitpoints damage".format(damage))
                if (monsterHitpoints <= 0):
                    print("The {0} is dead".format(monsterTitle))
            else:
                print("You missed")
            playerAttack=False
        else:
            hitRoll=random.randint(1,20)
            if (hitRoll>=monsterToHit):
                damage=random.randint(monsterMinDamage, monsterMaxDamage)
                hitpoints=hitpoints-damage
                print("{0} hit".format(monsterTitle))
                print("You took {0:d} hitpoints damage".format(damage))
                if (hitpoints <= 0):
                    print("You have been killed".format(monsterTitle))
            else:
                print("{0} missed".format(monsterTitle))
            playerAttack=True
    return hitpoints

Testing the balance of the battle mechanics

Before we add the battle mechanics to the game we need to test it to make sure it has the right balance. We don’t want to always die when they battle a monster, but at the same time the monster should not be a pushover. Furthermore, if the player acquires an item that is supposed to give them an edge in combat then it should indeed give them and edge. To test the battle function we need to call it many times and then analyse the number of player wins versus the number of player losses. The best way to do this to use a for loop as shown in the code below.

wins=0
for i in range(0,1000):
    hps=battle(20, 1, 8, 11, "Ogre", 25, 1, 12, 16)
    if (hps>0):
        wins=wins+1

print(wins)

In this case the player has 20 hitpoints and a to-hit-roll of 11 (meaning they will need at least an 11 on a roll between 1 and 20 to hit). The monster has 25 hitpoints, does more damage, but is only half as likely to hit. Running the battle mechanics 1000 times with the given configuration results in the player winning 60.5% of the battles. This is probably too high in the situation when the player has not found the sword, but too low if they have found the sword. This suggests we need to adjust the player statistics for these two scenarios. We will use trial an error to get a 20% win chance if they don’t have the sword and an 80% win chance if they do have the sword.

Modifying the player’s damage to 1-4 hps and increase the to-hit-roll to 12 gives the player a win percentage of approximately 20%. On the other hand, if we change the player’s damage to 4-10 and decrease the to-hit-roll to 11, the player will have a win percentage of 79%.

Integrating battle mechanics into the game

To integrate the battle mechanics into the game we modify the playLocation3 function. When the player selects the attack option, the code will call the battle function. The values passed to the battle function depend on whether or not the player has picked up the sword earlier in the game, remembering that possessing the sword gives the player a much better chance of defeating the Ogre (compare lines 20 and 23).

def playLocation3():
    global inventory
    print("You reach what looks like a rudimentary camp site.")
    print("You notice a fire pit in the middle of the camp site.")
    print("Scattered around the fire pit are the many bones picked clean of flesh.")
    print("Ominously some of the bones look human like.")
    print("Just as you finish surveying the scene you here a roar.")
    print("Running towards you, wielding a large club is an Ogre.")
    print("You can either (f)lee to the south, (a)ttack the Ogre or try to (t)alk with the Ogre.")
    response=input("> ")
    if (response=="f"):
        print("You run away from the Ogre at full speed")
        print("The Ogre pursues for a while but eventually gives up.")
        print("You survive another day.")
        print("Once you know you are safe you continue your journey southward at a slower pace.")
        location="location1"
    elif (response=="a"):
        if ("sword" in inventory):
            print("As the Ogre draws near you draw your sword.")
            hps=battle(20, 4, 10, 11, "Ogre", 25, 1, 12, 16)
        else:
            print("You pick up a fallen branch from the ground and prepare to battle the Ogre")
            hps=battle(20, 1, 4, 12, "Ogre", 25, 1, 12, 16)
        if (hps >0):
            print("You have felled the evil beast.")
            print("You search the body of the Ogre and find a key.")
            print("You take the key and head back towards the clearing with the chest.")
            location="location1"
            inventory.append("key")
        else:
            print("You fight valiantly with the Ogre.")
            print("You manage to inflict several telling blows against it but it fights on.")
            print("Having dodged several sweeping blows, the Ogre brings its club down upon you with a crash.")
            print("...")
            location="finish"    
    elif (response=="t"):
        print("You attempt to make conversation with the Ogre but this only makes it more angry.")
        location="location3"
    else:
        location="location3"
    return location

Download source code [Stage 4 code]