HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMajor

Snake game using PyGame

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
pygamegameusingsnake

Problem

I wrote a simple Python snake game which is about 250 lines of code. Can someone give me some advice on how I can refactor/make it better?

game.py

# game.py - 3/22/2013

import pygame, sys, os
from pygame.locals import *
from classes import *

def main():
    pygame.init()
    pygame.display.set_caption('PyGame Snake')

    window = pygame.display.set_mode((480, 480))
    screen = pygame.display.get_surface()
    clock = pygame.time.Clock()
    font = pygame.font.Font('freesansbold.ttf', 20)

    game = SnakeGame(window, screen, clock, font)

    while game.run(pygame.event.get()):
        pass

    pygame.quit()
    sys.exit()

if __name__ == '__main__':
    main()


classes.py

```
#classes.py - 3/22/2013

import pygame, random
from pygame.locals import *

# Gams speed
STARTING_FPS = 4
FPS_INCREMENT_FREQUENCY = 80

# Direction constants
DIRECTION_UP = 1
DIRECTON_DOWN = 2
DIRECTION_LEFT = 3
DIRECTION_RIGHT = 4

# World size
WORLD_SIZE_X = 20
WORLD_SIZE_Y = 20

# Snake and food attributes
SNAKE_START_LENGTH = 4
SNAKE_COLOR = (0, 255, 0)
FOOD_COLOR = (255, 0, 0)

# Snake class
class Snake:

# Initializes a Snake object
def __init__(self, x, y, startLength):
self.startLength = startLength
self.startX = x
self.startY = y
self.reset()

# Resets snake back to its original state
def reset(self):
self.pieces = []
self.direction = 1

for n in range(0, self.startLength):
self.pieces.append((self.startX, self.startY + n))

# Changes the direction of the snake
def changeDirection(self, direction):
# Moving in the opposite direction of current movement is not allowed
if self.direction == 1 and direction == 2: return
if self.direction == 2 and direction == 1: return
if self.direction == 3 and direction == 4: return
if self.direction == 4 and direction == 3: return

self.direction = direction

# Returns the head piece of th

Solution


  1. Introduction



This is not bad overall, considering that this is your first program written with PyGame. I've made many comments below, but don't take the length of this answer to heart: there are always many things to say about a piece of code of this length.

  1. Game design issues



-
The game could do with some instructions. I had to look at the source code to see that I need to use WASD for movement. Alternatively, you might allow the player to use the arrow keys too (these are natural keys the player might try).

-
You use FPS (the number of frames per second) to control the speed of the snake. This design decision commits you to processing everything in the game at the same frequency as the snake moves. The concepts frames per second and speed of the snake in moves per second are distinct, so it's good practice to separate them.

At the moment there's nothing in the game other than the snake, so you get away with this. But as soon as you add other game elements that need to animate at different speeds, you'll run up against this difficulty. Better to get this right while things are still simple.

See section 5 for one way to solve this problem.

-
New pieces of food can be created in positions occupied by the snake!

-
The food position is not reset when a new game starts. (This can cause the snake to be overlap the food at the start of the game.)

-
The score doesn't get drawn during the game.

  1. Major comments



-
The docstring for collidesWithSelf reads like this:

"""
# Because of the way new pieces are added when the snake grows, eating a
# new food block could cause the snake to die if it's in a certain position. 
# So instead of checking if any of the spots have two pieces at once, the new
# algorithm only checks if the position of the head piece contains more than one block.

for p in self.pieces:
    if len(self.pieces) - len([c for c in self.pieces if c != p]) > 1: return True
return False
"""


This is not appropriate content for a docstring. The purpose of a docstring is to explain the interface of a method to a programmer who is trying to use it. But here you have some notes to yourself about the history of this function and why it is implemented like it is. These notes properly belong in a comment.

The reason you have been having problems in this function is that the growth of the snake is not right. In the grow() method you grow a new tail segment in the opposite direction to the snake's current movement. But this can cause the snake to self-intersect.

The usual way that "snake" games work is that when the snake eats some food, it does not grow a new tail segment immediately. Instead, it waits until the next time it moves and grows a new tail segment in the position where its old tail used to be. This is easily implemented by incrementing a counter each time the snake eats food:

def grow(self):
    self.growth_pending += 1


and then decrementing the counter instead of deleting the tail segment:

if self.growth_pending > 0:
    self.growth_pending -= 1
else:
    # Remove tail
    self.pieces.pop()


This avoids self-intersection, and so this would allow you to implement the collision operation using your original approach. But you might consider this simpler approach:

it = iter(self.pieces)
head = next(it)
return head in it


-
You represent directions by numbers between 1 and 4. It is hard to remember which direction is which, so it would be easy to make a mistake and treat 1 as "up" in one part of the code but "down" in another. You'd be much less likely to make this mistake if you used the names DIRECTION_UP and so on. You went to all the trouble to create these names: why not use them?

(But see 3.4 below for a better suggestion.)

-
The code below looks dodgy because there is no else: on the end of the series of tests.

head = ()
if self.direction == 1: head = (headX, headY - 1)
elif self.direction == 2: head = (headX, headY + 1)
elif self.direction == 3: head = (headX - 1, headY)
elif self.direction == 4: head = (headX + 1, headY)


A programmer reading this would want to know what happens if self.direction is not in the range 1 to 4. Of course, you hope that you have designed the program so that this can't happen. So you might make this crystal clear by rewriting this code like this:

if   self.direction == DIRECTION_UP:    head = (headX, headY - 1)
elif self.direction == DIRECTION_DOWN:  head = (headX, headY + 1)
elif self.direction == DIRECTION_LEFT:  head = (headX - 1, headY)
elif self.direction == DIRECTION_RIGHT: head = (headX + 1, headY)
else: raise RuntimeError("Bad direction: {}".format(self.direction))


(But see 3.4 below for a better suggestion.)

-
Instead of representing a direction with a number from 1 to 4 (which it's hard to remember which is which), why not use a pair (δx, δy)? For example, you could write:

```
DIRECTION_UP = 0, -1
DIRECTION_DOWN = 0, 1
DIRECTION_LEFT = -1, 0
DIRECTION_RI

Code Snippets

"""
# Because of the way new pieces are added when the snake grows, eating a
# new food block could cause the snake to die if it's in a certain position. 
# So instead of checking if any of the spots have two pieces at once, the new
# algorithm only checks if the position of the head piece contains more than one block.

for p in self.pieces:
    if len(self.pieces) - len([c for c in self.pieces if c != p]) > 1: return True
return False
"""
def grow(self):
    self.growth_pending += 1
if self.growth_pending > 0:
    self.growth_pending -= 1
else:
    # Remove tail
    self.pieces.pop()
it = iter(self.pieces)
head = next(it)
return head in it
head = ()
if self.direction == 1: head = (headX, headY - 1)
elif self.direction == 2: head = (headX, headY + 1)
elif self.direction == 3: head = (headX - 1, headY)
elif self.direction == 4: head = (headX + 1, headY)

Context

StackExchange Code Review Q#24267, answer score: 28

Revisions (0)

No revisions yet.