One of the early topics covered when learning to code deals with the built-in data structures in Python. Lists are usually learned early on, followed by dictionaries and tuples. Sets are not normally one of the earliest topics covered. However, that’s not because they’re complex but because they’re used less often in Python. Understanding the similarities and differences between these data structures is important. But there’s more than just the ‘rules’ when using lists, tuples, dictionaries, and sets in Python.
In this article, you’ll write a simulation using lists, tuples, dictionaries, and sets in Python. The code will produce this animation:
The article’s principal aim is to practise using lists, tuples, dictionaries, and sets in Python and understand how each one is suited for a different purpose. The main purpose of the post is not to give a detailed explanation of the data structures. However, I’ll briefly review the basics of these built-in data structures in Python throughout the article.
You can find a lot more detail about lists in the Chapter about loops and lists in The Python Coding Book, and dictionaries and tuples are dealt with in the Chapter about data types.
Introducing the Chaotic Balls Animation
Have a look again at the video above showing the simulation in action. Can you guess the rules the simulation is following?
Here they are:
- The screen includes several tiles. There are three types of tiles that are identified by the colour of the outline: green, orange, or red.
- A ball appears randomly on the screen once every two seconds. Its direction of travel is also random.
- When a ball hits a green tile, it speeds up.
- When a ball hits a red tile, it slows down. When a ball slows down to zero, it disappears from the screen.
- When a ball hits an orange tile, it changes its direction of travel randomly.
- The colour of each ball indicates the ball’s speed.
You’ll use the turtle
module for this animation. This module is part of the standard library, so you don’t need to install it separately. You don’t need to have any previous experience with the turtle
module to get the most out of this article. It’s quite simple to use, and I’ll explain how it works throughout the article.
You’ll also need two other modules from the standard library: random
and time
.
A Quick Review of Lists and Tuples
Lists and tuples have many similarities. They’re both sequences in which the items are stored in order and can be referenced using an index showing the item’s position in the sequence:
>>> some_list = [4, 6, 7, 3, 2, 10, 4] >>> some_tuple = (4, 6, 7, 3, 2, 10, 4) >>> some_list[3] 3 >>> some_tuple[3] 3 >>> some_list[2:5] [7, 3, 2] >>> some_tuple[2:5] (7, 3, 2) >>> for number in some_list: ... print(number) ... 4 6 7 3 2 10 4 >>> for number in some_tuple: ... print(number) ... 4 6 7 3 2 10 4
Note that when creating a tuple, the parentheses ()
are optional. The following line creates the same tuple as the one in the example above:
>>> some_tuple = 4, 6, 7, 3, 2, 10, 4 >>> some_tuple (4, 6, 7, 3, 2, 10, 4)
The key difference between lists and tuples is that lists are mutable while tuples are immutable:
>>> some_list[2] = 100 >>> some_list [4, 6, 100, 3, 2, 10, 4] >>> some_tuple[2] = 100 Traceback (most recent call last): File "<input>", line 1, in <module> TypeError: 'tuple' object does not support item assignment
You can change, add, and remove items in a list, but you cannot do the same with tuples. Tuples are useful when you want to create a group of items that will not change in your code. You use a list for a container that’s meant to be flexible.
Getting Started: Setting Up the Animation
Let’s start setting up the animation and see where you need to use lists and where tuples would be more suitable. You can start by creating a window using the turtle
module and selecting its size and colour:
import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Temporary line to keep window open. We'll remove later turtle.done()
The name background_colour
stores the red, green, and blue (RGB) values representing the background colour. You can use a tuple to store RGB colour triplets. You also use a tuple for the width and height of the window, which you store in screen_size
.
The Create window section uses Screen()
from the turtle
module to create the window. The tracer()
method is used to control when things are drawn on the screen. Setting this to 0
means that you’ll be able to control when to refresh the screen by using the update()
method later on. The colormode()
method allows you to choose to represent colours as triplets of numbers between 0
and 255
to represent the RGB values.
setup()
is the method you can use to set the size of the window. This method needs two arguments to represent the width and height of the window in pixels. Therefore, you use the unpacking operator *
to unpack the tuple screen_size
into the two numbers that it contains. window.setup(*screen_size)
is the same as window.setup(screen_size[0], screen_size[1])
in this case since there are two items in screen_size
.
Finally, you change the window’s background colour using bgcolor()
which accepts a tuple with RGB values as an argument. When you run this code, you should see a square window with a grey background.
Creating the Balls
You can now set things up to create the balls that appear randomly on the screen at regular intervals. You’ll use a Turtle
object from the turtle
module for each ball. However, you want to store all the ball objects in the same place in your program. The data structure should be iterable so that you can go through it using a loop to deal with all the balls.
You also need the container to be flexible since you’ll add a new ball every two seconds, and you need to remove balls that have come to a standstill. This is an ideal scenario for creating a list. You can initialise an empty list, ready to store the balls as they are created. You can then define a function create_new_ball()
to create a ball at a random position and orientation:
import random import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 balls.append(ball) create_new_ball() # Start animation with one ball # Temporary lines. We'll remove later window.update() turtle.done()
Once you create an instance of the turtle.Turtle
class, you call several of its methods:
penup()
ensures that no lines are drawn when aTurtle
object movesshape()
changes the shape of the object displayed. Here, you’re changing the shape to a circle.pencolor()
selects the colour of any lines drawn by theTurtle
. As you calledpenup()
earlier, no lines will be drawn. However, the outline of the circle displayed will now be white.setposition()
moves theTurtle
object to the x– and y-coordinates given as arguments. The centre of the screen is represented by the coordinates (0, 0). Therefore, the first argument is a random number between-400
and400
since the width is800
. The second argument follows the same principle but uses the height of the window, which is the second item inscreen_size
. You use floor division//
to ensure the result is an integer asrandom.randint()
needs integer arguments.setheading()
changes the orientation of theTurtle
object. You will use theforward()
method later, which will move theTurtle
object in the direction the object is facing.
ball_speed
is not an attribute of the Turtle
class. You’re creating an instance variable with the line ball.ball_speed = 0.5
. If you want to brush up on this topic, you can read more about Python instance variables. Each ball will have its own speed as balls will speed up or slow down at different rates.
Each time you call create_new_ball()
, the program will create a new Turtle
representing a ball and add it to the list balls
. You call the function once right away so that there’s one ball at the start of the animation. You’ll call the function again later to create more balls. For the time being, when you run this code, you see a single, stationary ball placed in a random position on the screen. The ball has a white outline since you set this to white when you called pencolor()
. The rest of the ball is black, which is the default colour. You’ll change this colour later:
It’s now time to add movement to the animation.
Creating the Main Loop to Move the Balls
All animations will need a main loop to run through each frame of the animation. You can use a while True
loop in this case. Although you only have one ball in the animation, you know that all the balls will be represented by Turtle
objects stored in the list balls
. Therefore, you can iterate through this list in the while
loop to move the balls. You can also take care of what happens when the ball leaves the window from either of the four edges: left, right, top, or bottom. Here are the additions you’ll need to your code:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 balls.append(ball) create_new_ball() # Start animation with one ball # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) window.update() time.sleep(0.001)
You’ve now added a while
loop. Each iteration of this loop represents one frame of the animation. The while
loop so far consists of the following:
- A
for
loop that iterates through the list containing all theTurtle
objects representing the balls - The call to the
forward()
method of theTurtle
class. This method moves theTurtle
forward by the number of pixels given as an argument. The argument isball.ball_speed
. The instance variableball_speed
is one you have created increate_new_ball()
and eachTurtle
will have its own value. TheTurtle
will move in the direction it is facing, which you’ve set to a random value increate_new_ball()
. - Two
if
statements. These statements are needed to check whether the ball has left the screen through any of the four sides.- The methods
setx()
andsety()
are similar tosetposition()
which you used earlier. However, they only change one of theTurtle
object’s coordinates at a time instead of both. - The methods
xcor()
andycor()
return theTurtle
object’s x– and y-coordinates. - The
abs()
built-in function returns the absolute value of its argument. In this case, as the value will be a float, the function will always return the positive value of the difference between the ball’s coordinate and the screen’s half-width or half-height. This allows you to test for the left and right edges in the same statement and for the top and bottom edges in another.
- The methods
- The call to
update()
refreshes the display on the screen. This method is used withtracer(0)
to control when things are drawn in the animation. By placingwindow.update()
in thewhile
loop, you refresh the image once per frame. time.sleep()
introduces a small delay in the loop. You’ll need to import thetime
built-in module, too. In this animation, you’re not controlling the speed of each frame strictly. Instead, yourwhile
loop will run at whatever speed your computer allows it to! This means that the speed of the animation will vary from computer to computer. Putting in a small delay in thewhile
loop allows you to control the overall speed of the animation. Change the value used as an argument fortime.sleep()
to suit your computer speed. If your animation is too fast, use a larger number.
You’ve also removed the temporary lines you had at the bottom of your code earlier. You no longer need these lines now that the while
loop is in place.
This code gives output similar to the following video:
The ball will appear in a random position and move in a random direction. It should reappear at the opposite end of the screen whenever it leaves the window through any of the four edges.
Creating a Grid
You can now turn your attention to creating the tiles. You can create a virtual grid and work out how the grid maps to the whole screen. In the code below, you’ll create a 16x16
grid. Since the screen is 800x800
pixels, each cell of the grid will be 50x50
pixels, since 800÷16=50
.
However, you don’t want every one of the 16x16
cells of the grid to include a tile. In the animation, there are gaps where there are no tiles. You can now define some parameters at the top of your code to set up the grid:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) # ...
grid_size
and grid_scale
are both tuples containing two values representing the x– and y-values. grid_scale
contains the size in pixels of each cell in the grid. In this example, this is 50x50
.
You’ve then set the value for fraction_of_grid_points
to 0.35
. This means that 35% of all the 16x16
grid cells will be filled with tiles. The result of this calculation is stored in n_tiles
.
A Quick Review of Dictionaries and Sets
In this tutorial, you’re practising using lists, tuples, dictionaries, and sets in Python. You’ve already used lists and tuples. Now, it’s time for a quick review of the other two data structures you’re using in this example.
A dictionary is a mapping linking a key to a value. Each item in a dictionary consists of a key-value pair:
>>> some_dictionary = {"James": 10, "Mary": 20, "Kate": 15} >>> some_dictionary["James"] 10
The values of a dictionary can be of any data type, including other data structures. The values can also be function names:
>>> another_dict = {"first": print, "second": str.upper} >>> another_dict["first"] <built-in function print> >>> another_dict["first"]("hello") hello >>> another_dict["second"]("hello") 'HELLO'
The value of another_dict["first"]
is the function print
. Therefore, another_dict["first"]("hello")
is the same as the function call print("hello")
.
The key of a dictionary cannot be any data type, though. Have a look at the following examples:
>>> one_more_dictionary = {[1, 2]: "hello"} Traceback (most recent call last): File "<input>", line 1, in <module> TypeError: unhashable type: 'list' >>> one_more_dictionary = {(1, 2): "hello"} >>> one_more_dictionary {(1, 2): 'hello'}
Keys need to be hashable. You can see that when you tried to use a list as a key in the dictionary, you got an ‘unhashable type’ error. However, tuples can be used.
Sets share the same type of bracket with dictionaries, the curly brackets {}
, but items within a set are individual items and not pairs:
>>> some_set = {4, 6, 7, 6, 3, 4, 5, 4} >>> type(some_set) <class 'set'> >>> some_set {3, 4, 5, 6, 7}
Each value in a set must be distinct and therefore can only appear once. In the example above, you can see that the repeated values have been excluded from the set.
Note that when you want to create an empty set, you cannot use the same method as with lists, tuples, and dictionaries since the curly brackets default to an empty dictionary:
>>> a = [] >>> type(a) <class 'list'> >>> b = () >>> type(b) <class 'tuple'> >>> c = {} >>> type(c) <class 'dict'> >>> d = set() >>> type(d) <class 'set'>
Before going back to the animation code, we should have a quick word about comprehensions for lists, tuples, dictionaries, and sets.
Comprehensions
When using lists, tuples, dictionaries, and sets in Python, you’ll often need to initialise the empty data structure and then populate it with values. Often, you can use comprehensions to do this:
>>> some_list = [4, 6, 7, 3, 2, 10, 4] >>> some_list [4, 6, 100, 3, 2, 10, 4] >>> new_list = [item * 2 for item in some_list] >>> new_list [8, 12, 200, 6, 4, 20, 8] >>> new_set = {item * 2 for item in some_list} >>> new_set {4, 6, 8, 200, 12, 20}
You can use the same method for dictionaries by defining both the key and the value in the comprehension:
>>> names = ["James", "Mary", "Kate"] >>> numbers = [10, 20, 15] >>> some_dictionary = {key: value for key, value in zip(names, numbers)} >>> some_dictionary {'James': 10, 'Mary': 20, 'Kate': 15}
When using comprehensions to populate tuples, you need to beware of a common error:
>>> some_numbers = (item * 2 for item in some_list) >>> some_numbers <generator object <genexpr> at 0x7fe68991b3c0> >>> some_numbers = tuple(item * 2 for item in some_list) >>> some_numbers (8, 12, 14, 6, 4, 20, 8)
The expression in the parentheses ()
alone returns a generator and not a tuple. You can use the tuple()
function with a comprehension expression to create a tuple.
Adding Tiles to the Screen
You’ve created the parameters grid_size
and grid_scale
earlier that allow you to create a grid and map it to the screen size in pixels. You’ll read more about this mapping between grid and screen later on. You also defined fraction_of_grid_points_used
as 0.35
earlier, or 35% of all grid cells. This leads to 89
tiles in this animation. Therefore, you need to select 89
random pairs of grid coordinates which will host the tiles.
Choosing the Tile Coordinates
However, you need to ensure that the program selects 89
unique pairs of grid coordinates. One way of achieving this is by using a set:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 balls.append(ball) create_new_ball() # Start animation with one ball # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) window.update() time.sleep(0.001)
You initialised an empty set and used the uniqueness property of sets to run a while
loop until the required number of coordinates are reached. The grid coordinates that the program chooses range from (0, 0) to (15, 15). You can add a call to print(tile_grid_coords)
after the loop to display the grid coordinates chosen if you wish.
Tile Colours and Actions
Before you’re ready to draw the tiles, you’ll need to link each tile colour with an action. When a ball hits a tile, it will perform a specific action depending on that tile’s colour.
The three actions that a ball can perform are:
- Increase speed if the ball hits a green tile
- Decrease speed if the ball hits a red tile
- Change direction randomly if the ball hits an orange tile
You can start by defining these three functions, each taking a Turtle
object’s name as an input argument. You also define two new parameters to set the maximum speed that a ball can reach, to avoid balls going too fast, and the step size you’d like to use to increase and decrease the ball speed each time it hits a green or red tile:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) max_ball_speed = 2 ball_speed_step = 0.2 # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 balls.append(ball) create_new_ball() # Start animation with one ball # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) window.update() time.sleep(0.001)
The functions are described in the docstrings for each one. Type hinting is used to improve readability, showing that the input argument should be a Turtle
object.
The balls are removed from the list balls
when they become stationary, and they cannot exceed the maximum ball speed you set in the parameters at the top of your code.
Mapping Tile Colours to Ball Actions
Your next step is to map the tile colours to each of these actions. Dictionaries are an ideal data structure to create these mappings. As you’ve seen earlier, you can use tuples as keys in a dictionary, and the value can be a function name. You can create a dictionary called actions
which maps RGB colour triplets to the function names representing actions:
# ... # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # ...
The tuples used as keys in the dictionary actions
represent the light green, red, and orange colours used in this animation. Of course, you can choose your own favourite colours if you wish!
You’re now ready to assign a colour to each tile. You can create another dictionary named tiles
which uses the tuples containing the tile coordinates as keys and a colour as a value. This dictionary will contain items in the following format:
{(2, 3): (144, 238, 144), (7, 2): (255, 127, 80), ...}
Each pair of tile coordinates is mapped onto a colour from the three colours available. You can create the dictionary tiles
using a dictionary comprehension:
# ... # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # Create tiles tiles = { coord: random.choice(tuple(actions.keys())) for coord in tile_grid_coords } # ...
You loop through tile_grid_coords
in the dictionary comprehension and place each item as a key in the dictionary. For each key, you choose a random colour as a value. Since the colours available are the keys of the dictionary named actions
, you can use actions.keys()
as an argument for random.choice()
once you convert to a sequence such as a tuple. You can print(tiles)
if you wish to display the set of tiles and their colours.
Converting Between Grid Coordinates and Screen Coordinates
You have to deal with two sets of coordinates in this program:
- The grid coordinates represent the cells in the
16x16
grid. The bottom-left cell is (0, 0), and the top-right cell is (15, 15). - The screen coordinates correspond to each pixel on the screen. In the
Turtle
module, the centre of the screen has the coordinates (0, 0). Therefore, the screen coordinates include negative and positive values to represent all four quadrants of the screen.
The illustration below shows the relationship between grid coordinates and screen coordinates for a 4x4
grid. The grid coordinates are shown using square brackets and the screen coordinates using round brackets in this illustration:
In the code, the grid is 16x16
instead of 4x4
. The smaller grid was only used in the drawing to make it easier to illustrate.
You can now write a couple of functions to convert between the two coordinate systems. You can add these helper functions immediately after defining the parameters at the top of the code:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) max_ball_speed = 2 ball_speed_step = 0.2 # Functions to convert between grid and screen coordinates def convert_grid_to_screen_coords(grid_coords): return ( grid_coords[0] * grid_scale[0] - screen_size[0]/2 + grid_scale[0]/2, grid_coords[1] * grid_scale[1] - screen_size[1]/2 + grid_scale[1]/2, ) def convert_screen_to_grid_coords(screen_coords): return ( round( (screen_coords[0] - grid_scale[0]/2 + screen_size[0]/2) / grid_scale[0] ), round( (screen_coords[1] - grid_scale[1]/2 + screen_size[1]/2) / grid_scale[1] ), ) # ...
In the function convert_grid_to_screen_coords()
, a pair of grid coordinates such as (3, 1) is converted to the screen coordinates at the centre of the grid cell. The steps in the function are as follows:
- The input argument is a tuple containing the grid coordinates.
- The return value is another tuple containing the screen coordinates at the centre of the cell.
- The grid coordinates are multiplied by the
grid_scale
first. This is the size of each cell in the grid in pixels. This gives the left-most pixel when index0
is used in the tuple indexing or the bottom-most pixel when1
is used. - Since the grid coordinates start at the bottom left while the screen coordinates are centred at the middle of the screen, you need to subtract half the width or height of the screen.
- You now need to add half the
grid_scale
value to move from the bottom-left pixel of the grid cell to the centre pixel of the cell.
In the function convert_screen_to_grid_coords()
, the screen coordinates of any pixel are converted to the grid coordinates of the cell that contains that pixel:
- The input argument is a tuple containing the screen coordinates of a pixel.
- The return value is another tuple containing the grid coordinates for the grid which contains the pixel.
- The calculation is the reverse of the one described for
convert_grid_to_screen()
. The result is rounded to give the integers needed for the grid coordinate system.
There’s a bit more detail about the transformations occurring in these functions in an appendix to this article.
Drawing the Tiles
It’s time to draw the tiles on the screen. You can create a new Turtle
object to draw the tiles and then loop through the dictionary tiles
to draw each one.
The keys in tiles
are the grid coordinates of the cell, and the values are the colours. The steps needed to draw the tiles are the following:
- Loop through
tiles.items()
and assign the keys to the namecoord
and the values to the namecolour
. - Convert grid coordinates to screen coordinates.
- Move the
Turtle
object to the bottom-left region of the cell, allowing for a margin so that tiles are not in contact with each other. The factor of0.9
is used for this. - Change the
Turtle
object’s colour to the colour associated with the tile, which is stored in thetiles
dictionary. - Draw a square with the
Turtle
object. The factor of0.8
ensures that a margin is left between the tile drawn and the edge of the cell.
You can add this loop to your code:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) max_ball_speed = 2 ball_speed_step = 0.2 # Functions to convert between grid and screen coordinates def convert_grid_to_screen_coords(grid_coords): return ( grid_coords[0] * grid_scale[0] - screen_size[0]/2 + grid_scale[0]/2, grid_coords[1] * grid_scale[1] - screen_size[1]/2 + grid_scale[1]/2, ) def convert_screen_to_grid_coords(screen_coords): return ( round( (screen_coords[0] - grid_scale[0]/2 + screen_size[0]/2) / grid_scale[0] ), round( (screen_coords[1] - grid_scale[1]/2 + screen_size[1]/2) / grid_scale[1] ), ) # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # Create tiles tiles = { coord: random.choice(tuple(actions.keys())) for coord in tile_grid_coords } # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 balls.append(ball) create_new_ball() # Start animation with one ball # Draw tiles on screen grid_draw = turtle.Turtle() grid_draw.penup() grid_draw.hideturtle() for coord, colour in tiles.items(): coords = convert_grid_to_screen_coords(coord) grid_draw.setposition( coords[0] - grid_scale[0] / 2 * 0.9, coords[1] - grid_scale[1] / 2 * 0.9 ) grid_draw.color(colour) grid_draw.pendown() for _ in range(2): grid_draw.forward(grid_scale[0] * 0.8) grid_draw.left(90) grid_draw.forward(grid_scale[1] * 0.8) grid_draw.left(90) grid_draw.penup() # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) window.update() time.sleep(0.001)
When you run this code, you’ll see the single ball moving across the screen over the drawings of the tiles:
Before adding more balls to the animation, you can deal with the interactions between the ball and the tiles it hits.
Creating Interactions Between Balls and Tiles
You only have one ball in the animation so far. However, any steps you take in the main animation loop will apply to all balls in the animation since you’re looping through the list balls
.
The steps required to detect when a ball hits a tile and to perform the required actions on the ball are the following:
- Find which cell in the grid the ball is currently in.
- Check whether that cell has a tile on it.
- If the ball is on a tile, find the colour of the tile and what action is associated with that colour.
- Implement the required action on the ball.
There is another pitfall you’ll need to be careful about. The ball is moving in small steps, and therefore, it will overlap on a single tile for several iterations of the main animation loop. However, you only want the action to be performed when the ball first hits a tile. You can add another instance variable to each ball to store the last tile the ball has hit and then add this as an additional check to determine whether a ball has just hit a tile.
You can add the new instance variable and make additions to the main animation loop:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) max_ball_speed = 2 ball_speed_step = 0.2 # Functions to convert between grid and screen coordinates def convert_grid_to_screen_coords(grid_coords): return ( grid_coords[0] * grid_scale[0] - screen_size[0]/2 + grid_scale[0]/2, grid_coords[1] * grid_scale[1] - screen_size[1]/2 + grid_scale[1]/2, ) def convert_screen_to_grid_coords(screen_coords): return ( round( (screen_coords[0] - grid_scale[0]/2 + screen_size[0]/2) / grid_scale[0] ), round( (screen_coords[1] - grid_scale[1]/2 + screen_size[1]/2) / grid_scale[1] ), ) # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # Create tiles tiles = { coord: random.choice(tuple(actions.keys())) for coord in tile_grid_coords } # Create balls balls = [] def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 ball.current_grid = None balls.append(ball) create_new_ball() # Start animation with one ball # Draw tiles on screen grid_draw = turtle.Turtle() grid_draw.penup() grid_draw.hideturtle() for coord, colour in tiles.items(): coords = convert_grid_to_screen_coords(coord) grid_draw.setposition( coords[0] - grid_scale[0] / 2 * 0.9, coords[1] - grid_scale[1] / 2 * 0.9 ) grid_draw.color(colour) grid_draw.pendown() for _ in range(2): grid_draw.forward(grid_scale[0] * 0.8) grid_draw.left(90) grid_draw.forward(grid_scale[1] * 0.8) grid_draw.left(90) grid_draw.penup() # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) # Check whether ball hit tile and perform required action ball_grid_coords = convert_screen_to_grid_coords(ball.position()) if ( ball_grid_coords in tiles.keys() and ball_grid_coords != ball.current_grid ): colour = tiles[ball_grid_coords] actions[colour](ball) ball.current_grid = ball_grid_coords window.update() time.sleep(0.001)
The if
statement you just added in the while
loop contains two conditions:
- The ball must be on a tile. You verify this by checking whether the tuple containing the grid coordinates of the ball’s current position is one of the keys in the dictionary
tiles
. - The tile the ball is currently on must not be the same one as in the previous iteration.
When both conditions are met, you perform the following steps:
- You get the tile’s colour from the dictionary
tiles
and store it in the variablecolour
. - You get the name of the function mapped to the colour and call the function with
ball
as its argument. This is the same technique summarised in the section reviewing dictionaries above.actions
is a dictionary, and therefore,actions[colour]
gives the value associated with the tuplecolour
. This value is a function name (speed_up
,slow_down
, orchange_direction
). - You assign the current grid coordinates to the instance variable
ball.current_grid
so that these actions are not performed in the next iterations if the ball is still on this tile.
The output from the code so far gives the following output:
Note that as the tiles’ positions and colours and the ball’s position and orientation are all random, the outcome of each run will be different. When there’s only one ball, it is possible that this ball will be short-lived if it hits too many red tiles early on!
Using Colour to Show Ball Speed
You’ll indicate the speed of the ball by changing the ball’s colour. To achieve this, you’ll first need to select a colour for the balls. You can add this to the parameters at the top of your code.
Then, you can add a function that works out the right shade of that colour based on the speed of the ball. This function works out what fraction of the maximum speed the ball’s current speed is and scales the red, green, and blue values of the ball’s colour accordingly. You can use fillcolor()
, which is another Turtle
method, to fill the shape of the ball:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) ball_colour = 0, 191, 255 max_ball_speed = 2 ball_speed_step = 0.2 # Functions to convert between grid and screen coordinates def convert_grid_to_screen_coords(grid_coords): return ( grid_coords[0] * grid_scale[0] - screen_size[0]/2 + grid_scale[0]/2, grid_coords[1] * grid_scale[1] - screen_size[1]/2 + grid_scale[1]/2, ) def convert_screen_to_grid_coords(screen_coords): return ( round( (screen_coords[0] - grid_scale[0]/2 + screen_size[0]/2) / grid_scale[0] ), round( (screen_coords[1] - grid_scale[1]/2 + screen_size[1]/2) / grid_scale[1] ), ) # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # Create tiles tiles = { coord: random.choice(tuple(actions.keys())) for coord in tile_grid_coords } # Create balls balls = [] def change_ball_colour(ball): fraction_of_max_speed = ball.ball_speed / max_ball_speed ball.fillcolor( int(ball_colour[0] * fraction_of_max_speed), int(ball_colour[1] * fraction_of_max_speed), int(ball_colour[2] * fraction_of_max_speed), ) def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 ball.current_grid = None change_ball_colour(ball) balls.append(ball) create_new_ball() # Start animation with one ball # Draw tiles on screen grid_draw = turtle.Turtle() grid_draw.penup() grid_draw.hideturtle() for coord, colour in tiles.items(): coords = convert_grid_to_screen_coords(coord) grid_draw.setposition( coords[0] - grid_scale[0] / 2 * 0.9, coords[1] - grid_scale[1] / 2 * 0.9 ) grid_draw.color(colour) grid_draw.pendown() for _ in range(2): grid_draw.forward(grid_scale[0] * 0.8) grid_draw.left(90) grid_draw.forward(grid_scale[1] * 0.8) grid_draw.left(90) grid_draw.penup() # Main animation loop while True: for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) # Check whether ball hit tile and perform required action ball_grid_coords = convert_screen_to_grid_coords(ball.position()) if ( ball_grid_coords in tiles.keys() and ball_grid_coords != ball.current_grid ): colour = tiles[ball_grid_coords] actions[colour](ball) ball.current_grid = ball_grid_coords change_ball_colour(ball) window.update() time.sleep(0.001)
You call change_ball_colour()
in the function that creates the balls and in the main animation loop when a ball changes speed. The output of the code now looks like this:
Adding More Balls at Regular Intervals
The last step is to add more balls. You can define a parameter to set the time interval between new balls being created and then set a timer that resets every interval after creating a new ball.
Here’s the final version of the Chaotic Balls animation code:
import random import time import turtle # Parameters to set up animation background_colour = 50, 50, 50 screen_size = 800, 800 grid_size = 16, 16 grid_scale = ( screen_size[0] / grid_size[0], screen_size[1] / grid_size[1] ) fraction_of_grid_points_used = 0.35 n_tiles = int( fraction_of_grid_points_used * grid_size[0] * grid_size[1] ) ball_colour = 0, 191, 255 new_ball_interval = 2 max_ball_speed = 2 ball_speed_step = 0.2 # Functions to convert between grid and screen coordinates def convert_grid_to_screen_coords(grid_coords): return ( grid_coords[0] * grid_scale[0] - screen_size[0]/2 + grid_scale[0]/2, grid_coords[1] * grid_scale[1] - screen_size[1]/2 + grid_scale[1]/2, ) def convert_screen_to_grid_coords(screen_coords): return ( round( (screen_coords[0] - grid_scale[0]/2 + screen_size[0]/2) / grid_scale[0] ), round( (screen_coords[1] - grid_scale[1]/2 + screen_size[1]/2) / grid_scale[1] ), ) # Create window window = turtle.Screen() window.tracer(0) window.colormode(255) window.setup(*screen_size) window.bgcolor(background_colour) # Choose grid coordinates that will contain tiles tile_grid_coords = set() while len(tile_grid_coords) < n_tiles: tile_grid_coords.add( ( random.randint(0, grid_size[0] - 1), random.randint(0, grid_size[1] - 1) ) ) # Define actions based on grid point colour def speed_up(ball: turtle.Turtle): """Increase ball speed until it reaches max_ball_speed""" ball.ball_speed += ball_speed_step if ball.ball_speed > max_ball_speed: ball.ball_speed = max_ball_speed def slow_down(ball: turtle.Turtle): """Decrease ball speed. Hide and remove from list when stationary""" ball.ball_speed -= ball_speed_step if ball.ball_speed < ball_speed_step: ball.hideturtle() balls.remove(ball) def change_direction(ball: turtle.Turtle): """Rotate Turtle object by a random angle in [-90, 90] range""" ball.left(random.randint(-90, 90)) # Map colours to ball actions actions = { (144, 238, 144): speed_up, (220, 20, 60): slow_down, (255, 127, 80): change_direction, } # Create tiles tiles = { coord: random.choice(tuple(actions.keys())) for coord in tile_grid_coords } # Create balls balls = [] def change_ball_colour(ball): fraction_of_max_speed = ball.ball_speed / max_ball_speed ball.fillcolor( int(ball_colour[0] * fraction_of_max_speed), int(ball_colour[1] * fraction_of_max_speed), int(ball_colour[2] * fraction_of_max_speed), ) def create_new_ball(): ball = turtle.Turtle() ball.penup() ball.shape("circle") ball.pencolor("white") ball.setposition( random.randint(-screen_size[0] // 2, screen_size[0] // 2), random.randint(-screen_size[1] // 2, screen_size[1] // 2) ) ball.setheading(random.randint(0, 359)) ball.ball_speed = 0.5 ball.current_grid = None change_ball_colour(ball) balls.append(ball) create_new_ball() # Start animation with one ball # Draw tiles on screen grid_draw = turtle.Turtle() grid_draw.penup() grid_draw.hideturtle() for coord, colour in tiles.items(): coords = convert_grid_to_screen_coords(coord) grid_draw.setposition( coords[0] - grid_scale[0] / 2 * 0.9, coords[1] - grid_scale[1] / 2 * 0.9 ) grid_draw.color(colour) grid_draw.pendown() for _ in range(2): grid_draw.forward(grid_scale[0] * 0.8) grid_draw.left(90) grid_draw.forward(grid_scale[1] * 0.8) grid_draw.left(90) grid_draw.penup() # Main animation loop start_timer = time.time() while True: # Create new ball every time interval elapses if time.time() - start_timer > new_ball_interval: create_new_ball() start_timer = time.time() for ball in balls: # Move ball ball.forward(ball.ball_speed) # If ball goes out of bounds, move to other side if abs(ball.xcor()) > screen_size[0] / 2: ball.setx(-ball.xcor()) if abs(ball.ycor()) > screen_size[1] / 2: ball.sety(-ball.ycor()) # Check whether ball hit tile and perform required action ball_grid_coords = convert_screen_to_grid_coords(ball.position()) if ( ball_grid_coords in tiles.keys() and ball_grid_coords != ball.current_grid ): colour = tiles[ball_grid_coords] actions[colour](ball) ball.current_grid = ball_grid_coords change_ball_colour(ball) window.update() time.sleep(0.001)
And the output of this code is the following animation:
Final Words
In this article, you used the main built-in data structures in Python in a visual animation including many balls flying around a screen with many coloured tiles. The balls interact with each tile depending on the tile’s colour.
When learning about using lists, tuples, dictionaries, and sets in Python, it’s important to write some simple, short, code snippets to explore these data structures. But there’s also a lot of benefit in using them in a more elaborate manner.
This article and the Chaotic Balls simulation aim to demonstrate an alternative way of using lists, tuples, dictionaries, and sets in Python.
Each of these data structures has its own purposes:
- You used tuples to store the coordinates and the RGB colours since these don’t need to be flexible containers. Using tuples also allowed you to use them as keys in dictionaries, which you wouldn’t have been able to do if you had used lists.
- You used a list to store all the balls in the animation. This needs to be a flexible container as the number of balls increases and decreases throughout the animation. You need to store the balls in the same data structure to make use of loops to deal with all the balls in the animation effectively.
- You used a set when you needed to ensure the pairs of coordinates you created randomly for the tiles were unique.
- You used dictionaries to store the mappings between several bits of information in your code, including the mapping between tile colours and ball actions, and the mapping between the tile coordinates and their colours.
Now you can run the code and watch the hypnotic movement of the balls for a few minutes. You’ve earned the break!
Further Reading
- Read more about lists in the Chapter about loops and lists in The Python Coding Book
- You can also read about linked lists and how they compare with lists in the article about stacks and queues
- You’ll find out more about dictionaries and tuples, including a word analysis project using dictionaries, in the Chapter about data types in The Python Coding Book
- The example in this article used instance variables defined directly on instances of the class
turtle.Turtle
. You can read the article about Python instance variables and the full Chapter about Object-Oriented Programming in The Python Coding Book
Python 3.9 was used for the code in this article
Get the latest blog updates
No spam promise. You’ll get an email when a new blog post is published
Appendix: Converting Between Grid and Screen Coordinates
Let’s look back at the simplified illustration of the screen and grid coordinates:
There are three pixels that have been labelled on this illustration: (0, 0), (20, 20), and (-380, -380). Screen coordinates are shown using round brackets in the diagram.
The grid cells are shown using the dashed lines. There are 16
cells in all as this illustration shows a 4x4
grid. Some of the grid cells are labelled using their grid coordinates. These are shown using square brackets.
There are three transformations needed to convert from one coordinate system to the other. Shifts in coordinate systems are obtained through addition and subtraction. Scaling is obtained through multiplication and division:
- We are assuming that the grid coordinates represent the centre of a cell. Therefore there’s a shift of half the width of a cell horizontally and half the height of a cell vertically. This is a shift of
grid_scale / 2
. - The origin of the
turtle
screen is at the centre whereas the grid coordinates have the origin at the bottom left. This leads to another shift ofscreen_size / 2
. - Finally, there’s a scaling from an
NxN
grid to anMxM
array of pixels, or vice versa.
Let’s now work through the algorithms in the two functions convert_grid_to_screen_coords()
and convert_screen_to_grid_coords()
to understand each step. For this example, we’ll use the 4x4
grid set up, for simplicity.
Therefore, the parameters you’ll need are:
grid_size = 4, 4
screen_size = 800, 800
grid_scale = 800/4, 800/4 = 200, 200
convert_grid_to_screen_coords()
Consider the grid cell (2, 2) in the diagram. This is labelled with square brackets in the diagram to avoid confusion with screen coordinates, but I’ll stick to round brackets in the text. The aim of this function is to find the screen coordinates of the pixel at the centre of this cell.
- The tuple
(2, 2)
is the argument for the functionconvert_grid_to_screen_coords()
. - Each number in the tuple is multiplied by
grid_scale
, which gives(400, 400)
. This is the scaling needed to go from a4x4
grid to an800x800
array. If the screen coordinates also had the origin at the bottom left of the screen,(400, 400)
would represent the bottom left corner of the cell. - However, in
turtle
, the screen coordinates (0, 0) represent the centre of the screen. Therefore, you subtract the half-width and half-height of the screen to account for this. This is a shift, or translation, between the two coordinate systems. Since the screen size is800x800
pixels, this results in the tuple(400-400, 400-400)
, or more simply(0, 0)
. The bottom left corner of the cell with grid coordinates (2, 2) is the pixel with screen coordinates (0, 0). - However, if you want the pixel at the centre of the cell, you’ll need to account for the size of each cell in pixels. Specifically, you need to add half the size of each cell, that’s
grid_scale[i] / 2
.i
represents either1
or2
depending on whether it’s the x- or y-coordinates. The function outputs the tuple(100, 100)
. This means that the pixel at the centre of grid cell (2, 2) is the pixel with screen coordinates (100, 100).
convert_screen_to_grid_coords()
Consider the pixel with screen coordinates (20, 20) in the illustration. The aim of this function is to find the grid coordinates of the cell that contains this pixel.
- The tuple
(20, 20)
is the argument for the functionconvert_screen_to_grid_coords()
. - When you subtract
grid_scale[i] / 2
, which is the half-width and half-height of a cell, you get(-80, -80)
. This shift takes into account the fact that the grid coordinates represent the centre of a cell. - Next, you account for the difference in the origin between the grid coordinates and the screen coordinates with another shift. Since you’re converting from screen to grid coordinates, you add the half-width and half-height of the screen. This gives the
(-80+400, -80+400)
which is(320, 320)
. - Finally, you scale down from an array of pixels to cells in a grid. You achieve this by dividing
(320, 320)
bygrid_scale[i]
to give(1.6, 1.6)
and round to the nearest integer, giving the output(2, 2)
. Therefore the pixel (20, 20) lies within the cell with grid coordinates (2, 2).
The post Practise Using Lists, Tuples, Dictionaries, and Sets in Python With the Chaotic Balls Animation appeared first on The Python Coding Book.
from Planet Python
via read more
No comments:
Post a Comment