Update 21 Aug 2020: Fixed the link to the source code.
The codes are available here.
I was kind of idle once again in the office. For some reason we could not smoothly work in our client’s office yet and proceed with the system integration testing of our work. I had a lot of time to kill and I figured I could use it to continue my study in python programming. Yes I know most CS students start with python, but for us it was C so I’m still learning the language now. I’ve read through the manual for the basic stuff months ago but I never really worked on any exercise. And just last Tuesday (December 20) I thought, how else would you want to practice python programming than by creating a sample of the snake game? Yes I found it funny. I’m pretty shallow that way. But thinking it over, game programming is enjoyable and the snake game is pretty simple. It would be worth a shot. So here’s my work.
Note: If you want to try this on your own, please make sure that you have Python2.7 installed and the corresponding version of PyGame.
THE SPRITES
I put all the sprites in one file named sprites.py. The class names are pretty ridiculous for I was sort of doing things on the fly and I am sorry for that. I even used the name Apple instead of Food so for this game you are going to have an apple-eating snake.
To begin, we need to put the following line to import the pygame module:
import pygame
The Apple Class
class Apple(pygame.sprite.Sprite): #private constants _DEFAULT_COLOR = [255, 0, 0] #red _DEFAULT_SIZE = [10, 10] def __init__(self, color, size, position): #parameter validation if color == None: color = Apple._DEFAULT_COLOR if size == None: size = Apple._DEFAULT_SIZE if position == None: raise Exception('Invalid position.') #initialization self.color = color self.size = size self.image = pygame.Surface(size) self.image.fill(color) self.rect = self.image.get_rect() self.rect.topleft = position
This one’s pretty straight-forward. We have here an apple class accepting parameters for the color, size, and position upon creation of an instance. I provided default values for the color and size and a little parameter validation since it’s going to be a part of a class library.
Note: All sprites are just composed of square areas in the screen of about 10px X 10px filled by a certain color.
The Callable Class
class Callable: def __init__(self, anycallable): self.__call__ = anycallable
This class is only used to implement static functions in the sprite classes.
The Snake Class
The Snake class has with it three internal classes namely _SnakeTail, _SnakeHead, and SnakeMove with the first two as private. I figured, If I would like to use images rather than solid colors in the future, I would most certainly dedicate a different image to the head compared to the body. Hence, I decided to separate the snake’s head from its tail.
Like what I did in the Apple class, I also provided default values for the attributes.
#private constants _DEFAULT_COLOR = [0, 255, 0] #green _DEFAULT_SIZE = [10, 10] #10px X 10px _DEFAULT_POSITION = [30, 30] #space given to tail of length 2
The _SnakeTail Class
class _SnakeTail(pygame.sprite.Sprite): def __init__(self): #initialization self.tiles = [] def add_tile(self, color, size, position): #creates a new tile tile = pygame.Surface(size) tile.fill(color) rect = tile.get_rect() rect.topleft = position self.tiles.append({'image':tile, 'rect':rect})
The snake’s tail is composed of an array of color-filled square areas that I call tiles for lack of a better term. It also has an add_tile function that will make the tail grow longer. The add_tile function creates another instance of a tile of a given color and size and adds it to the tail’s array of tiles. Once the tail is re-rendered on screen, it should appear longer.
The _SnakeHead Class
class _SnakeHead(pygame.sprite.Sprite): def __init__(self, color, size, position): #initialization self.image = pygame.Surface(size) self.image.fill(color) self.rect = self.image.get_rect() self.rect.topleft = position
The _SnakeHead class is very similar to the Apple class.
The SnakeMove Class
class SnakeMove(): UP = '1Y' DOWN = '-1Y' RIGHT = '1X' LEFT = '-1X' #checks a direction's validity def SnakeMove_is_member(direction): if direction == Snake.SnakeMove.UP: return True elif direction == Snake.SnakeMove.DOWN: return True elif direction == Snake.SnakeMove.RIGHT: return True elif direction == Snake.SnakeMove.LEFT: return True return False #makes the function 'is_member' a static function SnakeMove_is_member = Callable(SnakeMove_is_member)
The creation of this class would have been avoided if I settled on accepting just some set of values for directions in making the snake move but it was against to what I’m used to. Since I wanted sprites.py to function like a standard class library I wanted it to implement a uniform way of using the classes and their functions and sort of inform developers about it. The SnakeMove class functions like an enum class in C#, this way developers won’t have to guess valid direction values once they deal with making the snake move. All they have to do is pass a constant from this class as a parameter to the Snake class’ move function.
The class has a SnakeMove_is_member static function which is used for validating the direction parameter in the Snake class’ move function. The static class was created with the help of the previously shown Callable class.
The Snake Class Functions
Initialization
def __init__(self, color, size, position): #parameter validation if color == None: color = Snake._DEFAULT_COLOR if size == None: size = Snake._DEFAULT_SIZE if size[0] != size[1]: raise Exception('Invalid tile size. Width and height must be equal.') if position == None: position = Snake._DEFAULT_POSITION self.color = color self.size = size self.head = Snake._SnakeHead(color, size, position) self.tail = Snake._SnakeTail() tailposition = [(position[0] - size[0]), position[1]] self.tail.add_tile(color, size, tailposition) tailposition = [(position[0] - 2*size[0]), position[1]] self.tail.add_tile(color, size, tailposition)
The initialization of the snake class simple involves validating the parameters and assigning default values for missing paramaters and creating the instance for the snake’s head and tail. As shown in the code, the initial length of the tail is 2 tiles. All in all, the Snake sprite has a color, size, head, and tail attributes.
Movement
def move(self, direction, frame_width, frame_height): #parameter validation if Snake.SnakeMove.SnakeMove_is_member(direction) != True: raise Exception('Invalid movement direction.') #initializes new position stepsize = self.head.image.get_rect()[2] #gets the size of the head tile newheadposition = [self.head.rect.topleft[0], self.head.rect.topleft[1]] if direction == Snake.SnakeMove.UP: newheadposition[1] = (newheadposition[1]-stepsize)%frame_height if direction == Snake.SnakeMove.DOWN: newheadposition[1] = (newheadposition[1]+stepsize)%frame_height if direction == Snake.SnakeMove.RIGHT: newheadposition[0] = (newheadposition[0]+stepsize)%frame_width if direction == Snake.SnakeMove.LEFT: newheadposition[0] = (newheadposition[0]-stepsize)%frame_width if self.occupies_position(newheadposition): return False #moves the head to its new position newtileposition = self.head.rect.topleft self.head.rect.topleft = newheadposition #moves the tail tiles to its respective new positions for count in range(len(self.tail.tiles)): prevtileposition = self.tail.tiles[count]['rect'].topleft self.tail.tiles[count]['rect'].topleft = newtileposition newtileposition = prevtileposition return True
As mentioned earlier, the move function first checks if the supplied direction is valid. If not, it will raise an exception. If it is, it will proceed to determing the next position of each tile starting with the head.
stepsize = self.head.image.get_rect()[2] #gets the size of the head tile
In this line, we are assuming that the size of the head is equal with the all the other tiles in the snake’s body.
if self.occupies_position(newheadposition): return False
Here we are making sure that the snake is not trying to move to a space that its body already occupies. If it is, then the snake won’t be able to move and the game will be over.
The remaining lines deal with moving each tile to the position of the one before it. If all goes well then the function will return True, signifying that the movement was successful.
Collision Detection
#checks if this snake's body occupies a given position def occupies_position(self, position): #parameter validation if position[0] == None or position[1] == None: return True if self.head.rect.topleft[0] == position[0] \ and self.head.rect.topleft[1] == position[1]: return True for count in range(len(self.tail.tiles)): if self.tail.tiles[count]['rect'].topleft[0] == position[0] \ and self.tail.tiles[count]['rect'].topleft[1] == position[1]: return True return False
PyGame already has a built-in function for collision detection involving objects with rect attribute but I found it quite late. I did this one and it was pretty good enough for me. As the function name implies, this function checks whether the snake is occupying a given position.
Lengthening the Tail
def lengthen_tail(self, number, current_direction): #parameter validation if number is None: number = 1 if Snake.SnakeMove.SnakeMove_is_member(current_direction) != True: raise Exception('Invalid movement direction.') size = self.size[0] color = self.color for count in range(number): lastindex = len(self.tail.tiles) - 1 X = self.tail.tiles[lastindex]['rect'].topleft[0] Y = self.tail.tiles[lastindex]['rect'].topleft[1] #determines position of new tile if current_direction == Snake.SnakeMove.UP: Y = Y - size + (count*size) elif current_direction == Snake.SnakeMove.DOWN: Y = Y + size + (count*size) elif current_direction == Snake.SnakeMove.RIGHT: X = X - size + (count*size) elif current_direction == Snake.SnakeMove.LEFT: X = X + size + (count*size) self.tail.add_tile(color, self.size, [X, Y])
Since the _SnakeTail class is supposed to be private, I provided this function on the Snake class. This should be the one that developers using my sprites library use when making the Snake grow longer. I included an number parameter just in case someone wants to make the snake grow longer by more than one tile. Determining the position for the new tile involves checking the snake’s current direction to ensure that addition of tiles will be done on the right end and following the right direction of movement.
THE GAME
The main file for this snake game is game.py. It handles the display and game flow.
Initialization
import pygame import pygame._view from pygame import * from sprites import Snake from sprites import Apple import random
The lines above shows the modules imported by the game file. Notice that only the Apple and Snake classes were imported from sprites.py.
pygame.init()
Initialize the game.
DEFAULT_SCREEN_SIZE = [640, 480] INITIAL_DIRECTION = Snake.SnakeMove.RIGHT DEFAULT_UPDATE_SPEED = 100 updatetime = pygame.time.get_ticks() + DEFAULT_UPDATE_SPEED
Some default values and update time initialization.
#screen initialization screen = pygame.display.set_mode(DEFAULT_SCREEN_SIZE) display.set_caption('Snake') #sprite initialization snake = Snake(None, None, None) apple = None
Screen and sprites initialization.
is_done = False #signifies escape from game is_over = False #signifies end of game by game rules direction = None score = 0 create_apple()
Initialization of variables that will be used in the game flow. Notice that we have is_done and is_over. create_apple was also called to create first instance of the apple.
Rendering
def render_snake(): screen.blit(snake.head.image, snake.head.rect) for count in range(len(snake.tail.tiles)): screen.blit(snake.tail.tiles[count]['image'] , snake.tail.tiles[count]['rect'])
This method renders the snake on screen. This simply displays all the tiles forming the image of the snake.
#renders the apple on screen def render_apple(): global apple screen.blit(apple.image, apple.rect)
Like the render_snake method, this method displays the apple on screen.
#creates a new apple def create_apple(): global apple global snake hlimit = (DEFAULT_SCREEN_SIZE[0]/Apple._DEFAULT_SIZE[0])-1 vlimit = (DEFAULT_SCREEN_SIZE[1]/Apple._DEFAULT_SIZE[1])-1 X, Y = None, None while snake.occupies_position([X, Y]) == True: X = random.randint(0, hlimit)*Apple._DEFAULT_SIZE[0] Y = random.randint(0, vlimit)*Apple._DEFAULT_SIZE[1] apple = Apple(None, None, [X, Y])
Notice that horiontal and vertical limits were computed first before generating a random position. This ensures that the position generated will be inside the area of the screen. Default values are used here ‘though, maybe we can make it configurable on a better version. We also keep on generating random positions if the current position is occupied by the snake.
The Game Flow
while is_done == False: screen.fill(0) #color screen black global direction if direction == None: direction = INITIAL_DIRECTION #checks for changes in direction and validates it for e in event.get(): if e.type == KEYUP: if e.key == K_ESCAPE: is_done = True elif e.key == K_UP: if direction != Snake.SnakeMove.DOWN: direction = Snake.SnakeMove.UP elif e.key == K_DOWN: if direction != Snake.SnakeMove.UP: direction = Snake.SnakeMove.DOWN elif e.key == K_RIGHT: if direction != Snake.SnakeMove.LEFT: direction = Snake.SnakeMove.RIGHT elif e.key == K_LEFT: if direction != Snake.SnakeMove.RIGHT: direction = Snake.SnakeMove.LEFT #updates the display currenttime = pygame.time.get_ticks() global updatetime global is_over if is_over == False: if currenttime >= updatetime: moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1]) if moved == False: is_over = True if snake.occupies_position(apple.rect.topleft) == True: create_apple() snake.lengthen_tail(1, direction) global score score += 1 display.set_caption('Snake: ' + str(score)) render_apple() render_snake() pygame.display.update() updatetime += DEFAULT_UPDATE_SPEED else: display.set_caption('Snake: ' + str(score) + ' GAME OVER')
The flow as expected is implemented using a loop.
screen.fill(0) #color screen black
First we fill the screen with black.
global direction if direction == None: direction = INITIAL_DIRECTION #checks for changes in direction and validates it for e in event.get(): if e.type == KEYUP: if e.key == K_ESCAPE: is_done = True elif e.key == K_UP: if direction != Snake.SnakeMove.DOWN: direction = Snake.SnakeMove.UP elif e.key == K_DOWN: if direction != Snake.SnakeMove.UP: direction = Snake.SnakeMove.DOWN elif e.key == K_RIGHT: if direction != Snake.SnakeMove.LEFT: direction = Snake.SnakeMove.RIGHT elif e.key == K_LEFT: if direction != Snake.SnakeMove.RIGHT: direction = Snake.SnakeMove.LEFT
Then we check for changes in direction based on keyboard input. Notice that global direction was used to make things less confusing. After all only one direction is needed. Additional check was also placed to deem direction opposite the current one as invalid.
#updates the display currenttime = pygame.time.get_ticks() global updatetime global is_over if is_over == False: if currenttime >= updatetime: moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1]) if moved == False: is_over = True if snake.occupies_position(apple.rect.topleft) == True: create_apple() snake.lengthen_tail(1, direction) global score score += 1 display.set_caption('Snake: ' + str(score)) render_apple() render_snake() pygame.display.update() updatetime += DEFAULT_UPDATE_SPEED else: display.set_caption('Snake: ' + str(score) + ' GAME OVER')
Updating the display depends on two things, one is if the game is not over yet and the other is if the update time was already reached.
moved = snake.move(direction, DEFAULT_SCREEN_SIZE[0], DEFAULT_SCREEN_SIZE[1]) if moved == False: is_over = True
If the game is not yet over and the update time was already reached, an attempt to move the snake is done. In case movement failed which is probably becaused the snake hit itself, the game will be over.
if snake.occupies_position(apple.rect.topleft) == True: create_apple() snake.lengthen_tail(1, direction) global score score += 1 display.set_caption('Snake: ' + str(score))
If movement was successful, it is determined if the snake passes through the position of the apple. If it does, then a new apple is created, the snake’s tail will be lengthened, the score will be incremented, and the score display will be updated.
render_apple() render_snake() pygame.display.update() updatetime += DEFAULT_UPDATE_SPEED
To cap off the updates, all the elements will be re-rendered and the update time will be set to a future time.
On the other hand, if the game is already over, the updates will simply stop and the display will flash GAME OVER together with the score.
Wow this post is pretty lengthy but I hope someone might find it useful at least for comparisons haha. Feel free to comment ‘though I’d be moderating them. Have a nice day everyone!
Superb 🙂 m a fan!
Oh thanks! I’ve sort of stopped developing games in Python though.
thank you! how can i change the color of the apples and the snake? i want a snake htat changes to the color of the apple it just ate.
it has been a very long time; i cannot perfectly recall everything. but you can do color randomization for the apple then fill the snake head and snake tail’s image with that color once the snake gets to eat the apple. the rendering function should render the snake with the new color.
thanks for asking. it’s nice to get a message from readers once in a while.
Thank you so much! I Will try that when I get to know a little bit more about programming.I am starting a python game programming course in coursera.org that you may want to reccomend to your readers/followers,
because (as your site), its fun and free! 😀
SIR, Can you email me on tanaynagarsheth.tn@gmail.com because this one has many and shows many errors
Hi, I’m not entirely sure of what you mean, but maybe it’s okay to post your questions here. That way even the other visitors can leave you a reply.