How I used Python to analyze Game of Thrones

How I used Python to analyze Game of Thrones

✅ I wanted to learn Python for a long time, Python to analyze but I could never find a reason. When my company had a bunch of daily reports that needed to be generated, I realized I had an opportunity to explore Python to cut out all the repetition.

This article is the result of a few weeks learning Python, playing around with the various libraries, and automating some of my tasks at work.

Now I want to share what Python is capable of.

Rather than give boring office related examples, let’s put them in a Game of Thrones frame!

In this post, I will be implementing web automation with the Selenium library, web scraping with the BeautifulSoup library, and generating reports with the csv module — which is sort of simulating the whole Pandas/Data Science side of Python.

And like I mentioned before —all of the examples will be using Game of Thrones.

Some Quick Notes:

  1. You shouldn’t need any Python experience to do this. I’ll explain the code, and you should have enough to get going.
  2. I’m not a super-expert at Python. This is roughly a few weeks of Python experience. It was just enough to automate my work and create these examples.
  3. Python is WELL DOCUMENTED. There are so many free guides to learning Python, like Automate the Boring Stuff, Python for Beginners, and the amazing Dataquest.io data science track. There’s even more links in the freeCodeCamp knowledge base.

Python, the best reptile-based computer language

For those unfamiliar with programming —

Python is a general purpose programming language which is strictly typed, interpreted, and known for its easy readability with great design principles.
Via the Freecodecamp.com guide

According to Stack Overflow’s 2018 Developer Survey, Python is the language most developers are wanting to learn (and also one of the fastest growing major programming languages).

Python powers site like Reddit, Instagram and Dropbox. It’s also a really readable language that has a lot of powerful libraries.

Python is named after Monty Python, not the reptile. BUT — in spite of that, it’s still the most popular reptile-based programming language, beating Serpent, Gecko, Cobra and Raptor! (I had to research that joke!)

If you have some background in programming (say in JavaScript)—

Some things about Python:

  • Python uses indentation vs curly brackets. Check the example below:

  • Python uses class-based inheritance — so it’s more like C languages. where as can JavaScript can simulate classes.
  • Python is also strongly typed. No mix-matching. For example, if you add a string and an integer together, it’ll start complaining.

Let’s jump right into it!

I’ll be breaking this into 3 pieces.

  • Game of Thrones and Python #1: Web automation
  • Game of Thrones and Python #2: Web Scraping
  • Game of Thrones and Python #3: Generating reports with the CSV Module

Game of Thrones and Python #1 — Web Automation

One of the coolest things you can do with Python is web automation.

For example — you can write a Python script that:

  1. Opens up a browser
  2. Automatically visits a specific website
  3. Logs you into that site
  4. Goes to another part of that website
  5. Finds the most recent blog post.
  6. Opens that blog post.
  7. Submits a comment that says, “Great writing! High five!”
  8. And finally logs you out of that website

It might not seem so hard to do. That takes what…. 20 seconds?

But if you had to do that over and over again, it would drive you insane.

For example — what if you had a staging site that’s still in development with 100 blog posts, and you wanted to post a comment on every single page to test its functionality?

That’s 100 blog posts * 20 seconds = roughly 33 minutes

And what if there are MULTIPLE testing phases, and you had to repeat the test six more times?

Other use cases for web automation include:

  • You might want to automate account creations on your site.
  • You might want to run a bot from start to finish in your online course.
  • You might want to push 100 bots to submit a form on your site with a single script.

What we will be doing

For this part, we’ll be automating the process to logging into all of our favorite Game of Thrones fan sites.

Don’t you hate when you have to waste time logging into westeros.org, the /r/freefolk subreddit, winteriscoming.net and all your other fan sites?

With this template, you can automatically log into various websites!

Now, for Game of Thrones!

The Code

You will need to install Python 3, Selenium, and the Firefox webdrivers to get started. If you want to follow along, check out my tutorial on How to automate form submissions with Python.

This one might get complicated. So I highly recommend sitting back and enjoying the ride.

## Game of Thrones easy login script
## 
## Description: This code logs into all of your fan sites automatically

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time


driver = webdriver.Firefox()
driver.implicitly_wait(5)
    ## implicity_wait makes the bot wait 5 seconds before every action
    ## so the site content can load up

# Define the functions

def login_to_westeros (username, userpass):

    ## Open the login page
    driver.get('https://asoiaf.westeros.org/index.php?/login/')    

    ## Log the details
    print(username + " is logging into westeros.")
    
    ## Find the fields and log into the account. 
    textfield_username = driver.find_element_by_id('auth')
    textfield_username.clear()
    textfield_username.send_keys(username)

    textfield_email = driver.find_element_by_id('password')
    textfield_email.clear()
    textfield_email.send_keys(userpass)

    submit_button = driver.find_element_by_id('elSignIn_submit')
    submit_button.click()

    ## Log the details
    print(username + " is logged in! -> westeros")



		
def login_to_reddit_freefolk (username, userpass):

    ## Open the login page
    driver.get('https://www.reddit.com/login/?dest=https%3A%2F%2Fwww.reddit.com%2Fr%2Ffreefolk')    

    ## Log the details
    print(username + " is logging into /r/freefolk.")
    
    ## Find the fields and log into the account. 
    textfield_username = driver.find_element_by_id('loginUsername')
    textfield_username.clear()
    textfield_username.send_keys(username) 
    textfield_email = driver.find_element_by_id('loginPassword')
    textfield_email.clear()
    textfield_email.send_keys(userpass)

    submit_button = driver.find_element_by_class_name('AnimatedForm__submitButton')
    submit_button.click()

    ## Log the details
    print(username + " is logged in! -> /r/freefolk.")
    

## Define the user and email combo. 

login_to_westeros("gameofthronesfan86", PASSWORDHERE)

time.sleep(2)
driver.execute_script("window.open('');")
Window_List = driver.window_handles
driver.switch_to_window(Window_List[-1])

login_to_reddit_freefolk("MyManMance", PASSWORDHERE)

time.sleep(2)
driver.execute_script("window.open('');")
Window_List = driver.window_handles
driver.switch_to_window(Window_List[-1])


## wait for 2 seconds
time.sleep(2)


print("task complete")

Breaking the code down

To start, I’m importing the Selenium library to help with the heavy lifting.

I also imported the time library, so after each action, it will wait x seconds. Adding a wait allows the page to load.

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time 

What is Selenium?

Selenium is the Python library we use for web automation. Selenium has developed an API so third-party authors can develop webdrivers to the communication to browsers. That way, the Selenium team can focus on their codebase, while another team can focus on the middleware.

For example:

  • The Chromium team made their own webdriver for Selenium called chromedriver.
  • The Firefox team made their own webdriver for Selenium called geckodriver.
  • The Opera team made their own webdriver for Selenium called operadriver.
driver = webdriver.Firefox()
driver.get('https://www.hbo.com/game-of-thrones')
driver.close()

In the code above, I’m asking Selenium to do things like “Set Firefox up as the browser of choice”, and “pass this link to Firefox”, and finally “Close Firefox”. I used the geckodriver to do that.

Logging into sites

To make it easier to read, I wrote a separate function to log into each site, to show the pattern that we are making.

def login_to_westeros (username, userpass):

    ## Log in
    driver.get('https://asoiaf.westeros.org/index.php?/login/')    

    ## Log the details
    print(username + " is logging into westeros.")
    
    ## 2) Look for the login box on the page
    textfield_username = driver.find_element_by_id('auth')
    textfield_username.clear()
    textfield_username.send_keys(username)

    textfield_email = driver.find_element_by_id('password')
    textfield_email.clear()
    textfield_email.send_keys(userpass)

    submit_button = driver.find_element_by_id('elSignIn_submit')
    submit_button.click()

    ## Log the details
    print(username + " is logged in! -> westeros")

If we break that down even more — each function has the following elements.

I’m telling Python to:

  1. Visit a specific page.
    driver.get('https://asoiaf.westeros.org/index.php?/login/')

2. Look for the login box
 * Clear the text if there is any
 * Submit my variable

 textfield_username = driver.find_element_by_id('auth')
    textfield_username.clear()
    textfield_username.send_keys(username)

3. Look for the password box
 * Clear the text if there is any
 * Submit my variable

    textfield_email = driver.find_element_by_id('password')
    textfield_email.clear()
    textfield_email.send_keys(userpass)

4. Look for the submit button, and click it

    submit_button = driver.find_element_by_id('elSignIn_submit')
    submit_button.click() 

As a note: each website has different ways to find the username/password and submit buttons. You’ll have to do a bit of searching for that.

How to find the login box and password box for any website

The Selenium Library has a bunch of handy ways to find elements on a webpage. Here are some of the ones I like to use.

  • find_element_by_id
  • find_element_by_name
  • find_element_by_xpath
  • find_element_by_class_name

For the whole list, visit the Selenium Python documentation for locating elements.

To use asoiaf.westeros.com as an example, when I inspect the elements — they all have IDs… which is GREAT! That makes my life easier.

Running the code

Here’s a short video of me running the code.

Enjoying the ride

With web automation, you’re playing a game of ‘how can I get Selenium to find the element’. Once you find it, you can then manipulate it.

Game of Thrones and Python #2 — Web Scraping

In this piece, we will be exploring web-scrapping.

The big picture process is:

  1. We’ll have Python visit a webpage.
  2. We’ll then parse that webpage with BeautifulSoup.
  3. You then set up the code to grab specific data.

For example: You might want to grab all the h1 tags. Or all the links. Or in our case, all of the images on a page.

Some other use cases for Web Scraping:

  • You can grab all the links on a web page.
  • You can grab all the post titles within a forum
  • You can use it to grab the daily NASDAQ Value without ever visiting the site.
  • You can use it to download all of the links within a website that doesn’t have a ‘Download All’.

In short, web scraping allows you to automatically grab web content through Python.

Overall, a very simple process. Except when it isn’t!

The challenge of Web Scraping for images

My goal was to turn my knowledge of web scraping content to grab images.

While web scraping for links, body text and headers is very straightforward, web scraping for images is significantly more complex. Let me explain.

As a web developer, hosting MULTIPLE full-sized images on a single webpage will slow the whole page down. Instead, use thumbnails and then only load the full-sized image when the thumbnail is clicked on.

For example: Imagine if we had twenty 1 megabyte images on our web page. Upon landing, a visitor would have to download 20 megabytes worth of images! The more common method is to make twenty 10kb thumbnail images. Now, your payload is only 200kb, or about 1/100 of the size!

So what does this have to do with web scraping images and this tutorial?

It means that it makes it pretty difficult to write a generic block of code that always works for every website. Websites implement all different ways to turn a thumbnail to a full-size image, which makes it a challenge to create a ‘one-size fits all’ model.

I’ll still teach what I learned. You’ll still gain a lot of skills from it. Just be aware that trying that code on other sites will require major modifications. Hurray for Zone of Proximal Development.

Python and Game of Thrones

The goal of this tutorial is that we’ll be gathering images of our favorite actors! Which will allow us to do weird things like make a Teenage Crush Actor Collage that we can hang in our bedroom (like so).

In order to gather those images, we’ll be using Python to do some web scraping. We’ll be using the BeautifulSoup library to visit a web page and grab all the image tags from it.

NOTE: In many website terms and conditions, they prohibit any web scraping of their data. Some develop APIs to allow you to tap into their data. Others do not. Additionally, try to be mindful that you are taking up their resources. So look to doing one request at a time rather than opening lots of connections in parallel and grinding their site to a halt.

The Code

# Import the libraries needed
import requests
import time
from bs4 import BeautifulSoup

# The URL to scrape
url = 'https://www.popsugar.com/celebrity/Kit-Harington-Rose-Leslie-Cutest-Pictures-42389549?stream_view=1#photo-42389576'
#url = 'https://www.bing.com/images/search?q=jon+snow&FORM=HDRSC2'

# Connecting
response = requests.get(url)

# Grab the HTML and using Beautiful
soup = BeautifulSoup (response.text, 'html.parser')

#A loop code to run through each link, and download it
for i in range(len(soup.findAll('img'))):

    tag = soup.findAll('img')[i]
    link = tag['src']

    #skip it if it doesn't start with http
    if "http" in full_link: 
        print("grabbed url: " + link)

        filename = str(i) + '.jpg'
        print("Download: " + filename)

        r = requests.get(link)
        open(filename, 'wb').write(r.content)

    else:
        print("grabbed url: " + link)
        print("skip")

    
    time.sleep(1)Breaking down the code

Having Python Visit the Webpage

We start by importing the libraries needed, and then storing the webpage link into a variable.

  • The Requests library is used to do all sorts of HTTP requests
  • The Time library is used to put a 1 second wait after each request. If we didn’t include that, the whole loop will fire off as fast as possible, which isn’t very friendly to the sites we are scraping from.
  • The BeautifulSoup Library is used to make exploring the DOM Tree easier.

Parse that webpage with BeautifulSoup

Next, we push our URL into BeautifulSoup.

Finding the content

Finally, we use a loop to grab the content.

It starts with a FOR loop. BeautifulSoup does some cool filtering, where my code asks BeautifulSoup find all the ‘img’ tags, and store it in a temporary array. Then, the len function asks for the length of the array.

#A loop code to run through each link, and download it
for i in range(len(soup.findAll('img'))):

So in human words, if the array held 51 items, the code will look like

For i in range(50):

Next, we’ll return back to our soup object, and do the real filtering.

tag = soup.findAll('img')[i]
   link = tag['src']

Remember that we are in a For loop, so [i] represents a number.

So we are telling BeautifulSoup to findAll ‘img’ tags, store it in a temp array, and reference a specific index number based on where we are in the loop.

So instead of calling an array directly like allOfTheImages[10], we’re using soup.findAll(‘img’)[10], and then passing it to the tag variable.

The data in the tag variable will look something like:

<img src="smiley.gif" alt="Smiley face" height="42" width="42">

Which is why the next step is pulling out the ‘src’.

Downloading the Content

Finally — it’s the fun part!

We go to the final part of the loop, with downloading the content.

There’s a few odd design elements here that I want to point out.

  1. The IF statement is actually a hack I made for other sites I was testing. There were times when I was grabbing images that was the part of the root site (like the favicon or the social media icons) that I didn’t want. So using the IF statement allowed me to ignore it.
  2. I also forced all the images to be .jpg. I could have written another chunk of IF statements to check the datatype, and then append the correct filetype. But that was adding a significant chunk of code that made this tutorial longer.
  3. I also added all the print commands. If you wanted to grab all the links of a webpage, or specific content — you can stop right here! You did it!

I also want to point out is the requests.get(link) and the open(filename, ‘wb’).write(r.content) code.

r = requests.get(link)open(filename, 'wb').write(r.content)

How this works:

  1. Requests gets the link.

2. Open is a default python function that opens or creates a file, gives it writing & binary mode access (since images are are just 1s and 0s), and writes the content of the link into that file.

#skip it if it doesn't start with http
    if "http" in full_link: 
        print("grabbed url: " + link)

        filename = str(i) + '.jpg'
        print("Download: " + filename)

        r = requests.get(link)
        open(filename, 'wb').write(r.content)

    else:
        print("grabbed url: " + link)
        print("skip")

    
    time.sleep(1)

Web Scraping has a lot of useful features.

This code won’t work right out of the box for most sites with images, but it can serve as a foundation to how to grab images on different sites.

Game of Thrones and Python #3 — Generating reports and data

Gathering data is easy. Interpreting the data is difficult. Which is why there’s a huge surge of demand for data scientists who can make sense of this data. And data scientists use languages like R and Python to interpret it.

In this tutorial, we’ll be using the csv module, which will be enough to generate a report. If we were working with a huge dataset, one that’s like 50,000 rows or bigger, we’d have to tap into the Pandas library.

What we will be doing is downloading a CSV, having Python interpret the data, send a query based on what kind of question we want answered, and then have the answer print out to us.

Python VS basic spreadsheet functions

You might be wondering:

“Why should I use Python when I can easily just use spreadsheet functions like =SUM or =COUNT, or filter out the rows I don’t need manually?”

Like for all the other automation tricks in Part 1 and 2, you can definitely do this manually.

But imagine if you had to generate a new report every day.

For example: I build online courses. And we want a daily report of every student’s progress. How many students started today? How many students are active this week? How many students made it to Module 2? How many students submitted their Module 3 homework? How many students clicked on the completion button on mobile devices?

I can either spend 15 minutes sorting through the data to generate a report for my team. OR write Python code that does it daily.

Other use cases for using code instead of default spreadsheet functions:

  • You might be working with a huge set of data (huge like 50,000 rows and 20 columns)
  • You require multiple slices of filters and segmentation to get your answers.
  • You need to run the same query on a dataset that changes repeatedly

Generating Reports with Game of Thrones

Every year, Winteriscoming.net, a Game of Thrones news site, has their annual March Madness. Visitors would vote for their favorite characters, and winners move up the bracket and compete against another person. After 6 rounds of votes, a winner is declared.

Since 2019’s votes are still happening, I grabbed all 6 rounds of 2018’s data and compiled them into a CSV file. To see how the poll looked like on winteriscoming.net, click here.

I’ve also added some additional background data (like where they are from), to make the reporting a bit more interesting.

Asking Questions

In order to generate a report, we have to ask some questions.

By definition: A report’s primary duty is to ANSWER questions.

So let’s make them up right now.

Based on this dataset… here’s some questions.

  1. Who won the popularity vote?
  2. Who won based on averages?
  3. Who is the most popular non-Westeros person? (characters not born in Westeros)

Before answering questions — let’s set up our Python code

To make it easier, I wrote the all the code, including revisions — in my new favorite online IDE, Repl.it.

Go ahead and take a look.

import csv

# Import the data
f_csv = open('winter-is-coming-2018.csv')
headers = next(f_csv) 
f_reader = csv.reader(f_csv)
file_data = list(f_reader)

# Make all blank cells into zeroes
# https://stackoverflow.com/questions/2862709/replacing-empty-csv-column-values-with-a-zero
for row in file_data:
  for i, x in enumerate(row):
    if len(x)< 1:
      x = row[i] = 0

Here’s my process with the code.

  1. I imported the csv module.

2. I imported the csv file, and turned it into a list type called file_data.

  • The way Python reads your file is by first passing the data to an object.
  • I removed the header, since it’ll fudge the data.
  • I then pass the object to a reader, and finally a list.
  • Note: I just realized I did it via the Python 2 way. There’s a cleaner way to do it in Python 3. Oh well. Still works.

3. In order to sum up any totals, I made all blank cells become 0.

  • This was one of those moments where found a Stack Overflow solution that was better than my original version.

With this set up, we can now loop through the list of data, and answer questions!

Question #1 — Who won the popularity vote?

The Spreadsheet method:

The easiest way would be to add up each cell, using a formula. 
Using row 2 as an example, in a blank column, you can write the formula:

=sum(E2:J2)

You can then drag that formula for the other rows.

Then, sort it by total. And you have a winner!

The Python Method:

## Include the code from above

# Push the data to a dictionary
total_score = {}

# Pass each character and their final score into total_score dictionary
for row in file_data:
  total = (int(row[4]) + 
          int(row[5]) + 
          int(row[6]) + 
          int(row[7]) + 
          int(row[8]) + 
          int(row[9]) )

  total_score[row[0]] = total

# Dictionaries aren't sortable by default, we'll have to borrow from these two classes.
# https://stackoverflow.com/questions/613183/how-do-i-sort-a-dictionary-by-value
from operator import itemgetter
from collections import OrderedDict

sorted_score = OrderedDict(sorted(total_score.items(), key=itemgetter(1) ,reverse=True))

# We get the name of the winner and their score
winner = list(sorted_score)[0] #jon snow
winner_score = sorted_score[winner] #score

print(winner + " with " + str(winner_score))

## RESULT => Jon Snow with 12959

The steps I took are:

  1. The dataset is just one big list. By using a for loop, you can then access each row.
  2. Within that for loop, I added each cell. (emulating the whole “=sum(E:J)” formula)
  3. Since dictionaries aren’t exactly sortable, I had to import two classes to help me sort the dictionary by their values, from high to low.
  4. Finally, I passed the winner, and the winner’s value as text.

To help understand that loop, I drew a diagram.

Overall, this process is a bit longer compared to the spreadsheet Method. But wait, it gets easier!

Question 2 — Who won based on averages?

You might have noticed that whoever proceeded farther in the rankings would obviously get more votes.

For example: If Jon Snow got 500 points in Round One and 1000 points in Round Two, he already beats The Mountain who only had 1000 points and never made it past his bracket.

So the next best thing is to sum the total, and then divide it based on how many rounds they participated in.

The Spreadsheet Method:

This is easy. In Column B is how many rounds they participated in. You would divide the rounds by the sum, and presto!

The Python method:

## OLD CODE FROM QUESTION 1
# Pass each character and their final score into total_score dictionary
for row in file_data:
  total = (int(row[4]) + 
          int(row[5]) + 
          int(row[6]) + 
          int(row[7]) + 
          int(row[8]) + 
          int(row[9]) )

  total_score[row[0]] = total

## NEW CODE
# Pass each character and their final score into total_score dictionary
for row in file_data:
  total = (int(row[4]) + 
          int(row[5]) + 
          int(row[6]) + 
          int(row[7]) + 
          int(row[8]) + 
          int(row[9]) )

  # NEW LINE - divide by how many rounds
  new_total = total / int(row[2])

  total_score[row[0]] = new_total

# RESULT => Davos Seaworth with 2247.6666666666665

Noticed the change? I just added one additional line.

That’s all it took to answer this question! NEXT!

Question 3 — Who is the most popular non-Westeros person?

With first two examples, it’s pretty easy to calculate the total with the default spreadsheet functions. For this question, things are a bit more complicated.

The Spreadsheet Method:

  1. Assuming you already have the sum
  2. You now have to filter it based on if they are Westeros/Other
  3. Then sort by the sum

The Python Method:

## OLD CODE FROM QUESTION 1
# Pass each character and their final score into total_score dictionary
for row in file_data:
  total = (int(row[4]) + 
          int(row[5]) + 
          int(row[6]) + 
          int(row[7]) + 
          int(row[8]) + 
          int(row[9]) )

  # NEW LINE - divide by how many rounds
  new_total = total / int(row[2])

  total_score[row[0]] = new_total

## NEW CODE
# Pass each character and their final score into total_score dictionary
for row in file_data:

  # Add IF-THEN statement
  if (row[3] == 'other'):
    total = (int(row[4]) + 
            int(row[5]) + 
            int(row[6]) + 
            int(row[7]) + 
            int(row[8]) + 
            int(row[9]) )
  else:
    total = 0

  total_score[row[0]] = total

# RESULT => Missandei with 4811

In Question 2, I added one line of code to answer that new question.

In Question 3, I added a IF-ELSE statement. If they are non-Westeros, then count their score. Else, give them a score of 0.

Reviewing this:

While the spreadsheet Method doesn’t seem like a lot of steps, it sure is a lot more clicks. The Python method took a lot longer to set up, but each additional query involved changing a few lines of code.

Imagine if the stakeholder asked a dozen more questions.

For example:

  1. How many points did characters whose names start with L have?
  2. Or how many points did everyone in round 3 get who lived in Westeros?
  3. Or if it was 640 GoT characters instead of just 64?

But also imagine this — you’re given a dataset that’s roughly 50 megabytes (Our Game of Thrones csv file was barely 50 kilobytes — roughly 1/1000 the size). A 50mb file that large would probably take Excel a few minutes to load. Additionally, it’s not unusual for Data Scientists to use datasets that are in the 10 gigabyte range!

Overall, as the data set scales, it’ll take longer and longer to process. And that’s where the power of Python comes in.

Conclusion

In Part 1, I covered web automation with the Selenium library. In Part 2, I covered web scraping with the BeautifulSoup library. And in Part 3, I covered generating reports with the csv module.

While I covered them in pieces — there’s also a synergy between them. Imagine if you had a project where you had to figure out who dies next in Game of Thrones based on the comments by the actors on the show. You might start with web scraping all of the actors’ names off of IMDB. You might use Selenium to automatically log into various social media platforms and search for their social media name. You might then compile all the data, and interpret it as a csv or, if it’s really huge, using the Pandas library.

We didn’t even get into Machine Learning, AI, Web Development, or the dozens of other things people use Python for.

Let this be a stepping stone into your Python journey!

30s ad

GUI Automation using Python| Python Automation

30 Days of Python | Unlock your Python Potential

Complete Python Programming with examples for beginners

Selenium WebDriver and Python: WebTest Automation Course

Interactive Data Visualization with Python & Bokeh

Game Development with Python: Snake Game

Game Development with Python: Snake Game

Today, I’m taking you along for a journey in game development. We are making it with the classic game of Snake with Kivy, Python

Hello, there.

A lot of people want to start programming apps for Android, but they prefer not to use Android Studio and/or Java. Why? Because it's an overkill. "I just wanna create Snake and nothing more!"

Let's snake without java! (with a bonus at the end)

Get familiarized

First app

Please confirm that you have already installed Kivy (if not, follow the instructions) and ran buildozer init in the project directory.

Let's run our first app:

# main.py
from kivy.app import App
from kivy.uix.widget import Widget

class WormApp(App):
    def build(self):
        return Widget()

if __name__ == '__main__':
    WormApp().run()

_First run of the application_

We created a Widget. Analogously, we can create a button or any other UI element:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button

class WormApp(App):
    def build(self):
        self.but = Button()
        self.but.pos = (100, 100)
        self.but.size = (200, 200)
        self.but.text = "Hello, cruel world"

        self.form = Widget()
        self.form.add_widget(self.but)
        return self.form

if __name__ == '__main__':
    WormApp().run()

_Creating a button_

Wow! Congratulations! You've created a button!

.kv files

However, there's another way to create UI elements. First, we implement our form:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.but1 = Button()
        self.but1.pos = (100, 100)
        self.add_widget(self.but1)

class WormApp(App):
    def build(self):
        self.form = Form()
        return self.form

if __name__ == '__main__':
    WormApp().run()

Then, we create a «worm.kv» file.

# worm.kv
<Form>:
    but2: but_id

    Button:
        id: but_id
        pos: (200, 200)

What just happened? We created another Button and assigned id as but_id. Then, but_id was matched to but2 of the form. It means that now we can refer to this button by but2.

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.but1 = Button()
        self.but1.pos = (100, 100)
        self.add_widget(self.but1)   #
        self.but2.text = "OH MY"

_Creating a new button_

Graphics

What we do next is creating a graphical element. First, we implement it in worm.kv:

<Form>:

<Cell>:
    canvas:
        Rectangle:
            size: self.size
            pos: self.pos

We linked the rectangle's position to self.pos and its size to self.size. So now, those properties are available from Cell, for example, once we create a cell, we can do:


class Cell(Widget):
    def __init__(self, x, y, size):
        super().__init__()
        self.size = (size, size)   # As you can see, we can change self.size which is "size" property of a rectangle
        self.pos = (x, y)

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell = Cell(100, 100, 30)
        self.add_widget(self.cell)

_Creating a cell_

Ok, we have created a cell.

Necessary Methods

Let's try to move it. To do that, we should add Form.update function and schedule it.

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock

class Cell(Widget):
    def __init__(self, x, y, size):
        super().__init__()
        self.size = (size, size)
        self.pos = (x, y)

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell = Cell(100, 100, 30)
        self.add_widget(self.cell)

    def start(self):
        Clock.schedule_interval(self.update, 0.01)

    def update(self, _):
        self.cell.pos = (self.cell.pos[0] + 2, self.cell.pos[1] + 3)

class WormApp(App):
    def build(self):
        self.form = Form()
        self.form.start()
        return self.form

if __name__ == '__main__':
    WormApp().run()

The cell will move across the form. As you can see, we can schedule any function with Clock.

Next, let's make a touch event. Rewrite Form:

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cells = []

    def start(self):
        Clock.schedule_interval(self.update, 0.01)

    def update(self, _):
        for cell in self.cells:
            cell.pos = (cell.pos[0] + 2, cell.pos[1] + 3)

    def on_touch_down(self, touch):
        cell = Cell(touch.x, touch.y, 30)
        self.add_widget(cell)
        self.cells.append(cell)

Each touch_down creates a cell with coordinates = (touch.x, touch.y) and size of 30. Then, we add it as a widget of the form AND to our own array (in order to easily access them).

Now you can tap on your form and generate cells.

_Generating multiple cells_

Neat settings

Because we want to get a nice snake, we should distinguish the graphical positions and the actual positions of cells.

Why?

_A lot of reasons to do so. All logic should be connected with the so-called actual data, while the graphical data is the result of the actual data. For example, if we want to make margins, the actual pos of the cell will be (100, 100) while the graphical pos of the rectangle — (102, 102).

P. S. We wouldn't do it if we dealt with classical on_draw. But here, we don't have to program on_draw._

Let's fix the worm.kv file:

<Form>:

<Cell>:
    canvas:
        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

and main.py:

...
from kivy.properties import *
...
class Cell(Widget):
    graphical_size = ListProperty([1, 1])
    graphical_pos = ListProperty([1, 1])

    def __init__(self, x, y, size, margin=4):
        super().__init__()
        self.actual_size = (size, size)
        self.graphical_size = (size - margin, size - margin)
        self.margin = margin
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def graphical_pos_attach(self):
        self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2)
...
class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell1 = Cell(100, 100, 30)
        self.cell2 = Cell(130, 100, 30)
        self.add_widget(self.cell1)
        self.add_widget(self.cell2)
...

_Connecting cells_

The margin appeared, so it looks pretty although we created the second cell with X = 130 instead of 132. Later, we will make smooth motion based on the distance between actual_pos and graphical_pos.

Coding the Worm

Implementation

Init config in main.py

class Config:
    DEFAULT_LENGTH = 20
    CELL_SIZE = 25
    APPLE_SIZE = 35
    MARGIN = 4
    INTERVAL = 0.2
    DEAD_CELL = (1, 0, 0, 1)
    APPLE_COLOR = (1, 1, 0, 1)

(Trust me, you'll love it!)

Then, assign config to the app:

class WormApp(App):
    def __init__(self):
        super().__init__()
        self.config = Config()
        self.form = Form(self.config)

    def build(self):
        self.form.start()
        return self.form

Rewrite init and start:

class Form(Widget):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

Then, the Cell:

class Cell(Widget):
    graphical_size = ListProperty([1, 1])
    graphical_pos = ListProperty([1, 1])

    def __init__(self, x, y, size, margin=4):
        super().__init__()
        self.actual_size = (size, size)
        self.graphical_size = (size - margin, size - margin)
        self.margin = margin
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def graphical_pos_attach(self):
        self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2)

    def move_to(self, x, y):
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def move_by(self, x, y, **kwargs):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs)

    def get_pos(self):
        return self.actual_pos

    def step_by(self, direction, **kwargs):
        self.move_by(self.actual_size[0] * direction[0], self.actual_size[1] * direction[1], **kwargs)

Hopefully, it's more or less clear.

and finally the Worm:

class Worm(Widget):
    def __init__(self, config):
        super().__init__()
        self.cells = []
        self.config = config
        self.cell_size = config.CELL_SIZE
        self.head_init((100, 100))
        for i in range(config.DEFAULT_LENGTH):
            self.lengthen()

    def destroy(self):
        for i in range(len(self.cells)):
            self.remove_widget(self.cells[i])
        self.cells = []

    def lengthen(self, pos=None, direction=(0, 1)):
        # If pos is set, we put the cell in pos, otherwise accordingly to the specified direction
        if pos is None:
            px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size
            py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size
            pos = (px, py)
        self.cells.append(Cell(*pos, self.cell_size, margin=self.config.MARGIN))
        self.add_widget(self.cells[-1])

    def head_init(self, pos):
        self.lengthen(pos=pos)

Let's give life to our wormie.

_IT'S ALIVE!_

Motion

Now, we will make it move.

It's simple:

class Worm(Widget):
...
    def move(self, direction):
        for i in range(len(self.cells) - 1, 0, -1):
            self.cells[i].move_to(*self.cells[i - 1].get_pos())
        self.cells[0].step_by(direction)
class Form(Widget):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        self.cur_dir = (1, 0)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

    def update(self, _):
        self.worm.move(self.cur_dir)

_He moving!_

It's alive! It's alive!

Controlling

As you could judge by the preview image, the controls of the snake will be the following:

class Form(Widget):
...
    def on_touch_down(self, touch):
        ws = touch.x / self.size[0]
        hs = touch.y / self.size[1]
        aws = 1 - ws
        if ws > hs and aws > hs:
            cur_dir = (0, -1)         # Down
        elif ws > hs >= aws:
            cur_dir = (1, 0)          # Right
        elif ws <= hs < aws:
            cur_dir = (-1, 0)         # Left
        else:
            cur_dir = (0, 1)           # Up
        self.cur_dir = cur_dir

_Even better!_

Cool.

Creating the Fruit

First, we initialize it.

class Form(Widget):
...
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
...
    def random_cell_location(self, offset):
        x_row = self.size[0] // self.config.CELL_SIZE
        x_col = self.size[1] // self.config.CELL_SIZE
        return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset)

    def random_location(self, offset):
        x_row, x_col = self.random_cell_location(offset)
        return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col

    def fruit_dislocate(self):
        x, y = self.random_location(2)
        self.fruit.move_to(x, y)
...
    def start(self):
        self.fruit = Cell(0, 0, self.config.APPLE_SIZE, self.config.MARGIN)
        self.worm = Worm(self.config)
        self.fruit_dislocate()
        self.add_widget(self.worm)
        self.add_widget(self.fruit)
        self.cur_dir = (1, 0)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

The current result:

_Creating the fruit_

Now, we should implement some Worm methods:

class Worm(Widget):
...
    # Here we get all the positions of our cells
    def gather_positions(self):
        return [cell.get_pos() for cell in self.cells]
    # Just check if our head has the same position as another Cell
    def head_intersect(self, cell):
        return self.cells[0].get_pos() == cell.get_pos()

...and add this check to update().

class Form(Widget):
...
    def update(self, _):
        self.worm.move(self.cur_dir)
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()

Detection of Self Tile Hitting

We want to know whether the head has the same position as one of the worm's cells.

class Form(Widget):
...
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
        self.game_on = True

    def update(self, _):
        if not self.game_on:
            return
        self.worm.move(self.cur_dir)
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()
       if self.worm_bite_self():
            self.game_on = False

    def worm_bite_self(self):
        for cell in self.worm.cells[1:]:
            if self.worm.head_intersect(cell):
                return cell
        return False

_Losing game if snake runs into itself_

Coloring, Decorating, and Code Refactoring

Let's start with code refactoring.

Rewrite and add

class Form(Widget):
...
    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        if self.fruit is not None:
            self.remove_widget(self.fruit)
        self.fruit = Cell(0, 0, self.config.APPLE_SIZE)
        self.fruit_dislocate()
        self.add_widget(self.fruit)
        Clock.schedule_interval(self.update, self.config.INTERVAL)
        self.game_on = True
        self.cur_dir = (0, -1)

    def stop(self):
        self.game_on = False
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop()
...
    def on_touch_down(self, touch):
        if not self.game_on:
            self.worm.destroy()
            self.start()
            return
        ...

Now, if the worm is dead (frozen), and you tap again, the game will be reset.
Now, let's go to decorating and coloring.

worm.kv

<Form>:
    popup_label: popup_label
    score_label: score_label

    canvas:
        Color:
            rgba: (.5, .5, .5, 1.0)

        Line:
            width: 1.5
            points: (0, 0), self.size

        Line:
            width: 1.5
            points: (self.size[0], 0), (0, self.size[1])

    Label:
        id: score_label
        text: "Score: " + str(self.parent.worm_len)
        width: self.width

    Label:
        id: popup_label
        width: self.width

<Worm>:

<Cell>:
    canvas:
        Color:
            rgba: self.color
        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

Rewrite WormApp:

class WormApp(App):
    def build(self):
        self.config = Config()
        self.form = Form(self.config)
        return self.form

    def on_start(self):
        self.form.start()

_Adding a score_

Let's color it. Rewrite Cell in .kv:

<Cell>:
    canvas:
        Color:
            rgba: self.color

        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

Add this to Cell.init

self.color = (0.2, 1.0, 0.2, 1.0)    # 

and this to Form.start

self.fruit.color = (1.0, 0.2, 0.2, 1.0)

Great, enjoy your snake

_The finished product!_

Finally, we will make a «game over» label

class Form(Widget):
...
    def __init__(self, config):
    ...
        self.popup_label.text = ""
...
    def stop(self, text=""):
        self.game_on = False
        self.popup_label.text = text
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop("GAME OVER" + " " * 5 + "\ntap to reset")

and make the hit cell red:

instead of

    def update(self, _):
    ...
        if self.worm_bite_self():
            self.game_over()
    ...

write

    def update(self, _):
        cell = self.worm_bite_self()
        if cell:
            cell.color = (1.0, 0.2, 0.2, 1.0)
            self.game_over()

_GAME OVER_

Are you still paying attention? Coming next is the most interesting part.

Bonus Section — Smooth Motion

Because the worm's step is equal to the cell_size, it's not that smooth. But we want to make it step as frequently as possible, without rewriting the entire logic of the game. So, we need to create a mechanism moving our graphical poses but not our actual poses. So, I wrote a simple file:

smooth.py

from kivy.clock import Clock
import time

class Timing:
    @staticmethod
    def linear(x):
        return x

class Smooth:
    def __init__(self, interval=1.0/60.0):
        self.objs = []
        self.running = False
        self.interval = interval

    def run(self):
        if self.running:
            return
        self.running = True
        Clock.schedule_interval(self.update, self.interval)

    def stop(self):
        if not self.running:
            return
        self.running = False
        Clock.unschedule(self.update)

    def setattr(self, obj, attr, value):
        exec("obj." + attr + " = " + str(value))

    def getattr(self, obj, attr):
        return float(eval("obj." + attr))

    def update(self, _):
        cur_time = time.time()
        for line in self.objs:
            obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line
            time_gone = cur_time - start_time
            if time_gone >= period:
                self.setattr(obj, prop_name_x, to_x)
                self.setattr(obj, prop_name_y, to_y)
                self.objs.remove(line)
            else:
                share = time_gone / period
                acs = timing(share)
                self.setattr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs)
                self.setattr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs)
        if len(self.objs) == 0:
            self.stop()

    def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear):
        self.objs.append((obj, prop_name_x, prop_name_y, self.getattr(obj, prop_name_x), self.getattr(obj, prop_name_y), to_x,
                          to_y, time.time(), t, timing))
        self.run()

class XSmooth(Smooth):
    def __init__(self, props, timing=Timing.linear, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props = props
        self.timing = timing

    def move_to(self, obj, to_x, to_y, t):
        super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing)

This module is not the most elegant solution ©. It's a bad solution and I acknowledge it. It is an only-hello-world solution.

So you just create smooth.py and copy-paste this code to the file.
Finally, let's make it work:

class Form(Widget):
...
    def __init__(self, config):
    ...
        self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"])

Then, we replace self.worm.move() with

class Form(Widget):
...
    def update(self, _):
    ...
        self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL))

And this is how methods of Cell should look like:

class Cell(Widget):
...
    def graphical_pos_attach(self, smooth_motion=None):
        to_x, to_y = self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2
        if smooth_motion is None:
            self.graphical_pos = to_x, to_y
        else:
            smoother, t = smooth_motion
            smoother.move_to(self, to_x, to_y, t)

    def move_to(self, x, y, **kwargs):
        self.actual_pos = (x, y)
        self.graphical_pos_attach(**kwargs)

    def move_by(self, x, y, **kwargs):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs)

That's it, thank you for your attention!

How the final result works, My final code

Thank for reading ! Originally published on dzone.com

Learn more

Beginning Game Development with Python

Python Online Multiplayer Game Development Tutorial

PyGame Tutorial – Game Development in Python with Pygame

PyGame Tutorial – Game Development in Python with Pygame

The PyGame library is an open-source module for the Python programming language specifically intended to help you make games and other multimedia applications. In this post, you'll learn how to use PyGame to write games in Python.

By the end of this article, you’ll be able to:

  • Draw items on your screen
  • Play sound effects and music
  • Handle user input
  • Implement event loops
  • Describe how game programming differs from standard procedural Python programming

Table of Contents

  • Background and Setup
  • Basic PyGame Program
  • PyGame Concepts
  • Initialization and Modules
  • Displays and Surfaces
  • Images and Rects
  • Basic Game Design
  • Importing and Initializing PyGame
  • Setting Up the Display
  • Setting Up the Game Loop
  • Processing Events
  • Drawing on the Screen
  • Using .blit() and .flip()
  • Sprites
  • Players
  • User Input
  • Enemies
  • Sprite Groups
  • Custom Events
  • Collision Detection
  • Sprite Images
  • Altering the Object Constructors
  • Adding Background Images
  • Game Speed
  • Sound Effects
  • A Note on Sources
  • Conclusion
Background and Setup

pygame is a Python wrapper for the SDL library, which stands for Simple DirectMedia Layer. SDL provides cross-platform access to your system’s underlying multimedia hardware components, such as sound, video, mouse, keyboard, and joystick. pygame started life as a replacement for the stalled PySDL project. The cross-platform nature of both SDL and pygame means you can write games and rich multimedia Python programs for every platform that supports them!

To install pygame on your platform, use the appropriate pip command:

$ pip install pygame

You can verify the install by loading one of the examples that comes with the library:

$ python3 -m pygame.examples.aliens

If a game window appears, then pygame is installed properly! If you run into problems, then the Getting Started guide outlines some known issues and caveats for all platforms.

Basic PyGame Program

Before getting down to specifics, let’s take a look at a basic pygame program. This program creates a window, fills the background with white, and draws a blue circle in the middle of it:

# Simple pygame program
# Import and initialize the pygame library
import pygame
pygame.init()
# Set up the drawing window
screen = pygame.display.set_mode([500, 500])
# Run until the user asks to quit
running = True
while running:
    # Did the user click the window close button?
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    # Fill the background with white
    screen.fill((255, 255, 255))
    # Draw a solid blue circle in the center
    pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
    # Flip the display
    pygame.display.flip()
# Done! Time to quit.
pygame.quit()

When you run this program, you’ll see a window that looks like this:

Let’s break this code down, section by section:

  • Lines 4 and 5 import and initialize the pygame library. Without these lines, there is no pygame.
  • Line 8 sets up your program’s display window. You provide either a list or a tuple that specifies the width and height of the window to create. This program uses a list to create a square window with 500 pixels on each side.
  • Lines 11 and 12 set up a game loop to control when the program ends. You’ll cover game loops later on in this tutorial.
  • Lines 15 to 17 scan and handle events within the game loop. You’ll get to events a bit later as well. In this case, the only event handled is pygame.QUIT, which occurs when the user clicks the window close button.
  • Line 20 fills the window with a solid color. screen.fill() accepts either a list or tuple specifying the RGB values for the color. Since (255, 255, 255) was provided, the window is filled with white.
  • Line 23 draws a circle in the window, using the following parameters:
  • screen: the window on which to draw
  • (0, 0, 255): a tuple containing RGB color values
  • (250, 250): a tuple specifying the center coordinates of the circle
  • 75: the radius of the circle to draw in pixels
  • Line 26 updates the contents of the display to the screen. Without this call, nothing appears in the window!
  • Line 29 exits pygame. This only happens once the loop finishes.

That’s the pygame version of “Hello, World.” Now let’s dig a little deeper into the concepts behind this code.

PyGame Concepts

As pygame and the SDL library are portable across different platforms and devices, they both need to define and work with abstractions for various hardware realities. Understanding those concepts and abstractions will help you design and develop your own games.

Initialization and Modules

The pygame library is composed of a number of Python constructs, which include several different modules. These modules provide abstract access to specific hardware on your system, as well as uniform methods to work with that hardware. For example, display allows uniform access to your video display, while joystick allows abstract control of your joystick.

After importing the pygame library in the example above, the first thing you did was initialize PyGame using pygame.init(). This function calls the separate init() functions of all the included pygame modules. Since these modules are abstractions for specific hardware, this initialization step is required so that you can work with the same code on Linux, Windows, and Mac.

Displays and Surfaces

In addition to the modules, pygame also includes several Python classes, which encapsulate non-hardware dependent concepts. One of these is the Surface which, at its most basic, defines a rectangular area on which you can draw. Surface objects are used in many contexts in pygame. Later you’ll see how to load an image into a Surface and display it on the screen.

In pygame, everything is viewed on a single user-created display, which can be a window or a full screen. The display is created using .set_mode(), which returns a Surface representing the visible part of the window. It is this Surface that you pass into drawing functions like pygame.draw.circle(), and the contents of that Surface are pushed to the display when you call pygame.display.flip().

Images and Rects

Your basic pygame program drew a shape directly onto the display’s Surface, but you can also work with images on the disk. The image module allows you to load and save images in a variety of popular formats. Images are loaded into Surface objects, which can then be manipulated and displayed in numerous ways.

As mentioned above, Surface objects are represented by rectangles, as are many other objects in pygame, such as images and windows. Rectangles are so heavily used that there is a special Rect class just to handle them. You’ll be using Rect objects and images in your game to draw players and enemies, and to manage collisions between them.

Okay, that’s enough theory. Let’s design and write a game!

Basic Game Design

Before you start writing any code, it’s always a good idea to have some design in place. Since this is a tutorial game, let’s design some basic gameplay for it as well:

  • The goal of the game is to avoid incoming obstacles:
  • The player starts on the left side of the screen.
  • The obstacles enter randomly from the right and move left in a straight line.
  • The player can move left, right, up, or down to avoid the obstacles.
  • The player cannot move off the screen.
  • The game ends either when the player is hit by an obstacle or when the user closes the window.

When he was describing software projects, a former colleague of mine used to say, “You don’t know what you do until you know what you don’t do.” With that in mind, here are some things that won’t be covered in this tutorial:

  • No multiple lives
  • No scorekeeping
  • No player attack capabilities
  • No advancing levels
  • No boss characters

You’re free to try your hand at adding these and other features to your own program.

Let’s get started!

Importing and Initializing PyGame

After you import pygame, you’ll also need to initialize it. This allows pygame to connect its abstractions to your specific hardware:

# Import the pygame module
import pygame
# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)
# Initialize pygame
pygame.init()

The pygame library defines many things besides modules and classes. It also defines some local constants for things like keystrokes, mouse movements, and display attributes. You reference these constants using the syntax pygame.<CONSTANT>. By importing specific constants from pygame.locals, you can use the syntax <CONSTANT> instead. This will save you some keystrokes and improve overall readability.

Setting Up the Display

Now you need something to draw on! Create a screen to be the overall canvas:

# Import the pygame module
import pygame
# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)
# Initialize pygame
pygame.init()
# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

You create the screen to use by calling pygame.display.set_mode() and passing a tuple or list with the desired width and height. In this case, the window is 800x600, as defined by the constants SCREEN_WIDTH and SCREEN_HEIGHT on lines 20 and 21. This returns a Surface which represents the inside dimensions of the window. This is the portion of the window you can control, while the OS controls the window borders and title bar.

If you run this program now, then you’ll see a window pop up briefly and then immediately disappear as the program exits. Don’t blink or you might miss it! In the next section, you’ll focus on the main game loop to ensure that your program exits only when given the correct input.

Setting Up the Game Loop

Every game from Pong to Fortnite uses a game loop to control gameplay. The game loop does four very important things:

  1. Processes user input
  2. Updates the state of all game objects
  3. Updates the display and audio output
  4. Maintains the speed of the game

Every cycle of the game loop is called a frame, and the quicker you can do things each cycle, the faster your game will run. Frames continue to occur until some condition to exit the game is met. In your design, there are two conditions that can end the game loop:

  1. The player collides with an obstacle. (You’ll cover collision detection later.)
  2. The player closes the window.

The first thing the game loop does is process user input to allow the player to move around the screen. Therefore, you need some way to capture and process a variety of input. You do this using the pygame event system.

Processing Events

Key presses, mouse movements, and even joystick movements are some of the ways in which a user can provide input. All user input results in an event being generated. Events can happen at any time and often (but not always) originate outside the program. All events in pygame are placed in the event queue, which can then be accessed and manipulated. Dealing with events is referred to as handling them, and the code to do so is called an event handler.

Every event in pygame has an event type associated with it. For your game, the event types you’ll focus on are keypresses and window closure. Keypress events have the event type KEYDOWN, and the window closure event has the type QUIT. Different event types may also have other data associated with them. For example, the KEYDOWN event type also has a variable called key to indicate which key was pressed.

You access the list of all active events in the queue by calling pygame.event.get(). You then loop through this list, inspect each event type, and respond accordingly:

# Variable to keep the main loop running
running = True
# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, stop the loop.
            if event.key == K_ESCAPE:
                running = False
        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False

Let’s take a closer look at this game loop:

  • Line 28 sets up a control variable for the game loop. To exit the loop and the game, you set running = False. The game loop starts on line 29.
  • Line 31 starts the event handler, walking through every event currently in the event queue. If there are no events, then the list is empty, and the handler won’t do anything.
  • Lines 35 to 38 check if the current event.type is a KEYDOWN event. If it is, then the program checks which key was pressed by looking at the event.key attribute. If the key is the Esc key, indicated by K_ESCAPE, then it exits the game loop by setting running = False.
  • Lines 41 and 42 do a similar check for the event type called QUIT. This event only occurs when the user clicks the window close button. The user may also use any other operating system action to close the window.

When you add these lines to the previous code and run it, you’ll see a window with a blank or black screen:

The window won’t disappear until you press the Esc key, or otherwise trigger a QUIT event by closing the window.

Drawing on the Screen

In the sample program, you drew on the screen using two commands:

  1. screen.fill() to fill the background
  2. pygame.draw.circle() to draw a circle

Now you’ll learn about a third way to draw to the screen: using a Surface.

Recall that a Surface is a rectangular object on which you can draw, like a blank sheet of paper. The screen object is a Surface, and you can create your own Surface objects separate from the display screen. Let’s see how that works:

# Fill the screen with white
screen.fill((255, 255, 255))
# Create a surface and pass in a tuple containing its length and width
surf = pygame.Surface((50, 50))
# Give the surface a color to separate it from the background
surf.fill((0, 0, 0))
rect = surf.get_rect()

After the screen is filled with white on line 45, a new Surface is created on line 48. This Surface is 50 pixels wide, 50 pixels tall, and assigned to surf. At this point, you treat it just like the screen. So on line, 51 you fill it with black. You can also access its underlying Rect using .get_rect(). This is stored as rect for later use.

Using .blit() and .flip()

Just creating a new Surface isn’t enough to see it on the screen. To do that, you need to blit the Surface onto another Surface. The term blit stands for Block Transfer, and .blit() is how you copy the contents of one Surface to another. You can only .blit() from one Surface to another, but since the screen is just another Surface, that’s not a problem. Here’s how you draw surf on the screen:

# This line says "Draw surf onto the screen at the center"
screen.blit(surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
pygame.display.flip()

The .blit() call on line 55 takes two arguments:

  1. The Surface to draw
  2. The location at which to draw it on the source Surface

The coordinates (SCREENWIDTH/2, SCREENHEIGHT/2) tell your program to place surf in the exact center of the screen, but it doesn’t quite look that way:

The reason why the image looks off-center is that .blit() puts the top-left corner of surf at the location given. If you want surf to be centered, then you’ll have to do some math to shift it up and to the left. You can do this by subtracting the width and height of surf from the width and height of the screen, dividing each by 2 to locate the center, and then passing those numbers as arguments to screen.blit():

# Put the center of surf at the center of the display
surf_center = (
    (SCREEN_WIDTH-surf.get_width())/2,
    (SCREEN_HEIGHT-surf.get_height())/2
)
# Draw surf at the new coordinates
screen.blit(surf, surf_center)
pygame.display.flip()

Notice the call to pygame.display.flip() after the call to blit(). This updates the entire screen with everything that’s been drawn since the last flip. Without the call to .flip(), nothing is shown.

Sprites

In your game design, the player starts on the left, and obstacles come in from the right. You can represent all the obstacles with Surface objects to make drawing everything easier, but how do you know where to draw them? How do you know if an obstacle has collided with the player? What happens when the obstacle flies off the screen? What if you want to draw background images that also move? What if you want your images to be animated? You can handle all these situations and more with sprites.

In programming terms, a sprite is a 2D representation of something on the screen. Essentially, it’s a picture. pygame provides a Sprite class, which is designed to hold one or several graphical representations of any game object that you want to display on the screen. To use it, you create a new class that extends Sprite. This allows you to use its built-in methods.

Players

Here’s how you use Sprite objects with the current game to define the player. Insert this code after line 18:

# Define a Player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of 'player'
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.Surface((75, 25))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect()

You first define Player by extending pygame.sprite.Sprite on line 22. Then .__init__() uses .super() to call the .__init__() method of Sprite.

Next, you define and initialize .surf to hold the image to display, which is currently a white box. You also define and initialize .rect, which you’ll use to draw the player later. To use this new class, you need to create a new object and change the drawing code as well. Expand the code block below to see it all together:

Run this code. You’ll see a white rectangle at roughly the middle of the screen:

What do you think would happen if you changed line 59 to screen.blit(player.surf, player.rect)? Try it and see:

# Fill the screen with black
screen.fill((0, 0, 0))
# Draw the player on the screen
screen.blit(player.surf, player.rect)
# Update the display
pygame.display.flip()

When you pass a Rect to .blit(), it uses the coordinates of the top left corner to draw the surface. You’ll use this later to make your player move!

User Input

So far, you’ve learned how to set up pygame and draw objects on the screen. Now, the real fun starts! You’ll make the player controllable using the keyboard.

Earlier, you saw that pygame.event.get() returns a list of the events in the event queue, which you scan for KEYDOWN event types. Well, that’s not the only way to read keypresses. pygame also provides pygame.event.get_pressed(), which returns a dictionary containing all the current KEYDOWN events in the queue.

Put this in your game loop right after the event handling loop. This returns a dictionary containing the keys pressed at the beginning of every frame:

# Get the set of keys pressed and check for user input
pressed_keys = pygame.key.get_pressed()

Next, you write a method in Player to accepts that dictionary. This will define the behavior of the sprite based off the keys that are pressed. Here’s what that might look like:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
    if pressed_keys[K_UP]:
        self.rect.move_ip(0, -5)
    if pressed_keys[K_DOWN]:
        self.rect.move_ip(0, 5)
    if pressed_keys[K_LEFT]:
        self.rect.move_ip(-5, 0)
    if pressed_keys[K_RIGHT]:
        self.rect.move_ip(5, 0)

K_UP, K_DOWN, K_LEFT, and K_RIGHT correspond to the arrow keys on the keyboard. If the dictionary entry for that key is True, then that key is down, and you move the player .rect in the proper direction. Here you use .move_ip(), which stands for move in place, to move the current Rect.

Then you can call .update() every frame to move the player sprite in response to keypresses. Add this call right after the call to .get_pressed():

# Main loop
while running:
    # for loop through the event queue
    for event in pygame.event.get():
        # Check for KEYDOWN event
        if event.type == KEYDOWN:
            # If the Esc key is pressed, then exit the main loop
            if event.key == K_ESCAPE:
                running = False
        # Check for QUIT event. If QUIT, then set running to false.
        elif event.type == QUIT:
            running = False
    # Get all the keys currently pressed
    pressed_keys = pygame.key.get_pressed()
    # Update the player sprite based on user keypresses
    player.update(pressed_keys)
    # Fill the screen with black
    screen.fill((0, 0, 0))

Now you can move your player rectangle around the screen with the arrow keys:

You may notice two small problems:

  1. The player rectangle can move very fast if a key is held down. You’ll work on that later.
  2. The player rectangle can move off the screen. Let’s solve that one now.

To keep the player on the screen, you need to add some logic to detect if the rect is going to move off screen. To do that, you check whether the rect coordinates have moved beyond the screen’s boundary. If so, then you instruct the program to move it back to the edge:

# Move the sprite based on user keypresses
def update(self, pressed_keys):
    if pressed_keys[K_UP]:
        self.rect.move_ip(0, -5)
    if pressed_keys[K_DOWN]:
        self.rect.move_ip(0, 5)
    if pressed_keys[K_LEFT]:
        self.rect.move_ip(-5, 0)
    if pressed_keys[K_RIGHT]:
        self.rect.move_ip(5, 0)
    # Keep player on the screen
    if self.rect.left < 0:
        self.rect.left = 0
    if self.rect.right > SCREEN_WIDTH:
        self.rect.right = SCREEN_WIDTH
    if self.rect.top <= 0:
        self.rect.top = 0
    if self.rect.bottom >= SCREEN_HEIGHT:
        self.rect.bottom = SCREEN_HEIGHT

Here, instead of using .move(), you just change the corresponding coordinates of .top, .bottom, .left, or .right directly. Test this, and you’ll find the player rectangle can no longer move off the screen.

Now let’s add some enemies!

Enemies

What’s a game without enemies? You’ll use the techniques you’ve already learned to create a basic enemy class, then create a lot of them for your player to avoid. First, import the random library:

# Import random for random numbers
import random

Then create a new sprite class called Enemy, following the same pattern you used for Player:

# Define the enemy object by extending pygame.sprite.Sprite
# The surface you draw on the screen is now an attribute of 'enemy'
class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.Surface((20, 10))
        self.surf.fill((255, 255, 255))
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
        )
        self.speed = random.randint(5, 20)
    # Move the sprite based on speed
    # Remove the sprite when it passes the left edge of the screen
    def update(self):
        self.rect.move_ip(-self.speed, 0)
        if self.rect.right < 0:
            self.kill()

There are four notable differences between Enemy and Player:

  1. On lines 62 to 67, you update rect to be a random location along the right edge of the screen. The center of the rectangle is just off the screen. It’s located at some position between 20 and 100 pixels away from the right edge, and somewhere between the top and bottom edges.
  2. On line 68, you define .speed as a random number between 5 and 20. This specifies how fast this enemy moves towards the player.
  3. On lines 73 to 76, you define .update(). It takes no arguments since enemies move automatically. Instead, .update() moves the enemy toward the left side of the screen at the .speed defined when it was created.
  4. On line 74, you check whether the enemy has moved off-screen. To make sure the Enemy is fully off the screen and won’t just disappear while it’s still visible, you check that the right side of the .rect has gone past the left side of the screen. Once the enemy is off-screen, you call .kill() to prevent it from being processed further.

So, what does .kill() do? To figure this out, you have to know about Sprite Groups.

Sprite Groups

Another super useful class that pygame provides is the Sprite Group. This is an object that holds a group of Sprite objects. So why use it? Can’t you just track your Sprite objects in a list instead? Well, you can, but the advantage of using a Group lies in the methods it exposes. These methods help to detect whether any Enemy has collided with the Player, which makes updates much easier.

Let’s see how to create sprite groups. You’ll create two different Group objects:

  1. The first Group will hold every Sprite in the game.
  2. The second Group will hold just the Enemy objects.

Here’s what that looks like in code:

# Create the 'player'
player = Player()
# Create groups to hold enemy sprites and all sprites
# - enemies is used for collision detection and position updates
# - all_sprites is used for rendering
enemies = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)
# Variable to keep the main loop running
running = True

When you call .kill(), the Sprite is removed from every Group to which it belongs. This removes the references to the Sprite as well, which allows Python’s garbage collector to reclaim the memory as necessary.

Now that you have an all_sprites group, you can change how objects are drawn. Instead of calling .blit() on just Player, you can iterate over everything in all_sprites:

# Fill the screen with black
screen.fill((0, 0, 0))
# Draw all sprites
for entity in all_sprites:
    screen.blit(entity.surf, entity.rect)
# Flip everything to the display
pygame.display.flip()

Now, anything put into all_sprites will be drawn with every frame, whether it’s an enemy or the player.

There’s just one problem… You don’t have any enemies! You could create a bunch of enemies at the beginning of the game, but the game would quickly become boring when they all left the screen a few seconds later. Instead, let’s explore how to keep a steady supply of enemies coming as the game progresses.

Custom Events

The design calls for enemies to appear at regular intervals. This means that at set intervals, you need to do two things:

  1. Create a new Enemy.
  2. Add it to all_sprites and enemies.

You already have code that handles random events. The event loop is designed to look for random events occurring every frame and deal with them appropriately. Luckily, pygame doesn’t restrict you to using only the event types it has defined. You can define your own events to handle as you see fit.

Let’s see how to create a custom event that’s generated every few seconds. You can create a custom event by naming it:

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# Create a custom event for adding a new enemy
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
# Instantiate player. Right now, this is just a rectangle.
player = Player()

pygame defines events internally as integers, so you need to define a new event with a unique integer. The last event pygame reserves is called USEREVENT, so defining ADDENEMY = pygame.USEREVENT + 1 on line 83 ensures it’s unique.

Next, you need to insert this new event into the event queue at regular intervals throughout the game. That’s where the time module comes in. Line 84 fires the new ADDENEMY event every 250 milliseconds, or four times per second. You call .set_timer() outside the game loop since you only need one timer, but it will fire throughout the entire game.

Add the code to handle your new event:

# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, stop the loop.
            if event.key == K_ESCAPE:
                running = False
        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False
        # Add a new enemy?
        elif event.type == ADDENEMY:
            # Create the new enemy and add it to sprite groups
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)
    # Get the set of keys pressed and check for user input
    pressed_keys = pygame.key.get_pressed()
    player.update(pressed_keys)
    # Update enemy position
    enemies.update()

Whenever the event handler sees the new ADDENEMY event on line 115, it creates an Enemy and adds it to enemies and allsprites. Since Enemy is in allsprites, it will get drawn every frame. You also need to call enemies.update() on line 126, which updates everything in enemies, to ensure they move properly:

However, that’s not the only reason there’s a group for just enemies.

Collision Detection

Your game design calls for the game to end whenever an enemy collides with the player. Checking for collisions is a basic technique of game programming, and usually requires some non-trivial math to determine whether two sprites will overlap each other.

This is where a framework like pygame comes in handy! Writing collision detection code is tedious, but pygame has a LOT of collision detection methods available for you to use.

For this tutorial, you’ll use a method called .spritecollideany(), which is read as “sprite collide any.” This method accepts a Sprite and a Group as parameters. It looks at every object in the Group and checks if its .rect intersects with the .rect of the Sprite. If so, then it returns True. Otherwise, it returns False. This is perfect for this game since you need to check if the single player collides with one of a Group of enemies.

Here’s what that looks like in code:

# Draw all sprites
for entity in all_sprites:
    screen.blit(entity.surf, entity.rect)
# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
    # If so, then remove the player and stop the loop
    player.kill()
    running = False

Line 135 tests whether player has collided with any of the objects in enemies. If so, then player.kill() is called to remove it from every group to which it belongs. Since the only objects being rendered are in all_sprites, the player will no longer be rendered. Once the player has been killed, you need to exit the game as well, so you set running = False to break out of the game loop on line 138.

At this point, you’ve got the basic elements of a game in place:

Now, let’s dress it up a bit, make it more playable, and add some advanced capabilities to help it stand out.

Sprite Images

Alright, you have a game, but let’s be honest… It’s kind of ugly. The player and enemies are just white blocks on a black background. That was state-of-the-art when Pong was new, but it just doesn’t cut it anymore. Let’s replace all those boring white rectangles with some cooler images that will make the game feel like an actual game.

Earlier, you learned that images on disk can be loaded into a Surface with some help from the image module. For this tutorial, we made a little jet for the player and some missiles for the enemies. You’re welcome to use this art, draw your own, or download some free game art assets to use. You can click the link below to download the art used in this tutorial:

Clone Repo: Click here to clone the repo you'll use to learn how to use PyGame in this tutorial.

Altering the Object Constructors

Before you use images to represent the player and enemy sprites, you need to make some changes to their constructors. The code below replaces the code used previously:

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
# from pygame.locals import *
from pygame.locals import (
    RLEACCEL,
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)
# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.image = pygame.image.load("jet.png").convert()
        self.image.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.image.get_rect()

Let’s unpack line 31 a bit. pygame.image.load() loads an image from the disk. You pass it a path to the file. It returns a Surface, and the .convert() call optimizes the Surface, making future .blit() calls faster.

Line 32 uses .set_colorkey() to indicate the color pygame will render as transparent. In this case, you choose white, because that’s the background color of the jet image. The RLEACCEL constant is an optional parameter that helps pygame render more quickly on non-accelerated displays. This is added to the pygame.locals import statement on line 11.

Nothing else needs to change. The image is still a Surface, except now it has a picture painted on it. You still use it in the same way.

Here’s what similar changes to the Enemy look like:

# Define the enemy object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.image.load("missile.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        # The starting position is randomly generated, as is the speed
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
        )
        self.speed = random.randint(5, 20)

Running the program now should show that this is the same game you had before, except now you’ve added some nice graphics skins with images. But why stop at just making the player and enemy sprites look nice? Let’s add a few clouds going past to give the impression of a jet flying through the sky.

Adding Background Images

For background clouds, you use the same principles as you did for Player and Enemy:

  1. Create the Cloud class.
  2. Add an image of a cloud to it.
  3. Create a method .update() that moves the cloud toward the left side of the screen.
  4. Create a custom event and handler to create new cloud objects at a set time interval.
  5. Add the newly created cloud objects to a new Group called clouds.
  6. Update and draw the clouds in your game loop.

Here’s what Cloud looks like:

# Define the cloud object by extending pygame.sprite.Sprite
# Use an image for a better-looking sprite
class Cloud(pygame.sprite.Sprite):
    def __init__(self):
        super(Cloud, self).__init__()
        self.surf = pygame.image.load("cloud.png").convert()
        self.surf.set_colorkey((0, 0, 0), RLEACCEL)
        # The starting position is randomly generated
        self.rect = self.surf.get_rect(
            center=(
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT),
            )
    # Move the cloud based on a constant speed
    # Remove the cloud when it passes the left edge of the screen
    def update(self):
        self.rect.move_ip(-5, 0)
        if self.rect.right < 0:
            self.kill()

That should all look very familiar. It’s pretty much the same as Enemy.

To have clouds appear at certain intervals, you’ll use event creation code similar to what you used to create new enemies. Put it right below the enemy creation event:

# Create custom events for adding a new enemy and a cloud
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)

This says to wait 1000 milliseconds, or one second, before creating the next cloud.

Next, create a new Group to hold each newly created cloud:

# Create groups to hold enemy sprites, cloud sprites, and all sprites
# - enemies is used for collision detection and position updates
# - clouds is used for position updates
# - all_sprites is used for rendering
enemies = pygame.sprite.Group()
clouds = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
all_sprites.add(player)

Next, add a handler for the new ADDCLOUD event in the event handler:

# Main loop
while running:
    # Look at every event in the queue
    for event in pygame.event.get():
        # Did the user hit a key?
        if event.type == KEYDOWN:
            # Was it the Escape key? If so, then stop the loop.
            if event.key == K_ESCAPE:
                running = False
        # Did the user click the window close button? If so, stop the loop.
        elif event.type == QUIT:
            running = False
        # Add a new enemy?
        elif event.type == ADDENEMY:
            # Create the new enemy and add it to sprite groups
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)
        # Add a new cloud?
        elif event.type == ADDCLOUD:
            # Create the new cloud and add it to sprite groups
            new_cloud = Cloud()
            clouds.add(new_cloud)
            all_sprites.add(new_cloud)

Finally, make sure the clouds are updated every frame:

# Update the position of enemies and clouds
enemies.update()
clouds.update()
# Fill the screen with sky blue
screen.fill((135, 206, 250))

Line 172 updates the original screen.fill() to fill the screen with a pleasant sky blue color. You can change this color to something else. Maybe you want an alien world with a purple sky, a toxic wasteland in neon green, or the surface of Mars in red!

Note that each new Cloud and Enemy are added to all_sprites as well as clouds and enemies. This is done because each group is used for a separate purpose:

  • Rendering is done using all_sprites.
  • Position updates are done using clouds and enemies.
  • Collision detection is done using enemies.

You create multiple groups so that you can change the way sprites move or behave without impacting the movement or behavior of other sprites.

Game Speed

While testing the game you may have noticed that the enemies move a little fast. If not, then that’s okay, as different machines will see different results at this point.

The reason for this is that the game loop processes frames as fast as the processor and environment will allow. Since all the sprites move once per frame, they can move hundreds of times each second. The number of frames handled each second is called the frame rate, and getting this right is the difference between a playable game and a forgettable one.

Normally, you want as high a frame rate as possible, but for this game, you need to slow it down a bit for the game to be playable. Fortunately, the module time contains a Clock which is designed exactly for this purpose.

Using Clock to establish a playable frame rate requires just two lines of code. The first creates a new Clock before the game loop begins:

# Setup the clock for a decent framerate
clock = pygame.time.Clock()

The second calls .tick() to inform pygame that the program has reached the end of the frame:

# Flip everything to the display
pygame.display.flip()
# Ensure program maintains a rate of 30 frames per second
clock.tick(30)

The argument passed to .tick() establishes the desired frame rate. To do this, .tick() calculates the number of milliseconds each frame should take, based on the desired frame rate. Then, it compares that number to the number of milliseconds that have passed since the last time .tick() was called. If not enough time has passed, then .tick() delays processing to ensure that it never exceeds the specified frame rate.

Passing in a smaller frame rate will result in more time in each frame for calculations, while a larger frame rate provides smoother (and possibly faster) gameplay:

Play around with this number to see what feels best for you!

Sound Effects

So far, you’ve focused on gameplay and the visual aspects of your game. Now let’s explore giving your game some auditory flavor as well. pygame provides mixer to handle all sound-related activities. You’ll use this module’s classes and methods to provide background music and sound effects for various actions.

The name mixer refers to the fact that the module mixes various sounds into a cohesive whole. Using the music sub-module, you can stream individual sound files in a variety of formats, such as MP3, Ogg, and Mod. You can also use Sound to hold a single sound effect to be played, in either Ogg or uncompressed WAV formats. All playback happens in the background, so when you play a Sound, the method returns immediately as the sound plays.

Note: The pygame documentation states that MP3 support is limited, and unsupported formats can cause system crashes. The sounds referenced in this article have been tested, and we recommend testing any sounds thoroughly before releasing your game.

As with most things pygame, using mixer starts with an initialization step. Luckily, this is already handled by pygame.init(). You only need to call pygame.mixer.init() if you want to change the defaults:

# Setup for sounds. Defaults are good.
pygame.mixer.init()
# Initialize pygame
pygame.init()
# Set up the clock for a decent framerate
clock = pygame.time.Clock()

pygame.mixer.init() accepts a number of arguments, but the defaults work fine in most cases. Note that if you want to change the defaults, you need to call pygame.mixer.init() before calling pygame.init(). Otherwise, the defaults will be in effect regardless of your changes.

After the system is initialized, you can get your sounds and background music setup:

# Load and play background music
# Sound source: http://ccmixter.org/files/Apoxode/59262
# License: https://creativecommons.org/licenses/by/3.0/
pygame.mixer.music.load("Apoxode_-_Electric_1.mp3")
pygame.mixer.music.play(loops=-1)
# Load all sound files
# Sound sources: Jon Fincher
move_up_sound = pygame.mixer.Sound("Rising_putter.ogg")
move_down_sound = pygame.mixer.Sound("Falling_putter.ogg")
collision_sound = pygame.mixer.Sound("Collision.ogg")

Lines 138 and 139 load a background sound clip and begin playing it. You can tell the sound clip to loop and never end by setting the named parameter loops=-1.

Lines 143 to 145 load three sounds you’ll use for various sound effects. The first two are rising and falling sounds, which are played when the player moves up or down. The last is the sound used whenever there is a collision. You can add other sounds as well, such as a sound for whenever an Enemy is created, or a final sound for when the game ends.

So, how do you use the sound effects? You want to play each sound when a certain event occurs. For example, when the ship moves up, you want to play move_up_sound. Therefore, you add a call to .play() whenever you handle that event. In the design, that means adding the following calls to .update() for Player:

# Define the Player object by extending pygame.sprite.Sprite
# Instead of a surface, use an image for a better-looking sprite
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.image.load("jet.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.surf.get_rect()
    # Move the sprite based on keypresses
    def update(self, pressed_keys):
        if pressed_keys[K_UP]:
            self.rect.move_ip(0, -5)
            move_up_sound.play()
        if pressed_keys[K_DOWN]:
            self.rect.move_ip(0, 5)
            move_down_sound.play()

For a collision between the player and an enemy, you play the sound for when collisions are detected:

# Check if any enemies have collided with the player
if pygame.sprite.spritecollideany(player, enemies):
    # If so, then remove the player
    player.kill()
    # Stop any moving sounds and play the collision sound
    move_up_sound.stop()
    move_down_sound.stop()
    collision_sound.play()
    # Stop the loop
    running = False

Here, you stop any other sound effects first, because in a collision the player is no longer moving. Then you play the collision sound and continue execution from there.

Finally, when the game is over, all sounds should stop. This is true whether the game ends due to a collision or the user exits manually. To do this, add the following lines at the end of the program after the loop:

# All done! Stop and quit the mixer.
pygame.mixer.music.stop()
pygame.mixer.quit()

Technically, these last few lines are not required, as the program ends right after this. However, if you decide later on to add an intro screen or an exit screen to your game, then there may be more code running after the game ends.

That’s it! Test it again, and you should see something like this:

A Note on Sources

You may have noticed the comment on lines 136-137 when the background music was loaded, listing the source of the music and a link to the Creative Commons license. This was done because the creator of that sound required it. The license requirements stated that in order to use the sound, both proper attribution and a link to the license must be provided.

Here are some sources for music, sound, and art that you can search for useful content:

As you make your games and use downloaded content such as art, music, or code from other sources, please be sure that you are complying with the licensing terms of those sources.

Conclusion

Throughout this tutorial, you’ve learned how game programming with pygame differs from standard procedural programming. You’ve also learned how to:

  • Implement event loops
  • Draw items on the screen
  • Play sound effects and music
  • Handle user input

To do this, you used a subset of the pygame modules, including the display, mixer and music, time, image, event, and key modules. You also used several pygame classes, including Rect, Surface, Sound, and Sprite. But these only scratch the surface of what pygame can do! Check out the official pygame documentation for a full list of available modules and classes.

You can find all of the code, graphics, and sound files for this article by clicking the link below:

Clone Repo: Click here to clone the repo you'll use to learn how to use PyGame in this tutorial.

Feel free to leave comments below as well. Happy Pythoning!

Thanks for reading

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

☞ Python Programming Tutorial - Full Course for Beginners

☞ Python Tutorial - Python GUI Programming - Python GUI Examples (Tkinter Tutorial)

☞ Python Tutorial: Image processing with Python (Using OpenCV)

Originally published at https://realpython.com/

Python Online Multiplayer Game Development Tutorial

Python Online Multiplayer Game Development Tutorial

This Python online game tutorial will show you how to code a scaleable multiplayer game with python using sockets/networking and pygame. You will learn how to deploy your game so that people anywhere around the world can play against each other.


You will learn and understand the following after this tutorial:

• How a Client/Server System Works

• How to Develop Applications to an External Server

• How to Code a Client

• How to Code a Server

• Sending Encrypted Data Such as Objects Over a Port

• Connecting Multiple Clients to a Server

💻Code: https://techwithtim.net/wp-content/uploads/2019/03/networkTutrorial.zip

⭐️ Contents ⭐️

⌨️ (0:00:51) 1 - Creating a Client Using Sockets

⌨️ (0:16:55) 2 - Creating a Server

⌨️ (0:34:04) 3 - Sending & Receiving Information From Server

⌨️ (0:44:43) 4 - Connecting Multiple Clients

⌨️ (1:03:38) 5 - Sending Objects With Pickle!

⌨️ (1:14:52) 6 - Online Rock Paper Scissors


Learn More

Complete Python Bootcamp: Go from zero to hero in Python 3

Machine Learning A-Z™: Hands-On Python & R In Data Science

Python and Django Full Stack Web Developer Bootcamp

Python Tutorial for Beginners (2019) - Learn Python for Machine Learning and Web Development

Build Your First Open Source Python Project

An A-Z of useful Python tricks

A Complete Machine Learning Project Walk-Through in Python

How To Install Python 3 and Set Up a Programming Environment on Ubuntu 18.04

Python Tutorial for Beginners - Crash Course 2019 | Build a Game with Python

🎥 Tutorial from Tech with Tim. Check out his YouTube channel: https://www.youtube.com/techwithtim