Let's now build a simple Snake game. As usual, we will be making use of the Canvas widget to provide the platform for our Snake program.
We will use canvas.create_line
to draw our snake, and canvas.create_rectangle
to draw the snake-food.
One of the primary objectives for this project is to introduce the Queue implementation in Python as we used it in conjunction with the threading module.
So far, we have built single-threaded applications. However, threading can be difficult to handle when there is more than one thread in an application, and these threads need to share attributes or resources among them. In this case, you cannot predict the thread execution order at all. OS does it very randomly and swiftly each time.
To handle this complexity, threading module provides some synchronization tools, such as locks, join, semaphores, events, and condition variables. However, it is—in most cases—safer and simpler to use queues. Simply put, a queue is a compound memory structure that is thread-safe; queues effectively channel access to a resource to multiple threads in a sequential order, and are a recommended design pattern that uses threads for most of the scenarios that require concurrency.
The Queue module provides a way to implement different kinds of queuing, such as FIFO (default implementation), LIFO queue, and Priority queue, and this module comes with a built-in implementation of all locking semantics required for running multithreaded programs.
Here's a quick roundup of the basic usage of the Queue module:
myqueue = Queue() #create empty queue myqueue.put(data)# put items into queue task = myqueue.get () #get the next item in the queue myqueue.task_done() # called when a queued task has completed myqueue.join() # called when all tasks in queue get completed
Let's see a simple demonstration of using queue to implement a multithreaded application (refer to 7.02 threading with queue.py
available in the code bundle):
import Queue import threading class Worker(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: task = self.queue.get() self.taskHandler(task) def taskHandler(self, job): print'doing task %s'%job self.queue.task_done() def main(tasks): queue = Queue.Queue() for task in tasks: queue.put(task) # create some threads and assign them queue for i in range(6): mythread = Worker(queue) mythread.setDaemon(True) mythread.start() queue.join() print'all tasks completed' if __name__ == "__main__": tasks = 'A B C D E F'.split() main(tasks)
The description of the code is as follows:
Worker
class, which inherits from the threading
module of Python. The __init__
method takes in a queue as its argument.run
method of the threading
module to get each item from the queue using queue.get()
, which is then passed on to the taskHandler
method, which actually executes the task specified in the current queue item. In our example, it does nothing useful but printing the name of the task.taskHandler
method, it sends a signal to the queue telling that the task has been completed using the queue.task_done()
method.Worker
class, we create an empty queue in our main()
method. This queue is populated with a list of tasks using queue.put(task)
.mythread.setDaemon(True)
method. Doing this passes control to our main program once all threads have completed execution. If you comment out the line, the program would still run, but would fail to exit after all threads have completed executing the tasks in the queue. Without the daemon threads, you'd have to keep track of all the threads and tell them to exit before your program could completely quit.queue.join()
method ensures that the program flow waits there until the queue is empty.Now that we know how to use queues to handle multithreaded applications effectively, let's build our Snake game. In its final form, the game would be like the one shown in the following screenshot (refer to the 7.03 game of snake.py
Python file available in the code bundle):
GUI
class.class GUI(Tk): def __init__(self, queue): Tk.__init__(self) self.queue = queue self.is_game_over = False self.canvas = Canvas(self, width=495, height=305, bg='#FF75A0') self.canvas.pack() self.snake = self.canvas.create_line((0, 0), (0,0), fill='#FFCC4C', width=10) self.food = self.canvas.create_rectangle(0, 0, 0, 0, fill='#FFCC4C', outline='#FFCC4C') self.points_earned = self.canvas.create_text(455, 15, fill='white', text='Score: 0') self.queue_handler()
The description of the code is as follows:
GUI
classes several times in the past.__init__
method, our GUI class now inherits from the Tk class. The line Tk.__init__(self)
ensures that the root window is available to all methods of this class. This way we can avoid writing root
attribute on every line by referencing self.root
simply as self
.queueHandler().
This yet to be defined method would be similar to main
method defined in the previous queue example. This would be the central method which will process all tasks in the queue. We will come back to define this method once we have added some tasks to the queue.Food
class, as shown in the following code snippet:class Food(): def __init__(self, queue): self.queue = queue self.generate_food() def generate_food(self): x = random.randrange(5, 480, 10) y = random.randrange(5, 295, 10) self.position = x, y self.exppos = x - 5, y - 5, x + 5, y + 5 self.queue.put({'food': self.exppos})
The description of the code is as follows:
__init__
method of the Food
class. We choose to run this from the main program thread to demonstrate how a code which is being executed in the main thread can communicate with attributes and methods from other threads.__init__
method calls another method called generate_food()
, which is responsible for generating the snake-food at random positions on the canvas.generate_food
method generates a random (x, y) position on the canvas. However, because the place where the coordinates coincide is just a small point on the canvas, it would be barely visible. We therefore generate an expanded coordinate (self.exppos
) ranging from five values less than the (x,y) coordinate up to five values higher than the same coordinate. Using this range, we can create a small rectangle on the canvas which would be easily visible and would represent our food.However, we do not create the rectangle here. Instead, we pass the coordinates for the food (rectangle) into our queue using queue.put
. Because this queue is to be made available to all our classes, we will have a centralized worker named queue_handler()
, which will process this queue to generate the rectangle from our GUI class later. This is the central idea behind a Queue implementation.
Snake
class. We have already passed a task to generate our food to the central queue. However, no thread was involved in the task. We could also generate our Snake class without using threads. However, because we are talking about ways to implement multithreaded applications, let's implement our Snake class to work from a separate thread (refer to 7.03 game of snake.py
):class Snake(threading.Thread): def __init__(self, gui, queue): threading.Thread.__init__(self) self.gui = gui self.queue = queue self.daemon = True self.points_earned = 0 self.snake_points = [(495, 55), (485, 55), (475, 55), (465, 55), (455, 55)] self.food = Food(queue) self.direction = 'Left' self.start() def run(self): while not self.gui.is_game_over: self.queue.put({'move':self.snake_points}) time.sleep(0.1) self.move()
The description of the code is as follows:
Snake
to run from a separate thread. This class takes the GUI and queue as its input arguments.self.snake_points
.move()
method at small intervals. During every run of the loop, the method populates the queue with a dictionary having the key as 'move'
and the value equal to the updated position of the snake through the self.snake_points
attribute.The thread initialized above calls the Snake
class move()
method to move the snake around on the canvas. However, before we can move the snake, we need to know the direction in which the snake should move. This obviously depends on the particular key pressed by the user (Left/Right/Top/Down key).
Accordingly, we need to bind these four events to the Canvas widget. We will do the actual binding later. However, we can now create a method named called key_pressed
, which takes the key_press
event itself as its argument and sets the direction value according to the key that is pressed.
def key_pressed(self, e): self.direction = e.keysym
Now that we have the directions, let's code the move
method:
def move(self): new_snake_point = self.calculate_new_coordinates() if self.food.position == new_snake_point: self.points_earned += 1 self.queue.put({'points_earned':self.points_earned }) self.food.generate_food() else: self.snake_points.pop(0) self.check_game_over(new_snake_point) self.snake_points.append(new_snake_point) def calculate_new_coordinates(self): last_x, last_y = self.snake_points[-1] if self.direction == 'Up': new_snake_point = last_x, (last_y - 10) elif self.direction == 'Down': new_snake_point = last_x, (last_y + 10) elif self.direction == 'Left': new_snake_point = (last_x - 10), last_y elif self.direction == 'Right': new_snake_point = (last_x + 10), last_y return new_snake_point def check_game_over(self, snake_point): x,y = snake_point[0], snake_point[1] if not -5 < x < 505 or not -5 < y < 315 or snake_point in self.snake_points: self.queue.put({'game_over':True})
The description for the code is as follows:
move
method obtains the latest coordinates for the snake depending on the keyboard event. It uses a separate method called calculate_new_coordinates
to get the latest coordinates.Food
class generate_food
method to generate a new food at a new location.self.snake_points.pop(0)
.check_game_over
to check if the snake collides against the wall or against itself. If the snake does collide, it appends a new dictionary item in the queue with the value 'game_over':True
.self.snake_points
. This is automatically added to the queue, because we have defined self.queue.put({'move':self.snake_points})
in the Snake
class's run()
method to update every 0.1 seconds as long as the game is not over.We now have a Food
class feeding the centralized queue from the main program thread. We also have the Snake
class adding data to the queue from one thread and a GUI
class running the queue_handler
method from another thread. So, the queue is the central point of interaction between these three threads.
Now, it is time to handle these data to update the content on the canvas. We accordingly define the queue_handler()
method in our GUI
class to work on items in the queue.
def queue_handler(self): try: while True: task = self.queue.get(block=False) if task.has_key('game_over'): self.game_over() elif task.has_key('move'): points = [x for point in task['move'] for x in point] self.canvas.coords(self.snake, *points) elif task.has_key('food'): self.canvas.coords(self.food, *task['food']) elif task.has_key('points_earned'): self.canvas.itemconfigure(self.points_earned, text='Score:{}'.format(task['points_earned'])) self.queue.task_done() except Queue.Empty: if not self.is_game_over: self.canvas.after(100, self.queue_handler)
The description for the code is as follows:
queue_handler
method gets into an infinite loop looking for tasks in the queue using task = self.queue.get(block=False)
. If the queue becomes empty, the loop is restarted using canvas.after
.'game_over'
, it calls another method named game_over()
that we defined next.'move'
, it uses canvas.coords
to move the line to its new position.'points_earned'
, it updates the score on the canvas.task_done()
method.
queue.get
can take both block=True
(default) and block=False
as its argument.
When the block is set to False
, it removes and returns an item from the queue, if available. If the queue is empty, it raises Queue.Empty
. When the block is set to True
, queue.get
fetches an item from the queue by suspending the calling thread, if required, until an item is available.
game_over
feature for the game.The queue_handler
method calls the game_over
method in case of a matching queue key:
def game_over(self):
self.is_game_over = True
self.canvas.create_text(200, 150, fill='white', text='Game Over')
quitbtn = Button(self, text='Quit', command =self.destroy)
self.canvas.create_window(200, 180, anchor='nw', window=quitbtn)
The description for the code is as follows:
game_over
attribute to True
. This helps us exit out of the infinite loop of queue_handler
. Then, we add a text on the canvas displaying the content Game Over.main()
:def main(): queue = Queue.Queue() gui = GUI(queue) snake = Snake(gui, queue) gui.bind('<Key-Left>', snake.key_pressed) gui.bind('<Key-Right>', snake.key_pressed) gui.bind('<Key-Up>', snake.key_pressed) gui.bind('<Key-Down>', snake.key_pressed) gui.mainloop() if __name__ == '__main__': main()
We create an empty queue, and pass it as an argument to all three of our classes so that they can feed tasks into the queue. We also bind the four directional keys to the key_pressed
method, which is defined earlier within our Snake
class.
Our game is now functional. Go try your hands at controlling the snake, while keeping its stomach filled.
To summarize, we created three classes such as Food
, Snake
, and GUI
. These three classes feed information about the task related to their class to a centralized queue which is passed as an argument to all the classes.
Then, we create a centralized method named queue_handler
, which handle tasks from the queue by polling tasks one at a time and completing it in a non-blocking manner.
The game could have been implemented without threads and queues, but it would have been slower, longer, and more complex. By using queues to manage data from multiple threads effectively, we have been able to contain the program to less than 150 lines of code.
Hopefully, you should now be able to implement queues for managing other programs that you design at your work.
18.216.77.153