Inventory system design and development

This tutorial describes the design and development of an inventory system for a dungeon crawler game. An object-oriented paradigm is employed, with a UML class diagram created using the draw.io tool, used to identify the main objects and their relationships. This class diagram will be used as a framework for the development of code.

For this initial iteration of the inventory design, the design consists of four classes. All item objects (represented by the Item class) include name and weight attributes. Two specialisations of the Item class are defined using inheritance. The first of these is the Weapon subclass which adds two additional attributes representing the minimum and maximum amounts of damage done by the weapon. The Armour subclass has one additional attribute that is used to represent the level of protection provided by the armour.

The Inventory class is used to keep track of all items carried by a player character. The directed link from the Inventory class to the Item class indicates that each inventory contains 0 to many items. Attributes in the class are used to represent items that are currently equipped (in use) by the player character. Methods are included that enable items to be added and removed from the inventory. The total_weight method calculates the combined weight of all items in the inventory – this can be used to determine whether a character is encumbered. The last three methods shown are used to equip items.

Item class

The Item class contains an initialiser method for creating new instances. The name of the item and its weight is passed as arguments to the initialiser method, these are then used to set the name and weight attributes. A third attribute, bonus, is used to represent a (typically magical) bonus that improves the capabilites of the item (e.g. a weapon would deal more damage, while armour would provide more protection).

As shown in the class diagram, two subclasses of the Item class are created. Subclasses inherit the attributes and methods of its parent class, but may include additional attributes and/or method, or may indeed override (re-implement) methods of the parent class. In our example the Armour and Weapon subclasses are introduced. They both have additional attributes that make the subclass as specialisation of its parent. Armour includes an additional attribute, armour_class, which models the amount of protection that the armour provides. The Weapon class includes attributes representing the minimum and maximum amount of raw damage done by the weapon.

Both of the subclasses override the initialiser method of the parent class. The initialiser method for the Armour class includes an additional argument, ac, used to the armour_class attribute. The initialiser of the subclasses also calls the initialiser method the parent class using the super method, as shown on lines 10 and 16 of the code listing below.

class Item:
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight
        self.bonus = 0

class Armour(Item):
    def __init__(self, name, weight, ac):
        super().__init__(name, weight)
        self.armour_class = ac

class Weapon(Item):
    def __init__(self, name, weight, min_damage, max_damage):
        super().__init__(name, weight)
        self.min_damage = min_damage
        self.max_damage = max_damage

Inventory class

The Inventory class keeps track of the items a player character has in their possession. The collection of items is stored in a list, which is initially empty. Additional attributes are used to model the items that are currently equipped by the player character. For this initial design, items held in the right and left hands, together with any armour worn are modelled.

A method for adding items to the inventory is defined next, followed by a method that is called to remove an item from the inventory. For the latter method a check is done to ensure that the item is in the inventory before it is dropped.

class Inventory:
    def __init__(self):
        self.contents = []
        self.right_arm = None
        self.left_arm = None
        self.armour = None

    def add_item(self, item):
        self.contents.append(item)

    def drop_item(self, item):
        if item in self.contents:
            self.contents.remove(item)

The following method calculates the total weight of the items in the inventory. This is done using a list comprehension to collect all of the weights into a list. The sum function is then applied to this to find the total weight.

    def total_weight(self):
        return sum([i.weight for i in self.contents])

The following method is used to equip a weapon in the right hand of the player character. A check if done to ensure that the item is in the inventory contents and that the item is a weapon. The method returns a Boolean-valued result indicating whether or not the method was successful.

    def equip_right_arm(self, item):
        if item in self.contents and type(item).__name__ == "Weapon":
            self.right_arm = item
            return True
        else:
            return False

The following method creates a string representation of the inventory. The equipped items are shown first, using an inline if statement to check whether or not the equipment slots contain an item. A for loop is then used to represent the complete contents of the inventory.

    def __str__(self):
        result = "Equipped items\n"
        result += "---------------\n"
        result += "Right Arm ::" + (self.right_arm.name if self.right_arm else "empty") + "\n"
        result += "Left Arm ::" + (self.left_arm.name if self.left_arm else "empty") + "\n"
        result += "Armour ::" + (self.armour.name if self.armour else "empty") + "\n"
        result += "\nOther items\n"
        result += "---------------\n"
        for i in self.contents:
            result += i.name + "\n"
        return result

Driver Code

The driver code shown below is used to test the correctness of the previous code. Instances of armour and weapon classes are created. A new inventory object is created with the item instances added to this inventory. The contents of the inventory is then printed using the string representation method which is called implicitly when the inventory object is passed as the print argument. Further tests are done to check the correctness of the other methods from the inventory class.

from item import *
from inventory import *

armour1 = Armour("Plate Armour", 120, 18)
armour2 = Armour("Leather Armour", 20, 12)

weapon1 = Weapon("Longsword", 15, 1, 8)
weapon2 = Weapon("Axe", 8, 1, 6)

# create new inventory and add items
inventory = Inventory()

inventory.add_item(armour1)
inventory.add_item(armour2)
inventory.add_item(weapon1)
inventory.add_item(weapon2)

print(inventory)

# remove item from inventory

inventory.drop_item(armour2)
print(inventory)

# check the total weight of the inventory
print(inventory.total_weight())

# equip a weapon with right arm
inventory.equip_right_arm(weapon1)
print(inventory)

# try equipping right arm with armour
inventory.equip_right_arm(armour1)

print(inventory)

Designing a turn-based combat controller

The following tutorial explains how to design a turn-based combat system for a dungeon crawler game using a finite state machine. This forms part of a bigger project aimed at developing a 2D top-down dungeon crawler using pygame. The finite state machine shows the different states that are possible during combat and the transitions that can be made between these states.

For this example we limit the design to a single player character completing a single turn of combat. The character can move and make melee or ranged attack actions.

Finite state machine

During each turn of combat player character can move and perform an action. Movement can occur before and/or after the action, provided the total movement does not exceed the character’s move distance. Because the screen needs to refresh each frame we need to also keep track other other states between movement and action. For example, when the player character elects to move, the combat controller needs to transition through several states to complete the move. This includes states for:

  • Initiating movement
  • Selecting the target location
  • Moving the character to the target location

These states are shown at the top of the following finite state machine diagram. States are shown as circles, with a title/description for each state. Transitions between the states
are shown as arcs (directed lines) that connect the states. These arcs are labelled with conditions that must be met for the transition to occur. For example, the combat controller will transition from the “pre move location” state to the “pre move” state when a valid target location is selected. If no selection is made, or the selection is invalid (e.g. too far away) then the combat controller remains in the same state.

To explain this further, we look demonstrate two main parts of the combat controller (movement and ranged attacks) in more detail using examples from the final game.

Pre action movement

In the video below, the selected player character begins in the “move or action state”. The player selects the move options (key “m”) at which point the character transitions into the “pre move location” state. In this state the player is able to select the target location that the character moves to. This is done by moving the crosshairs using mouse controls. The player then clicks on the target location – if this is a valid location then the character transitions to the “pre move” state. In this state the movement of the character occurs. When the target location is reached, the character transitions into either the “melee attack” or “ranged target” states.

Ranged attacks

A ranged attack is initiated when the player enters “r” either at the beginning of their turn, or after they have moved, at this point the character transitions to the “ranged target” state.
In this state the player moves the crosshairs to the target of their ranged attack. When they have selected a valid target and pressed the mouse button, the character transitions to the “ranged shoot” state. In this state the arrow travels towards the target enemy, moving a small distance each frame. When the arrow reaches the enemy, the character transitions to the “ranged attack” state. At this point it is determined whether the arrow hit the enemy, and if so how much damage is done. The combat controller moves the “show damage” state, either indicating a miss, or a hit with the amount of damage done.

Installing Python packages behind a firewall

The following guide shows how to install python packages behind a firewall using the trusted-host option with the pip command.
This is typically needed when a ConnectTimeoutError is received when you are attempting to install python packages.

Option 1: Adding an option to the pip command in the shell

The first option involves adding a trusted-host option each time pip is called in the shell.

pip install <package-name> pip --trusted-host pypi.org --trusted-host files.pythonhosted.org

Option 2: Adding configuration option in Pycharm

If you are installing packages from the Pycharm IDE, you can set the trusted host(s) within the Option field.

Pip trusted-host option in Pycharm
The following command is added to the Option field

pip --trusted-host pypi.org --trusted-host files.pythonhosted.org

Option 3: One-off configuration in the shell

The third option is the best to use for a long-term solution. In this case a trusted hosts are set within a configuration file which is checked whenever pip is called (either in the shell or in Pycharm).

pip config set global.trusted-host "pypi.org files.pythonhosted.org"

A screenshot of the pip config command being called is shown below.
Command for configuring pip in Powershell

The contents of the pip configuration file after the command is called is shown below, viewed using the Notepad++ editor.

Pip configuration file

Graphing in Python using Matplotlib and Pandas

Weather analysis

In this tutorial we use the Python Pandas and Matplotlib packages to analyse and visualise weather data. Time series graphs, scatter plots, histograms and box-and-whisker plots are created using matplotlib functions. Pandas functions are used to read the data file, display summary information and rename columns.

The data source used in this tutorial is from the Australian Bureau of Meteorology. We focus on the minimum and maximum daily temperatures for Adelaide, Australia, in the first three months of 2022 using data downloaded from the .

Note that the code from this tutorial is taken from a Jupyter notebook where commands are processed in cells and the results displayed. However this code can be easily adapted to run from a Python IDE such as IDLE or Pycharm. The main change required is to add a print statement to display table results, and a plt.show() command to display graphs.

Lightning cloud to cloud (aka)

Importing packages

This tutorial uses the Pandas package to read the data from the source file into a dataframe. Graphical representations of the data, including histograms, box plots and time series graphs are created using functions from the Matplotlib package.

import pandas as pd
import matplotlib.pyplot as plt

Reading the data

We begin by reading the weather data for January 2022. The following options are used:

  • the dayfirst option lets the reader know that the dates are given in Australian/European format where the days are given first (by default the reader uses the US format where the month is written first).
  • the parse_dates option indicates which columns should be converted into dates.
df1=pd.read_csv("data/IDCJDW5081.202201.csv", dayfirst=True, parse_dates=['Date'])

Next we print out the first five rows of the data, restricting the view to the first three columns, which contain the date, minimum temperature and maximum temperature.

df1[0:5][df1.columns[0:3]]
Date Minimum temperature (°C) Maximum temperature (°C)
0 2022-01-01 22.5 33.6
1 2022-01-02 19.3 30.6
2 2022-01-03 14.1 25.9
3 2022-01-04 14.2 24.4
4 2022-01-05 14.4 21.5

Renaming columns

To make it easier to refer to the minimum and maximum temperature columns we rename the label for these two columns. Calling the info function then prints a summary of the data stored in the dataframe. Notice that the minimum and maximum value columns have been renamed.

df1.rename(columns={df1.columns[1]: "Minimum", df1.columns[2] : "Maximum"}, inplace=True)
df1.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 31 entries, 0 to 30
Data columns (total 21 columns):
 #   Column                             Non-Null Count  Dtype         
---  ------                             --------------  -----         
 0   Date                               31 non-null     datetime64[ns]
 1   Minimum                            31 non-null     float64       
 2   Maximum                            31 non-null     float64       
 3   Rainfall (mm)                      31 non-null     float64       
 4   Evaporation (mm)                   0 non-null      float64       
 5   Sunshine (hours)                   0 non-null      float64       
 6   Direction of maximum wind gust     31 non-null     object        
 7   Speed of maximum wind gust (km/h)  31 non-null     int64         
 8   Time of maximum wind gust          31 non-null     object        
 9   9am Temperature (°C)               31 non-null     float64       
 10  9am relative humidity (%)          31 non-null     int64         
 11  9am cloud amount (oktas)           0 non-null      float64       
 12  9am wind direction                 31 non-null     object        
 13  9am wind speed (km/h)              31 non-null     object        
 14  9am MSL pressure (hPa)             31 non-null     float64       
 15  3pm Temperature (°C)               31 non-null     float64       
 16  3pm relative humidity (%)          31 non-null     int64         
 17  3pm cloud amount (oktas)           0 non-null      float64       
 18  3pm wind direction                 31 non-null     object        
 19  3pm wind speed (km/h)              31 non-null     int64         
 20  3pm MSL pressure (hPa)             31 non-null     float64       
dtypes: datetime64[ns](1), float64(11), int64(4), object(5)
memory usage: 5.2+ KB

Time series graphs

The first graphs that we create are time series graphs, which will display the change in minimum/maximum temperatures over time. This is created using the plot_date function. This function takes two lines of the values – the first list corresponds to the dates in the Date column, the second list corresponds to the minimum temperatures column.

The autofmt_xdate function ensures that the dates are displayed in an appropriate manner.

fig,ax=plt.subplots()
ax.set_title("Minimum temperatures for January 2022")
ax.set_xlabel("Date")
ax.set_ylabel("Temperature (°C)")
fig.autofmt_xdate()
ax.plot_date(df1[["Date"]], df1[["Minimum"]], linestyle="solid", markersize=0)
[<matplotlib.lines.Line2D at 0x1da7f8a6a30>]

The same process is used to display the maximum temperatures over time.

fig,ax=plt.subplots()
ax.set_title("Maximum temperatures for January 2022")
ax.set_xlabel("Date")
ax.set_ylabel("Temperature (°C)")
fig.autofmt_xdate()
ax.plot_date(df1[["Date"]], df1[["Maximum"]], linestyle="solid", markersize=0, color="orange")
[<matplotlib.lines.Line2D at 0x1da7db9be50>]

Scatter plots

Scatter plots can be used to compare two sets of data values. In this case create a scatter plot to compare the daily minimum and maximum temperatures.

fig,ax=plt.subplots()
ax.set_title("Comparison of maximum and minimum temperatures")
ax.set_xlabel("Minimum temperature (°C)")
ax.set_ylabel("Maximum temperature (°C)")
ax.scatter(df1[["Minimum"]], df1[["Maximum"]], marker="x")
<matplotlib.collections.PathCollection at 0x1da7dc8c160>

Combining data

Data can be read from multiple data sources and then combined into a single dataframe. For this example we combine the weather data from January 2022 with data from February 2022 and March 2022.

df2=pd.read_csv("data/IDCJDW5081.202202.csv", dayfirst=True, parse_dates=['Date'])
df3=pd.read_csv("data/IDCJDW5081.202203.csv", dayfirst=True, parse_dates=['Date'])
df2.rename(columns={df2.columns[1]: "Minimum", df2.columns[2] : "Maximum"}, inplace=True)
df3.rename(columns={df3.columns[1]: "Minimum", df3.columns[2] : "Maximum"}, inplace=True)
df=pd.concat([df1, df2, df3])
df[df.columns[0:3]]
Date Minimum Maximum
0 2022-01-01 22.5 33.6
1 2022-01-02 19.3 30.6
2 2022-01-03 14.1 25.9
3 2022-01-04 14.2 24.4
4 2022-01-05 14.4 21.5
26 2022-03-27 16.9 31.8
27 2022-03-28 16.4 27.4
28 2022-03-29 12.5 24.0
29 2022-03-30 13.6 22.9
30 2022-03-31 14.6 21.0

90 rows × 3 columns

df[["Minimum","Maximum"]].describe()
Minimum Maximum
count 90.000000 90.000000
mean 17.063333 28.116667
std 3.524344 4.399240
min 11.500000 21.000000
25% 14.625000 24.400000
50% 16.600000 27.550000
75% 19.150000 31.775000
max 27.200000 40.300000

Histograms

Histograms are used to show the distribution of continuous data. In this section we create histograms to display the distribution of minimum and maximum temperatures.

We begin by creating a histogram to display the minimum temperatures.

plt.hist(df[["Minimum"]], edgecolor="k", alpha=0.4)
plt.xlabel("Temperature (°C)")
plt.ylabel("Frequency")
plt.title("Distribution of minimum temperatures")
Text(0.5, 1.0, 'Distribution of minimum temperatures')

Whilst this graph shows the distribution of temperatures quite clearly, the automatic selection of bins (the lower and upper limits of each of the columns in the histogram) is not ideal. In particular it is difficult to see what the exact limits of the bins are. We can improve this by setting these values.

In the code below we set the bins for the minimum and maximum temperature histograms. This is done using a list comprehension.

minbins=[2*x for x in range(5, 15)]
maxbins=[2*x for x in range(10, 21)]
minbins
[10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
plt.hist(df[["Minimum"]],bins=minbins, edgecolor="k", alpha=0.4)
plt.xlabel("Temperature (°C)")
plt.ylabel("Frequency")
plt.title("Distribution of minimum temperatures")
Text(0.5, 1.0, 'Distribution of minimum temperatures')

Combining graphs

Multiple graphs can be displayed using subplots.

In the example below we display histograms for minimum and maximum temperatures, showing the graphs side by side.

  • The first argument of the subplots function defines the number of rows of graphs.
  • The second argument of the subplots function defines the number of columns of graphs.
  • The figsize option defines the size of the resulting figure containing the graphs. In this case the resultant figure will be 8 inches across, by 6 inches high.
  • The sharey option indicates that the two graphs will share
    the scale for y-axis.

The suptitle command sets a title for all graphs within the subplots.

fig, (ax1,ax2) = plt.subplots(1,2, figsize=(8,6), sharey=True)
ax1.hist(df[["Minimum"]], bins=minbins, edgecolor='k', alpha=0.4)
ax1.set_xlabel("Minimum temperature")
ax2.hist(df[["Maximum"]], bins=maxbins, edgecolor='k', alpha=0.4, color="red")
ax2.set_xlabel("Maximum temperature")
ax1.set_ylabel("Frequency")
plt.suptitle("Distribution of minimum and maximum temperatures")
Text(0.5, 0.98, 'Distribution of minimum and maximum temperatures')

Box and whisker plots

Box and whisker plots are created using the boxplot function. In the following example we create box plot showing the distribution for minimum temperatures. The following options are used:

  • vert determines whether or not to display the box plots vertically. In this case we set the option to false, meaning the boxplots will be displayed horizontally.
  • labels takes a list of strings. These strings are used for the boxplot labels.

Outliers are displayed as a circle beyond the whiskers. In this case there is one outlier corresponding to the minimum temperature of 27.2°C.

fig, ax= plt.subplots()
ax.boxplot(df["Minimum"], vert=False, labels=["Minimum"])
ax.set_title("Minimum temperature distribution")
ax.set_xlabel("Temperature")
Text(0.5, 0, 'Temperature')

Parallel boxplots

Parallel boxplots are created by passing multiple lists of values to the first input of the boxplot function.

fig, ax= plt.subplots()
ax.boxplot(df[["Minimum", "Maximum"]], vert=False, labels=["Minimum", "Maximum"])
ax.set_title("Comparison of temperature distributions")
ax.set_xlabel("Temperature")
Text(0.5, 0, 'Temperature')

Hiding outliers

Outliers can be hidden in boxplots by setting the showfliers option to false.

fig, ax= plt.subplots()
ax.boxplot(df[["Minimum", "Maximum"]], vert=False, labels=["Minimum", "Maximum"], showfliers=False)
ax.set_title("Comparison of temperature distributions")
ax.set_xlabel("Temperature")
Text(0.5, 0, 'Temperature')

Pandas Tutorial: Grouping data

In this tutorial we look at how data can be grouped (for example by year) and how the resulting grouped data can be viewed and analysed.


The data source for this tutorial is the Top 1000 Movies by IMDB Rating.
The data will be grouped by year, genre and actor in this tutorial to provide a variety of insights into the data.

Reading the data

import pandas as pd

We begin by reading data from IMDB into the movies_df dataframe. We then show the first 5 rows in this dataframe.

movies_df = pd.read_csv("data/IMDB-Movie-Data.csv", index_col="Rank")
movies_df.head(5)
Title Genre Description Director Actors Year Runtime (Minutes) Rating Votes Revenue (Millions) Metascore
Rank
1 Guardians of the Galaxy Action,Adventure,Sci-Fi A group of intergalactic criminals are forced … James Gunn Chris Pratt, Vin Diesel, Bradley Cooper, Zoe S… 2014 121 8.1 757074 333.13 76.0
2 Prometheus Adventure,Mystery,Sci-Fi Following clues to the origin of mankind, a te… Ridley Scott Noomi Rapace, Logan Marshall-Green, Michael Fa… 2012 124 7.0 485820 126.46 65.0
3 Split Horror,Thriller Three girls are kidnapped by a man with a diag… M. Night Shyamalan James McAvoy, Anya Taylor-Joy, Haley Lu Richar… 2016 117 7.3 157606 138.12 62.0
4 Sing Animation,Comedy,Family In a city of humanoid animals, a hustling thea… Christophe Lourdelet Matthew McConaughey,Reese Witherspoon, Seth Ma… 2016 108 7.2 60545 270.32 59.0
5 Suicide Squad Action,Adventure,Fantasy A secret government agency recruits some of th… David Ayer Will Smith, Jared Leto, Margot Robbie, Viola D… 2016 123 6.2 393727 325.02 40.0

Next, for convenience, we rename all of the column titles so that they are all lowercase.

movies_df.columns = [col.lower() for col in movies_df]

Grouping by year

The data is grouped by year. This creates a grouped dataframe – this is essentially a collection of dataframes that are index by the year.

grouped=movies_df.groupby(["year"])

Inspecting a particular year

If wanted to view the information for a particular group (in this case for a particular year) we can use the get_group function. The code below will get the dataframe for 2009 and show the first 5 entries.

grouped.get_group(2009).head(5)
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
Rank
78 Inglourious Basterds Adventure,Drama,War In Nazi-occupied France during World War II, a… Quentin Tarantino Brad Pitt, Diane Kruger, Eli Roth,Mélanie Laurent 2009 153 8.3 959065 120.52 69.0
88 Avatar Action,Adventure,Fantasy A paraplegic marine dispatched to the moon Pan… James Cameron Sam Worthington, Zoe Saldana, Sigourney Weaver… 2009 162 7.8 935408 760.51 83.0
141 Star Trek Action,Adventure,Sci-Fi The brash James T. Kirk tries to live up to hi… J.J. Abrams Chris Pine, Zachary Quinto, Simon Pegg, Leonar… 2009 127 8.0 526324 257.70 82.0
148 Watchmen Action,Drama,Mystery In 1985 where former superheroes exist, the mu… Zack Snyder Jackie Earle Haley, Patrick Wilson, Carla Gugi… 2009 162 7.6 410249 107.50 56.0
252 Kynodontas Drama,Thriller Three teenagers live isolated, without leaving… Yorgos Lanthimos Christos Stergioglou, Michele Valley, Angeliki… 2009 94 7.3 50946 0.11 73.0

Sorting values

In the following code we use the sort_values function to sort the movies released in 2009 into from best to worst (using ascending=False) and from worst to best (using ascending=True or omitting this option).

The first three entries from each of these sorted dataframes are displayed.

grouped.get_group(2009).sort_values(by="rating", ascending=False).head(3)
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
Rank
431 3 Idiots Comedy,Drama Two friends are searching for their long lost … Rajkumar Hirani Aamir Khan, Madhavan, Mona Singh, Sharman Joshi 2009 170 8.4 238789 6.52 67.0
78 Inglourious Basterds Adventure,Drama,War In Nazi-occupied France during World War II, a… Quentin Tarantino Brad Pitt, Diane Kruger, Eli Roth,Mélanie Laurent 2009 153 8.3 959065 120.52 69.0
500 Up Animation,Adventure,Comedy Seventy-eight year old Carl Fredricksen travel… Pete Docter Edward Asner, Jordan Nagai, John Ratzenberger,… 2009 96 8.3 722203 292.98 88.0
grouped.get_group(2009).sort_values(by="rating").head(3)
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
Rank
872 Dragonball Evolution Action,Adventure,Fantasy The young warrior Son Goku sets out on a quest… James Wong Justin Chatwin, James Marsters, Yun-Fat Chow, … 2009 85 2.7 59512 9.35 45.0
937 The Human Centipede (First Sequence) Horror A mad scientist kidnaps and mutilates a trio o… Tom Six Dieter Laser, Ashley C. Williams, Ashlynn Yenn… 2009 92 4.4 60655 0.18 33.0
349 Jennifer’s Body Comedy,Horror A newly possessed high school cheerleader turn… Karyn Kusama Megan Fox, Amanda Seyfried, Adam Brody, Johnny… 2009 102 5.1 96617 16.20 47.0
grouped.get_group(2009).sort_values(by="metascore", ascending=False).head(3)
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
Rank
500 Up Animation,Adventure,Comedy Seventy-eight year old Carl Fredricksen travel… Pete Docter Edward Asner, Jordan Nagai, John Ratzenberger,… 2009 96 8.3 722203 292.98 88.0
815 Fantastic Mr. Fox Animation,Adventure,Comedy An urbane fox cannot resist returning to his f… Wes Anderson George Clooney, Meryl Streep, Bill Murray, Jas… 2009 87 7.8 149779 21.00 83.0
88 Avatar Action,Adventure,Fantasy A paraplegic marine dispatched to the moon Pan… James Cameron Sam Worthington, Zoe Saldana, Sigourney Weaver… 2009 162 7.8 935408 760.51 83.0
grouped.get_group(2009).sort_values(by="metascore", ascending=True).head(3)
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
Rank
398 Couples Retreat Comedy A comedy centered around four couples who sett… Peter Billingsley Vince Vaughn, Malin Akerman, Jon Favreau, Jaso… 2009 113 5.5 86417 109.18 23.0
900 Bride Wars Comedy,Romance Two best friends become rivals when they sched… Gary Winick Kate Hudson, Anne Hathaway, Candice Bergen, Br… 2009 89 5.5 83976 58.72 24.0
453 Pandorum Action,Horror,Mystery A pair of crew members aboard a spaceship wake… Christian Alvart Dennis Quaid, Ben Foster, Cam Gigandet, Antje … 2009 108 6.8 126656 10.33 28.0

Summary information

Summary calculations can be done on the grouped data. For example we can calculate the mean rating for each year using the mean function.

grouped[["rating"]].mean()
rating
year
2006 7.125000
2007 7.133962
2008 6.784615
2009 6.960784
2010 6.826667
2011 6.838095
2012 6.925000
2013 6.812088
2014 6.837755
2015 6.602362
2016 6.436700

Similarly, the maximum metascore value for each year can be calculated using the max function. Whilst this tables shows us what the top metascore was for each year, it does not show the associated movie title (or any other associated information). Other aggregate functions also have a similar limitation. We will show in the next section how to create a more detailed summary of the best (or worst) movies.

grouped[["metascore"]].max()
metascore
year
2006 98.0
2007 96.0
2008 94.0
2009 88.0
2010 95.0
2011 94.0
2012 95.0
2013 96.0
2014 100.0
2015 95.0
2016 99.0

Top rated movie for each year

In the following code we create a new dataframe showing the top rated movie from each year from 2006 up until 2016. For each entry we show the year of release, movie title, rating and (for comparison) the metascore.

To create this dataframe we begin by creating a new dictionary and add entries for the four columns of interest noted above. Then for each group in the grouped data frame we get the row with the highest rating. This is done by sorting the group on the rating in descending order, then getting the first row in the sorted group.

data = {}
data["year"]=[]
data["title"]=[]
data["rating"]=[]
data["metascore"]=[]
for year, group in grouped:
    best=group.sort_values(by="rating", ascending=False).iloc[0]
    data["year"].append(best['year'])
    data["title"].append(best['title'])
    data["rating"].append(best['rating'])
    data["metascore"].append(best['metascore'])
best_df=pd.DataFrame(data)
best_df
year title rating metascore
0 2006 The Prestige 8.5 66.0
1 2007 Taare Zameen Par 8.5 42.0
2 2008 The Dark Knight 9.0 82.0
3 2009 3 Idiots 8.4 67.0
4 2010 Inception 8.8 74.0
5 2011 The Intouchables 8.6 57.0
6 2012 The Dark Knight Rises 8.5 78.0
7 2013 The Wolf of Wall Street 8.2 75.0
8 2014 Interstellar 8.6 74.0
9 2015 Bahubali: The Beginning 8.3 NaN
10 2016 Dangal 8.8 NaN

Best movies by metascore

For comparison we also create a dataframe to store the top movie (ranked by metascore) for each year.

data = {}
data["year"]=[]
data["title"]=[]
data["rating"]=[]
data["metascore"]=[]
for year, group in grouped:
    best=group.sort_values(by="metascore", ascending=False).iloc[0]
    data["year"].append(best['year'])
    data["title"].append(best['title'])
    data["rating"].append(best['rating'])
    data["metascore"].append(best['metascore'])
best_df=pd.DataFrame(data)
best_df
year title rating metascore
0 2006 Pan’s Labyrinth 8.2 98.0
1 2007 Ratatouille 8.0 96.0
2 2008 The Hurt Locker 7.6 94.0
3 2009 Up 8.3 88.0
4 2010 The Social Network 7.7 95.0
5 2011 Megan Is Missing 4.9 94.0
6 2012 Zero Dark Thirty 7.4 95.0
7 2013 Gravity 7.8 96.0
8 2014 Boyhood 7.9 100.0
9 2015 Carol 7.2 95.0
10 2016 Moonlight 7.5 99.0

Summarising the distributions of ratings using a boxplot

In the following code we show how boxplots can be created for each year of release and shown on the same graph for easy comparison of movie ratings across each year.

import matplotlib.pyplot as plt
data = []
years = []
for year, group in grouped:
    data.append(group["rating"])
    years.append(year)
plt.boxplot(data, labels=years, vert=False)
plt.xlabel("Rating (0..10)")
plt.ylabel("Year")
Text(0, 0.5, 'Year')

Grouping by genre

Next we group the movies by genre. However we note that in the original datafile many of the movies had multiple comma separated genres. The following code creates a new data frame with movies that had multiple genres split across multiple rows with one genre list per row.

data={}
for c in movies_df.columns:
    data=[]
for i in range(0, movies_df.shape[0]):
    genre_list=movies_df.iloc[i]['genre'].split(',')
    
    for g in genre_list:
        data["genre"].append(g)
        for c in movies_df.columns:
            if c != "genre":
                data.append(movies_df.iloc[i])
movies_genre_df = pd.DataFrame(data)
movies_genre_df
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
0 Guardians of the Galaxy Action A group of intergalactic criminals are forced … James Gunn Chris Pratt, Vin Diesel, Bradley Cooper, Zoe S… 2014 121 8.1 757074 333.13 76.0
1 Guardians of the Galaxy Adventure A group of intergalactic criminals are forced … James Gunn Chris Pratt, Vin Diesel, Bradley Cooper, Zoe S… 2014 121 8.1 757074 333.13 76.0
2 Guardians of the Galaxy Sci-Fi A group of intergalactic criminals are forced … James Gunn Chris Pratt, Vin Diesel, Bradley Cooper, Zoe S… 2014 121 8.1 757074 333.13 76.0
3 Prometheus Adventure Following clues to the origin of mankind, a te… Ridley Scott Noomi Rapace, Logan Marshall-Green, Michael Fa… 2012 124 7.0 485820 126.46 65.0
4 Prometheus Mystery Following clues to the origin of mankind, a te… Ridley Scott Noomi Rapace, Logan Marshall-Green, Michael Fa… 2012 124 7.0 485820 126.46 65.0
2550 Search Party Adventure A pair of friends embark on a mission to reuni… Scot Armstrong Adam Pally, T.J. Miller, Thomas Middleditch,Sh… 2014 93 5.6 4881 NaN 22.0
2551 Search Party Comedy A pair of friends embark on a mission to reuni… Scot Armstrong Adam Pally, T.J. Miller, Thomas Middleditch,Sh… 2014 93 5.6 4881 NaN 22.0
2552 Nine Lives Comedy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Kevin Spacey, Jennifer Garner, Robbie Amell,Ch… 2016 87 5.3 12435 19.64 11.0
2553 Nine Lives Family A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Kevin Spacey, Jennifer Garner, Robbie Amell,Ch… 2016 87 5.3 12435 19.64 11.0
2554 Nine Lives Fantasy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Kevin Spacey, Jennifer Garner, Robbie Amell,Ch… 2016 87 5.3 12435 19.64 11.0

2555 rows × 11 columns

movies_genre_df.groupby("genre")["rating"].mean().sort_values(ascending=False)
genre
War          7.353846
Animation    7.324490
Biography    7.290123
History      7.127586
Music        7.075000
Sport        7.011111
Drama        6.953801
Musical      6.940000
Mystery      6.886792
Crime        6.786667
Adventure    6.772201
Western      6.771429
Sci-Fi       6.716667
Romance      6.685816
Family       6.684314
Comedy       6.647670
Action       6.614521
Thriller     6.593333
Fantasy      6.548515
Horror       6.089916
Name: rating, dtype: float64
movies_genre_df.groupby("genre")["rating"].count().sort_values(ascending=False)
genre
Drama        513
Action       303
Comedy       279
Adventure    259
Thriller     195
Crime        150
Romance      141
Sci-Fi       120
Horror       119
Mystery      106
Fantasy      101
Biography     81
Family        51
Animation     49
History       29
Sport         18
Music         16
War           13
Western        7
Musical        5
Name: rating, dtype: int64

Grouping by actor

In the following code a new dataframe is created containing one actor per row. Before the individual actor names are added to the dataframe any leading spaces at the beginning of the line are removed using the lstrip function.

data={}
for c in movies_df.columns:
    data=[]
for i in range(0, movies_df.shape[0]):
    actor_list=movies_df.iloc[i]['actors'].split(',')
    
    for a in actor_list:
        data["actors"].append(a.lstrip())
        for c in movies_df.columns:
            if c != "actors":
                data.append(movies_df.iloc[i])
movies_actor_df = pd.DataFrame(data)
movies_actor_df
title genre description director actors year runtime (minutes) rating votes revenue (millions) metascore
0 Guardians of the Galaxy Action,Adventure,Sci-Fi A group of intergalactic criminals are forced … James Gunn Chris Pratt 2014 121 8.1 757074 333.13 76.0
1 Guardians of the Galaxy Action,Adventure,Sci-Fi A group of intergalactic criminals are forced … James Gunn Vin Diesel 2014 121 8.1 757074 333.13 76.0
2 Guardians of the Galaxy Action,Adventure,Sci-Fi A group of intergalactic criminals are forced … James Gunn Bradley Cooper 2014 121 8.1 757074 333.13 76.0
3 Guardians of the Galaxy Action,Adventure,Sci-Fi A group of intergalactic criminals are forced … James Gunn Zoe Saldana 2014 121 8.1 757074 333.13 76.0
4 Prometheus Adventure,Mystery,Sci-Fi Following clues to the origin of mankind, a te… Ridley Scott Noomi Rapace 2012 124 7.0 485820 126.46 65.0
3994 Search Party Adventure,Comedy A pair of friends embark on a mission to reuni… Scot Armstrong Shannon Woodward 2014 93 5.6 4881 NaN 22.0
3995 Nine Lives Comedy,Family,Fantasy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Kevin Spacey 2016 87 5.3 12435 19.64 11.0
3996 Nine Lives Comedy,Family,Fantasy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Jennifer Garner 2016 87 5.3 12435 19.64 11.0
3997 Nine Lives Comedy,Family,Fantasy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Robbie Amell 2016 87 5.3 12435 19.64 11.0
3998 Nine Lives Comedy,Family,Fantasy A stuffy businessman finds himself trapped ins… Barry Sonnenfeld Cheryl Hines 2016 87 5.3 12435 19.64 11.0

3999 rows × 11 columns

Sorting by number of movie appearances

The count method is used to determine how many movies each actor has appeared in. The list is sorted from highest to lowest.

movies_actor_df.groupby("actors")["title"].count().sort_values(ascending=False)
actors
Mark Wahlberg         15
Hugh Jackman          14
Brad Pitt             13
Christian Bale        13
Scarlett Johansson    12
                      ..
Jackie Earle Haley     1
Jackie Chan            1
Jacki Weaver           1
Jack Taylor            1
Óscar Jaenada          1
Name: title, Length: 1985, dtype: int64

Ranking actors

Next we rank actors based on the average metascore of all movies they have appeared in.

In this case the list is skewed by actors who have only appeared in a single movie.

grouped_by_actors =movies_actor_df.groupby("actors")
grouped_by_actors["metascore"].mean().sort_values(ascending=False)
actors
Ellar Coltrane       100.0
Elijah Smith         100.0
Patricia Arquette    100.0
Shariff Earp          99.0
Mahershala Ali        99.0
                     ...  
Tom Hughes             NaN
Val Kilmer             NaN
Vanessa Ferlito        NaN
Zoë Bell               NaN
Émilie Leclerc         NaN
Name: metascore, Length: 1985, dtype: float64

Adding a minimum number of movies filter

To ensure that the ranking of actors is not skewed by those who have only appeared in a small number of movies, we filter the data to only include actors who have appeared in more than 6 movies.

The high flyers
filtered_grouped_by_actors= grouped_by_actors.filter(lambda x: x['rating'].count() > 6).groupby("actors")
filtered_grouped_by_actors["metascore"].mean().sort_values(ascending=False).head(10)
actors
Jeremy Renner           75.625000
George Clooney          73.142857
Amy Adams               72.625000
Brad Pitt               72.076923
Joel Edgerton           71.625000
Rooney Mara             70.428571
Ryan Gosling            70.400000
Joseph Gordon-Levitt    70.250000
Matt Damon              70.250000
Jonah Hill              70.111111
Name: metascore, dtype: float64
The stinkers
filtered_grouped_by_actors["metascore"].mean().sort_values(ascending=True).head(10)
actors
Adam Sandler        30.888889
Jennifer Aniston    40.625000
Ryan Reynolds       44.714286
Zac Efron           45.500000
Will Smith          47.111111
Teresa Palmer       47.333333
Liam Neeson         47.600000
Jason Sudeikis      48.833333
Gerard Butler       49.600000
Robert De Niro      50.125000
Name: metascore, dtype: float64

GUI Development Basics in Python

In this tutorial we present the development of a GUI-based application in Python using tkinter. A variety of widgets are covered including: labels, entries, buttons, spinboxes, combo boxes, radio buttons, check buttons, scale and calendars. The focus for this tutorial is on constructing the view layer (how the application appears) with minimal development of the underlying logic.

Application Design

Before we write any code we need to design the layout for our application.
For this example we will divide our application into three main parts. The left frame will contain the entry (input) components of our application. The right frame will include additional configuration components in the form of radio buttons and checkbuttons.
The button frame will contain command buttons. An overview of the layout is shown below.

Step 1: The top-level window

We begin by creating the top-level window (root) for the application. Lines 1 and 2 of the code import the tkinter package, that contains all of the basic interface widgets, together with the ttk extension package, which includes enhanced and additional widgets.
The main control window of the application is created on line 5 of the code. The last line of the code (line 17) initialises the control loop for the GUI application – this line should always occur at the end of the code listing.

Lines 7-15 create frames for the three main parts of the application interface as explained above. To ensure that the button frame spans all of the application window we use the columnspan option.

from tkinter import *
from tkinter.ttk import *

# create the main application window
root = Tk()

# create a frame for a variety of entry and scale widgets
left_frame = Frame(root)
left_frame.grid(row=0, column=0)

right_frame = Frame(root)
right_frame.grid(row=0, column=1)

button_frame = Frame(root)
button_frame.grid(row=1, column=0, columnspan=2)

root.mainloop()

Step 2: Labels and Entries

In the next step we add two pairs of labels and entry widgets that will allow the user to enter their first name and last name.
These widgets are packed using the grid packing system within the left frame as shown in the diagram below. The grid consists of two rows and two columns, with the numbering starting at 0 for both the row and column.

Labels are added to the application using the Label widget as seen in line 2 below. The font command is used to change the appearance of the text, using a size larger than the default and representing the text using boldface. The Entry widget is used to get input from the user, in this case to get their first and last names. Both of the Entry widgets are associated with a text variable. When text is entered in the entry box the value of the text variable is updated automatically. Both of the text variables are mapped string variables.

The following code is added to the previous code after the creation of the left, right and button frames and before the final line of code.

# label and entry widgets used to enter information. Values linked to string variable.
first_name_label = Label(left_frame, text="First Name", font=("-size", 15, "-weight", "bold"))
first_name_label.grid(row=0, column=0, padx=5, pady=5)
first_name = StringVar()
first_name_entry = Entry(left_frame, textvariable=first_name)
first_name_entry.grid(row=0, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

last_name_label = Label(left_frame, text="Last Name", font=("-size", 15, "-weight", "bold"))
last_name_label.grid(row=1, column=0, ipady=5)
last_name = StringVar()
last_name_entry = Entry(left_frame, textvariable=last_name)
last_name_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

The resulting GUI after this code is run is shown below.

Step 3: Adding a command button

In this step we add a command button that will display the details that the user has entered using a message box.
The button is created using a Button widget. This is placed in the button frame that appears at the bottom of the screen. The Button widget command takes two parameters in this case. The first, text, sets the text that will be displayed in the button. The second, command, configures the function that will be called when the button is pressed.

print_button = Button(button_frame, text="Print details", command=print_details)
print_button.grid(row=0, column=0, padx=5, pady=10)

When the button is pressed a message box will be displayed showing the first and last name that has been entered in the two Entry boxes. To create a messagebox object an additional import command must be added to the top of the code file (just below the existing import commands).

The function print_details is defined which will be called when the button is pressed. The function prints the first and last names to the terminal output – this line of code is used for code development and debugging purposes and could be removed from the final version. Line 5 of the code below creates a new messagebox which will print the user’s first and last names in a pop-up window.

from tkinter import messagebox

def print_details():
    print(first_name.get(), last_name.get())
    messagebox.showinfo("Details", "hello " + str(first_name.get()) + " " + str(last_name.get()))

Step 4: Adding a spinbox

The next step involves adding a spinbox entry, which will allow users to enter a numeric value (their age) from a fixed selection of values. The spinbox entry is linked to an integer-valued variable by the textvariable option. Two further options also specify the minimum and maximum values that can be selected from. Note the use of an underscore at the from_ option – this is avoid a name class with the builtin from keyword.

age_label = Label(left_frame, text="Age", justify="center", font=("-size", 15, "-weight", "bold"))
age_label.grid(row=2, column=0, ipady=5)
age = IntVar()
age_entry = Spinbox(left_frame, textvariable=age, from_=0, to=100)
age_entry.grid(row=2, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

A screenshot of the resulting spin box is shown below.

Step 5: Adding a Combo box

Step 5 involves adding a Combo box to the application, which allows the user to select from a predefined list of choices. In this case the Combo box provides a list of colours that the user can choose from to pick their favourite colour.
The Combobox widget is linked to the colour string variable using the textvariable option. The values option links to the list used to store the available colours.

colour_label = Label(left_frame, text="Favourite Colour", justify="right", font=("-size", 15, "-weight", "bold"))
colour_label.grid(row=3, column=0, ipady=5)
colour = StringVar()
colour_list = [
    'green',
    'blue',
    'red',
    'yellow',
    'orange',
    'purple',
    'pink'
]
colour_entry = Combobox(left_frame, textvariable=colour, values=colour_list)
colour_entry.grid(row=3, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

The screenshot below shows the combo box being used to select the user’s favourite colour. When clicked on, a menu drops down from the entry boxv allowing the user to select from the available colours.

Step 6: Date entry

The DateEntry widget is used in this step to enter the user’s birthday. To access the DateEntry widget – together with its associated calendar widget, we need to import the tkcalendar package. This import code should be placed near the top of the file after the other import commands.

from tkcalendar import Calendar, DateEntry

The locale argument is configured so that the dates are displayed using Australian date formatting (en_AU). The year argument is used to set the year that the DateEntry will start at when it is first opened. Similar arguments are included for setting the initial day and the month. The date_pattern argument is used to specify the format that the dates will be displayed in. In this case the day and month will both be displayed using two digits, while the year will be displayed using four digits.

dob_label = Label(left_frame, text="Date of birth", justify="right", font=("-size", 15, "-weight", "bold"))
dob_label.grid(row=4, column=0)
dob_entry = DateEntry(left_frame, width=12, locale="en_AU", background='darkblue', foreground='white', borderwidth=2, year=2000, month=1, day=1, date_pattern = 'dd/mm/y')
dob_entry.grid(row=4, column=1, padx=5, sticky=tkinter.E)

The resulting date entry box is shown below.

Step 7: Using a scale widget

For step 7 we use a scale widget to introduce a slider that can be used to select a test score between 0 and 100. Like the Spinbox widget, the Scale widget includes options for setting the minimum and maximum values for the slide. The variable argument links the value of the slider to an integer valued variable. The command argument links the slider to a function that is called whenever the slider is moved. A Label widget is used to display the value of the test score – this will be updated when the slider is moved.

test_score_label = Label(left_frame, text="Test score", font=("-size", 15, "-weight", "bold"))
test_score_label.grid(row=5, column=0)
testScore = IntVar()
test_score_scale = Scale(left_frame, from_=0, to=100, orient=HORIZONTAL, variable=testScore, command=update_score_display)
test_score_scale.grid(row=5, column=1)
test_score_display = Label(left_frame, text="Score is 0")
test_score_display.grid(row=6, column=1)

The function update_score_display is called whenever the slider is moved. This function updates the text displayed in the test score display label by getting the current value of the test score variable. The function, as shown below, is placed after any import commands with other function definitions.

def update_score_display(event):
    test_score_display.config(text="Score is "+ str(testScore.get()))

A screenshot of the resulting slider is shown below.

Step 8: Radio buttons

Radio buttons allow the user to make a single selection from a predefined list of choices. The selection is made by choosing the appropriate button. For our application users will select their favourite pet by clicking the appropriate button.

The radio buttons are grouped together using a LabelFrame, which as its name suggests is a frame that includes a text label.
A string-valued variable is used to store the value of the favourite pet – this variable is then linked to each of the radio buttons.

fave_animal = StringVar()
pet_frame = LabelFrame(right_frame, text="Favourite Pet")
pet_frame.grid(row=0, column=0, padx=10)
dog_lover_label= Label(pet_frame, text="Dog Lover")
dog_lover_label.grid(row=0, column=0)
dog_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="dog")
dog_lover_select.grid(row=0, column=1)
cat_lover_label = Label(pet_frame, text="Cat Lover")
cat_lover_label.grid(row=1, column=0)
cat_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="cat")
cat_lover_select.grid(row=1, column=1)
rabbit_lover_label = Label(pet_frame, text="Rabbit Lover")
rabbit_lover_label.grid(row=2, column=0)
rabbit_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="rabbit")
rabbit_lover_select.grid(row=2, column=1)

The resulting label frame containing three labelled radio buttons is shown below.

Step 9: Adding check buttons

In this step check buttons are used to allow the user to select zero or more showbags. The check buttons are grouped together in a labelled frame and placed below the radio buttons frame. For each option there is an associated Boolean-valued variable that switches between true and false, with the default value in this case being false. Checkbutton widgets include a text label and a variable option which associates with the corresponding Boolean-valued variable.

showbag_frame = LabelFrame(right_frame, text="Showbag Order")
showbag_frame.grid(row=1, column=0, padx=10, pady=10)

warheads = BooleanVar()
warheads_select = Checkbutton(showbag_frame, text="Warheads", variable=warheads)
warheads_select.grid(row=0, column=0, sticky=W)

freddo = BooleanVar()
freddo_select = Checkbutton(showbag_frame, text="Freddo", variable=freddo)
freddo_select.grid(row=1, column=0, sticky=W)

blues_clues = BooleanVar()
blues_clues_select = Checkbutton(showbag_frame, text="Blue's Clues", variable=blues_clues)
blues_clues_select.grid(row=2, column=0, sticky=W)

Final application

A screenshot of the final application is shown below.

Below is a full code listing.

import tkinter as tk
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
from tkcalendar import Calendar, DateEntry


def print_details():
    print(first_name.get(), last_name.get())
    messagebox.showinfo("Details", "hello " + str(first_name.get()) + " " + str(last_name.get()))

def update_score_display(event):
    test_score_display.config(text="Score is "+ str(testScore.get()))
# create the main application window
root = Tk()

# create a frame for a variety of entry and scale widgets
left_frame = Frame(root)
left_frame.grid(row=0, column=0)

right_frame = Frame(root)
right_frame.grid(row=0, column=1, sticky=N)

button_frame = Frame(root)
button_frame.grid(row=1, column=0, columnspan=2)
# label and entry widgets used to enter information. Values linked to string variable.
first_name_label = Label(left_frame, text="First Name", justify="center", font=("-size", 15, "-weight", "bold"))
first_name_label.grid(row=0, column=0, padx=5, pady=5)
first_name = StringVar()
first_name_entry = Entry(left_frame, textvariable=first_name)
first_name_entry.grid(row=0, column=1, padx=5, pady=(0, 10), sticky=tk.E)

last_name_label = Label(left_frame, text="Last Name", justify="center", font=("-size", 15, "-weight", "bold"))
last_name_label.grid(row=1, column=0, ipady=5)
last_name = StringVar()
last_name_entry = Entry(left_frame, textvariable=last_name)
last_name_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky=tk.E)


print_button = Button(button_frame, text="Print details", command=print_details)
print_button.grid(row=0, column=0, padx=5, pady=10)

age_label = Label(left_frame, text="Age", justify="center", font=("-size", 15, "-weight", "bold"))
age_label.grid(row=2, column=0, ipady=5)
age = IntVar()
age_entry = Spinbox(left_frame, textvariable=age, from_=0, to=100)
age_entry.grid(row=2, column=1, padx=5, pady=(0, 10), sticky=tk.E)

colour_label = Label(left_frame, text="Favourite Colour", justify="right", font=("-size", 15, "-weight", "bold"))
colour_label.grid(row=3, column=0, ipady=5)
colour = StringVar()
colour_list = [
    'green',
    'blue',
    'red',
    'yellow',
    'orange',
    'purple',
    'pink'
]
colour_entry = Combobox(left_frame, textvariable=colour, values=colour_list)
colour_entry.grid(row=3, column=1, padx=5, pady=(0, 10), sticky=tk.E)

dob_label = Label(left_frame, text="Date of birth", justify="right", font=("-size", 15, "-weight", "bold"))
dob_label.grid(row=4, column=0)
dob_entry = DateEntry(left_frame, width=12, locale="en_AU", background='darkblue', foreground='white', borderwidth=2, year=2000)

dob_entry.grid(row=4, column=1, padx=5, sticky=tk.E)

test_score_label = Label(left_frame, text="Test score", font=("-size", 15, "-weight", "bold"))
test_score_label.grid(row=5, column=0)
testScore = IntVar()
test_score_scale = Scale(left_frame, from_=0, to=100, orient=HORIZONTAL, variable=testScore, command=update_score_display)
test_score_scale.grid(row=5, column=1)
test_score_display = Label(left_frame, text="Score is 0")
test_score_display.grid(row=6, column=1)

s = Style()
s.configure('TLabelframe.Label', font=("-size", 15, "-weight", "bold"))

fave_animal = StringVar()
pet_frame = LabelFrame(right_frame, text="Favourite Pet")
pet_frame.grid(row=0, column=0, padx=10)
dog_lover_label= Label(pet_frame, text="Dog Lover")
dog_lover_label.grid(row=0, column=0)
dog_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="dog")
dog_lover_select.grid(row=0, column=1)
cat_lover_label = Label(pet_frame, text="Cat Lover")
cat_lover_label.grid(row=1, column=0)
cat_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="cat")
cat_lover_select.grid(row=1, column=1)
rabbit_lover_label = Label(pet_frame, text="Rabbit Lover")
rabbit_lover_label.grid(row=2, column=0)
rabbit_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="rabbit")
rabbit_lover_select.grid(row=2, column=1)

showbag_frame = LabelFrame(right_frame, text="Showbag Order")
showbag_frame.grid(row=1, column=0, padx=10, pady=10)

warheads = BooleanVar()
warheads_select = Checkbutton(showbag_frame, text="Warheads", variable=warheads)
warheads_select.grid(row=0, column=0, sticky=W)

freddo = BooleanVar()
freddo_select = Checkbutton(showbag_frame, text="Freddo", variable=freddo)
freddo_select.grid(row=1, column=0, sticky=W)

blues_clues = BooleanVar()
blues_clues_select = Checkbutton(showbag_frame, text="Blue's Clues", variable=blues_clues)
blues_clues_select.grid(row=2, column=0, sticky=W)

root.mainloop()

Asteroids Simulator

This article explains how to develop a simulator of part of an Asteroids game in Geogebra using parametric curves based on position vectors. Note that this simulator is intended for exploring mathematical models and as such many of the details of a full Asteroids game have been abstracted.

The animated gif image below is an example of a simple simulator developed in Geogebra. The large circle represents an Asteroid, while the smaller circle (dot) represents a missile that has been fired. The simulator demonstrates whether or not the missile hits the Asteroid and can be used to determine the minimum distance between the centre of the Asteroid and the missile.


Before we create the simulator, we provide a brief description of the main components of the Geogebra tool.
The screenshot below shows Geogebra Classic 6. On the left-hand side is the Algebra window. This is where we will input components of the mathematical model, including curves and points. The middle and main part of the display is the Graphic window, this is where the asteroids model will be displayed. The toolbar at the top of the screen provide access to the main geometric tools – in this article we some of these tools to create a slider, create a circle to represent an asteroid and add text showing the distance between two points.


The highlighted settings button allows for the configuration of elements on the Graph window. Clicking on the background allows us to configure the background window.

Step 1: Screen dimensions

We begin by setting the screen dimensions. We assume that the game is played on a 4:3 aspect ratio with a resolution of 800 pixels wide and 600 pixels high. In Geogebra we will let 1 unit represent 1 pixel in the game. As such the range of x values will be between 0 and 800, while the range of y values will be between 0 and 600. Settings for this in Geogebra are shown below. Note that maximum x and y values go slightly beyond the range. Note also that we have a 1:1 ratio between the x and y scales. To access the configuration options for the background, click on the settings button.

Step 2: Representing time

Time (in seconds) will be represented using a slider. For our example time will range from 0 to 10 seconds, with increments of 0.01 seconds. This slider is created using the slider tool.

The configuration options are shown in the following image.


The width of the slider is increased from the default of 200 pixels to 500 pixels. To configure this right-click on the slider and select “Settings”.

The settings window for the slider is shown below with the updated width value highlighted.

Step 3: Modelling a missile

The missile in our example has an initial position of (30, 40) and a velocity vector of [50, 30]. The position vector (relative to the origin) is given as follows.
\vec{r_A} = [30, 40]+t[50,30]
To represent this position vector in Geogebra we use the Curve function. The function takes parameterised expressions for the x-coordinates and y-coordinates of points on the curve, together with parameter used in the expressions, and the lower and upper limits of values passed to the parameter. In this case we use the variable t as the parameter and set the range of values from 0 to 10. The input for this in Geogebra is as follows:

Curve(30+50t, 40+30t, t, 0, 10)

To trace the path of the missile at a particular time we create a new point representing the point on the curve for the current value of the time variable. The input for this in Geogebra is given below:

a(t)

The details of the curve and parameterised point should appear in Geogebra as shown in the screenshot below.

The graphing window should look like the screenshot below after the commands shown above have been entered. It shows the path that the missile will take over the 10 second time interval, together with the current position of the missile (shown as point A). The time variable has a value of 0 and the missile is at its starting position of (30, 40).

Step 4: Modelling an asteroid

Next we model an asteroid with an initial position of (400, 250) and a velocity vector of [-20, -30]. The asteroid will be modelled as a circle with a diameter of 80 pixels. The position vector (relative to the origin) of the asteroid is given as follows.
\vec{r_A} = [400, 250]+t[-20,-30]
The path of the asteroid is modelled using the Curve function in Geogebra.

Curve(400-20t, 250-30t, t, 0, 10)

Next, the centre of the asteroid is modelled as a parameterised point.

b(t)

Finally, a circle whose centre is the parameterised point is created using the “Circle: Center and Radius” tool.

The radius of the circle is 40 pixels. The details of the curve, parameterised point and circle are shown in the screenshot below.

After these commands have been added the graphing window should look like the screenshot below.

Step 5: Tracking the distance between objects

To determine whether the missile hits the asteroid we will track the distance between the centres of these objects. We begin by choosing the “Distance or Length” tool and selecting the point representing the missile and the point representing the centre of the asteroid (points A and B). This will create a distance calculation object together with a text object to display the distance.

Double-clicking on the text element pops up a window allowing us to edit the text. In this case we change the text on the left-hand side of the equal sign as shown in the screenshot below.

We then open the settings for the text object and click on the Pin to Screen option. Selecting this lets us move the text to the top of the screen.

After these steps have been completed the Graphic Window will look like the screenshot below (note this also includes a change in colour for the asteroid which is accessed by clicking on the asteroid object and then selecting settings).

Extra bling

The look of the simulator can be improved by adding a space background. This is done by choosing the “Image” tool.

In this case an image that has the correct 4:3 aspect ratio is chosen. The image appears in the middle of the Graph window as shown in the following screenshot.

The anchor points of the image (points C and D) are so that the image covers an 800×600 area.

The final result is shown in the following screenshot.

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.