Skills Mapping
Dr J. G-H-Cater - 31/01/2025

Python Programming Exercise


Demonstrating Python Programming Skills

This lab explores the fundamentals of Object-Oriented Programming (OOP) in Python, and provides an outline for working towards a knowledge and application claim against the python programming skill.

Lab Summary

In this lab, you will be responsible for developing a dice rolling mechanic suitable for use in an arbitrary table-top game. This system will provide support for rolling dice with different face counts, custom face labels, and plotting the distribution of possible outcomes. The final solution will take the form shown in

The UML Diagram showing the classes for a dice rolling tool for boardgames.
The UML Diagram showing the classes for a dice rolling tool for boardgames.

The lab is broken into two distinct parts, the first will walk you through the design for the Dice and DiceRoll classes, providing the support suitable for a knowledge claim of the python programming skill. The second part simply provides a specification for the RollAnalyser class, providing a means to claim application of the skill. You will need to ensure that any additional reflection required by the claim is provided.

Part 1 - Rolling Dice

In this section we will create the Dice and DiceRoll classes. These classes are designed to incrementally add functionality to our program. Upon completion of this section, you should have a workable solution for rolling arbitrary handfuls of dice.

The Dice Class

The Dice class provides the core rolling functionality for a single die. It has support for custom face counts and face labels - meaning that you could create a 20-sided die, or a die with 6 sides, each labelled with a different even number.

The UML Diagram for the Dice Class.
The UML Diagram for the Dice Class.
  1. Define a new class called Dice and add the following docstring under the class definition line:
    1
    2
    3
    4
    5
    6
    7
    
    """
    A class that represents a single die with defined face labels or face count.
    
    Args:
        faces (int | List[int]): An integer to represent the number of faces, 
            or a list containing the label on each face.
    """
    
    The docstring for the Dice class.
  2. Create the __init__() function for the Dice class. This function should detect whether the user has passed a single instance of an int or a list of integers and populate the face_count and face_labels parameters of the class accordingly.
  3. Now create the roll() method for the dice class, which uses the following docstring:
    1
    2
    3
    4
    5
    6
    
    """
    Simulates rolling the die once.
    
    Returns:
        int: The label of the face that lands on top.
    """
    
    The docstring for the roll() method of the Dice class.
    Note, this method should pick a random face from the face_labels list. You may need to use the randint function available from the random package.
  4. Finally, create the roll_n(times) method for the dice class. This method simply calls the roll() method for however many times the user has requested, and returns a list of the results. You can probably achieve this using list comprehension if you wish to be efficient. Add the following docstring to this function:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    """
    Simulates rolling the die multiple times.
    
    Args:
        times (int): The number of rolls to perform.
    
    Returns:
        List[int]: A list of results for each roll.
    """
    
    The docstring for the roll_n() method of the Dice class.

You may wish to test the class you have just created, for example you could instantiate a die with 6 faces and roll it 30 times, making sure that you see all 6 faces come up at least once. The greatest risk here is that you get the indexing slightly wrong, meaning that the lowest or highest face may never occur. Make sure to test passing both a face count and a face list into the class when instantiating to ensure that it functions correctly in both cases.

The DiceRoll Class

The DiceRoll class provides a higher level functionality, where rolls may consist of multiple quantities of different dice. It also provides a tool to calculate each possible outcome for the roll it represents. For example, you may wish to roll 2d2 + 3 (rolling two 2 sided dice and then adding a constant 3 modifier to the result). This has 9 possible outcomes:

Raw Rolls Total Roll Outcome (Total Roll + 3)
[1, 1] 2 5
[1, 2] 3 6
[1, 3] 4 7
[2, 1] 3 6
[2, 2] 4 7
[2, 3] 5 8
[3, 1] 4 7
[3, 2] 5 8
[3, 3] 6 9

This class has three parameters: the dice parameter contains a list of dice for use in the roll; the count parameter contains a list of how many times each die should be rolled (meaning that dice and count should be the same length); and finally, the modifier parameter is the constant value that should be added to the roll total (this modifier could be negative). These parameters may be seen listed in .

The UML Diagram for the DiceRoll Class. Note that the possible_outcomes() method will only be written in part 2 of this lab.
The UML Diagram for the DiceRoll Class. Note that the possible_outcomes() method will only be written in part 2 of this lab.

This class also supports two key methods: the roll() method provides a single result as a tuple, where the first entry is the roll outcome, and the second is the list of raw rolls that resulted in this outcome. For example, if I rolled the 2d2 + 3 example from above, the result could be:

  1. Define a new class called DiceRoll and add the following docstring under the class definition line:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    """
    Represents a roll of multiple dice, optionally with a modifier.
    
    Attributes:
        dice (List[Dice]): A list of `Dice` objects representing the dice to roll.
        count (List[int]): A list of integers representing how many times each die is rolled.
        modifier (int): A numeric modifier to add to the total roll result.
    
    Args:
        roll (List[Tuple[Dice, int]]): A list of tuples where each tuple contains a `Dice` object 
            and the number of times it should be rolled.
        modifier (int | None): An optional modifier to add to the total result. Defaults to 0.
    """
    
    The docstring for the DiceRoll class.
  2. Create the __init__() method for the DiceRoll class. This method should accept a list of tuple pairs as its first argument, for example [(Dice(6),3), (Dice(4),2)], where the first entry of each tuple is the die to roll, and the second is the number of times that it should be rolled. In this example, the tuple represents 3 6-sided dice and 2 4-sided dice being rolled together. It should also accept an integer modifier as an optional argument (defaulting to 0 if not provided). Write code so that the __init__() method can split the list of tuples up into a list of dice (self.dice) and a list of roll counts (self.count), as well as storing the modifier (self.modifier).
  3. Create a method called roll() which cycles through each self.dice entry and rolls it self.count times (using the previously defined Dice.roll_n() method). The results of this set of die rolls should be added to a list, so that - by the end - you have a list of all dice roll results. The sum of these dice roll results should then be found, and the modifier applied. This final result should be combined into a tuple with the roll list. For example, asking DiceRoll to roll 2d6 + 3 could yield something like: (9,[4,2]).

    Again, the following docstring may be applied to this method:
    1
    2
    3
    4
    5
    6
    7
    
    """
    Performs a roll of all dice with their respective counts and adds the modifier.
    
    Returns:
        Tuple[int, List[int]]: A tuple where the first element is the total roll result (including the modifier), 
        and the second element is a list of the individual rolls contributing towards the total.
    """
    
    The docstring for the DiceRoll Roll() method.

Claiming the Python Programming Skill (Knowledge)

Assuming that you have succeeded with the previous steps, you have now written enough code to claim the Python Programming Skill at knowledge level. For your claim, you will need to write a section describing the purpose of the code, provide the raw code, and provide a demonstration that the code functions correctly. A video of the code working could be used in such claims, however to make it easier for this lab you can also use the following script. Simply add the code to the bottom of your file, run the file, and provide a screenshot of the terminal output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Create several dice of different face types
print('Running test script.')
print('Creating a d4, d6 and unlucky d6 (where 5 of the faces are 1).\n')
d4 = Dice(4)
d6 = Dice(6)
d6_unlucky = Dice([1,1,1,1,1,6])

# Roll the dice
print(f'1d4: \t\t{d4.roll()},\t20d4:\t\t{d4.roll_n(20)}')
print(f'1d6: \t\t{d6.roll()},\t20d6:\t\t{d6.roll_n(20)}')
print(f'1d6 (unlucky):\t{d6_unlucky.roll()},\t20d6 (unlucky):\t{d6_unlucky.roll_n(20)}\n')

print(f'Max roll achieved on d6: {max(d6.roll_n(1000))} (should be 6)')
print(f'Min roll achieved on d6: {min(d6.roll_n(1000))} (should be 1)')
print(f'Average roll achieved on d6: {sum(d6.roll_n(100000))/100000} (should be ~3.5)\n')

# Create a more complex roll
print('Creating a complex roll: 4d4 + 1d20 - 2')
mixed_roll = DiceRoll([(d4, 4),(Dice(20), 1)], -2)
roll_result, raw_rolls = (mixed_roll.roll())
print(f'4d4 + 1d20 -2 result: {roll_result} (sum{raw_rolls} - 2)')
Script to test that the code works as expected.

If successful, you should find an output like the one shown below:

Selection Disabled
> python lab4.py
Running test script
Creating a d4, d6 and unlucky d6 (where 5 of the faces are 1).

1d4:            1,      20d4:           [1, 1, 1, 1, 4, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 3, 2]
1d6:            2,      20d6:           [3, 1, 3, 5, 4, 6, 5, 2, 6, 5, 5, 6, 1, 3, 6, 5, 2, 1, 1]
1d6 (unlucky):  1,      20d6 (unlucky): [1, 1, 1, 1, 6, 6, 6, 1, 1, 6, 1, 1, 1, 1, 6, 1, 1, 6, 1]

Max roll achieved on d6: 6 (should be 6)
Min roll achieved on d6: 1 (should be 1)
Average roll achieved on d6: 3.49723 (should be ~3.0)

Creating a complex roll: 4d4 + 1d20 - 2
4d4 + 1d20 -2 result: 15 (sum[3, 1, 4, 2, 7] - 2)
Example result from running the test script.

Part 2 - Roll Analysis

In this section we will add to the DiceRoll class and create a RollAnalyser class. These classes are designed to add additional functionality to our program. Upon completion of this section, you should have a workable solution for analysing the possible outcomes for any given roll.

This section is designed to test your understanding of Python, but upon completion should be suitable for a claim at Application level (assuming you provide the necessary elements within your claim). Given that this exercise is intended for Application level claims, a specification for each class will be provided, without further step-by-step instruction. You can use the UML diagram provided in to guide your approach to this problem - however it is ultimately up to you how you go about meeting the specification.

The DiceRoll Class Additions

The DiceRoll class should provide a function that generates a collection of all possible outcomes for a given roll (e.g. 2d6 + 3), including permutations (e.g., rolling [3, 2] is considered different from rolling [2, 3]). This collection should include both the outcome result and the dice rolls that contributed towards each result so that the user can perform whatever analysis they wish.

The RollAnalyser Class

This class takes a DiceRoll instance and provides two core (somewhat related) features. The first is to produce a dictionary that shows how many different ways that a given outcome value can be achieved (e.g. rolling 2d4 could result in a final score of 4 in 3 different ways - [1,3], [3,1] and [2,2]). The key for each dictionary entry is the roll outcome total (including modifier), while the value for each entry will be the number of occurrences that can result in that outcome total.

The second feature is to plot this dictionary information as a histogram. An example plot for a 4d4 roll may be seen below in .

The histogram of possible outcomes from rolling 4 4-sided dice.
The histogram of possible outcomes from rolling 4 4-sided dice.

You can then add further features such as calculating the average of the possible roll outcomes, or the standard deviation for the possible roll outcomes.

Claiming the Python Programming Skill (Application)

As before, you can use this code to claim the Python Programming skill, this time at Application level. As I am sure you are aware, alongside the elements identified previously, this will require a critical reflection on what went well and what could be improved. The evidence for the code working could be in the form of a video, or some additional test script and screenshots.

Conclusion

By this stage in the course, you have learnt all of the core Python Programming content. The remainder of these labs will focus on the other programming skills, including software design and test and validation of code. I hope that all of the python programming labs so far have proved interesting and helpful - and welcome any feedback you may have. As we look forwards towards the software design and test and validation topics, your understanding of python will be key in demonstrating these skills.