Page 1 of 1

Snake Game - A Pygame Tutorial

#1 DK3250  Icon User is offline

  • Pythonian
  • member icon

Reputation: 292
  • View blog
  • Posts: 922
  • Joined: 27-December 13

Posted 23 March 2017 - 12:17 PM

A Snake Game

Screendump:

Posted Image

This tutorial is walk-through of the code for a classic Snake Game.
I post this in response to a few request for game tutorias in the Python “Tutorial Request” section.

The code is made with Python 3.4 and Pygame 1.9

Game objective

You maneuver a snake around on the screen using the arrow keys. Whenever you hit a target (here: an apple) the snake grows by one unit and the apple moves to a new (random) position. The game target is to get the longest possible snake.
If you hit the walls of the screen, the snake dies.
If you hit yourself (head hits tail), the snake dies. Take care not to hit yourself during U-turns.

In the two-player version, two snakes move simultaneously and compete to get to the apple. If you hit the other snake (your head hits other snakes tail), your snake dies.
In the two-player game, the winner is the one not dying first.

The snake(s) move freely around on the screen, not limited by a grid.

Game Analysis

The snake consists of a Head and a Tail.
In my game, each body-element is a circle, the circles are spaced such that the perimeter of two adjacent circles just touches.

Movement of the Head is simple, directed by the users press of the Arrows: Up, Down, Left and Right. Once a movement is started, it will continue until another Arrow-key is pressed, or until the snake hits a wall and dies.

Tail movements are more complicated. Say you have N tail elements, then the element number N move to the position of number N-1; N-1 moves to N-2; and so on until element number 1 moves to the head position, and then finally the head moves as described above.

Now, such motion requires all element to move a full diameter at each step which will look very clunky/abrupt – not what we want.
We want the movement to be only two pixels at a time (the value is a parameter in the code).
To make smooth tail movement in step length of 2 pixels, we insert a number of 'invisible' tail elements – just enough extra elements to keep the visible parts in the correct position.
Then, during Snake movement, we move all tail elements, both visible and invisible, according to the general 'tail-rule'.

The picture below show schematically the Head (red) and two visible tail elements (black) as well as 2 x 7 invisible tail elements. Try to imagine the movement of the 16 tail elements (last one [right] first) and finally a user directed movement of the Head.

Posted Image

With this analysis, we are ready for the coding.

Surface, Rect and collisions


For an introduction to the all-important graphical elements in Pygame, look-up the first part of the Platform Game Tutorial: http://www.dreaminco...e-part-%231/#1/

The Code

Before we dive into the code, let’s see the full code. Try running it and get a feeling for the game.
Feel free to change the value of SIZE and STEP (in first section of the code), but for best result, STEP must be a divisor of SIZE.

"""
A classic Single Player Snake Game
By DK3250, March 2017

This game is made as a tutorial for
DreamInCode's Python Forum

It demonstrates a vararity of Pygame functions
as well as basic Python programming.

The comments are few as the accompanoying text
aer meant to explain the code.

The code is made with Python 3.4 and Pygame 1.9
"""
import pygame, sys, random
pygame.init()

X = 400
Y = 300

BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
BLUE = (0, 0, 255)
TRANSPARENT = (1, 2, 3)
bg = GREEN

SIZE = 16
STEP = 2
fps = 100//STEP
n_invisible = SIZE//STEP-1
n_noCollision = SIZE//STEP * 3

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Snake Game Presentation by DK3250")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Verdana", 18)


class Snake():
    """ The basic snake object
    Snake is build of snakeElement's - they are separate objects.

    The methods in Snake are:
    * __init__()
    * reset()
    * grow()
    * move()
    * move_tail()
    * move_head()
    * checkForCollisions()
    * draw()

    See the tutorial text for explanation.
    """
    def __init__(self, x, y, color, name):
        self.startx = x
        self.starty = y
        self.color = color
        self.name = name

    def reset(self):
        """ Each snake is build of several 'snakeElement's
    here the list is reset to only one element """
        self.snakeList = [SnakeElement(self.startx, self.starty, self.color)]
        self.length = 0
        self.dir = [False] * 4  # self.dir corresponds to down, up, left, right

    def grow(self):
        """ One Growth Cycle consist of a number of invisible
    elements, and one visible element (BLACK) """
        self.length += 1
        for _ in range(n_invisible):
            self.snakeList.append(SnakeElement(self.snakeList[-1].rect.centerx,
                                  self.snakeList[-1].rect.centery, TRANSPARENT, show=False))
        self.snakeList.append(SnakeElement(self.snakeList[-1].rect.centerx,
                              self.snakeList[-1].rect.centery, BLACK))

    def move(self):
        self.move_tail()
        self.move_head()

    def move_tail(self):        
        " tail moves one step forward "
        for i in range(len(self.snakeList)-1, 0, -1):
            self.snakeList[i].rect.center = self.snakeList[i-1].rect.center

    def move_head(self):
        " Head moves according to user input (self.dir) "
        head = self.snakeList[0]
        down, up, left, right = self.dir
        if down:
            head.rect.centery += STEP
        elif up:
            head.rect.centery -= STEP
        elif left:
            head.rect.centerx -= STEP
        elif right:
            head.rect.centerx += STEP
        
    def checkForCollisions(self):
        head = self.snakeList[0]
        ## --- check for collision with window --- ##
        if (head.rect.top < 0 or
            head.rect.bottom > Y or
            head.rect.left < 0 or
            head.rect.right > X):
            game.state = game.end
            return

        ## --- check for collision with self --- ##
        if (len(self.snakeList) > n_noCollision and
            head.rect.collidelist(self.snakeList[n_noCollision:]) > -1):
            game.state = game.end
            return

        ## --- check for hit with apple --- ##
        if apple.rect.colliderect(head):
            apple.reset()
            self.grow()

    def draw(self):
        for s in self.snakeList:
            s.draw()
        txt = font.render("%s: %d" %(self.name, self.length), 1, WHITE)
        screen.blit(txt, (10, 10))


class SnakeElement():
    """
    Creation of an anti-aliased snake body-element.
    In essence a circle with a color gradient and anti-aliased to the background.
    
    Both the gardient function (see below) and the anti-aliasing is explained here:
    http://www.dreamincode.net/forums/topic/388729-anti-aliasing/
    """

    def __init__(self, x, y, COLOR, rad=SIZE//2, light_pos=(0.35, -0.35), show=True):
        x0 = rad + 1
        y0 = rad + 1

        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(TRANSPARENT)
        self.surf.set_colorkey(TRANSPARENT)
        self.rect = self.surf.get_rect()
        self.rect.centerx = x
        self.rect.centery = y
        self.show = show

        if not show:
            return

        self.perimeter = []  # list of pixels that needs anti-aliasing

        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d >= rad+1:
                    continue
                else:
                    color = COLOR
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad
                    color = gradient(d2, color)

                    if d > rad:  # prepare for anti-aliasing, get the perimeter pixels
                        alfa = d-rad
                        color_rim = [c * (1-alfa) for c in color]
                        self.perimeter.append(((i, j), color_rim, alfa))
                        color = TRANSPARENT  # colorkey at rim

                    self.surf.set_at((i, j), color)

    def draw(self):
        """
    First, the self.surf (picture without perimeter) is blitted to screen.
    Secondly, the perimeter is updated, anti-aliasing to the background.
    """
        if self.show is False:
            return
        
        screen.blit(self.surf, self.rect)

        # anti-aliasing
        for p in self.perimeter:
            x, y = p[0]
            x += self.rect.x
            y += self.rect.y
            if x < 0 or x >= X or y < 0 or y >= Y:
                continue
            color = p[1]
            alfa = p[2]
            bg = screen.get_at((x, y))
            color_aa = [rim + back * alfa for rim, back in zip(color, bg)]
            screen.set_at((x, y), color_aa)


class Apple():
    def __init__(self):
        self.surf = pygame.surface.Surface((SIZE, SIZE))
        self.surf.fill(bg)
        self.surf.blit(picApple, [0, 0])
        self.rect = self.surf.get_rect()

    def draw(self):
        screen.blit(self.surf, self.rect)

    def reset(self):
        self.rect.centerx = random.randint(SIZE//2, X-SIZE//2)
        self.rect.centery = random.randint(30, Y-SIZE//2)
        while self.rect.collidelist(snake.snakeList) > -1:
            self.rect.centerx = random.randint(SIZE//2, X-SIZE//2)
            self.rect.centery = random.randint(30, Y-SIZE//2)


class GameState():
    def __init__(self):
        self.state = self.gameLoop

    def gameLoop(self):
        """ The basic game loop handling user input during game """
        clock.tick(fps)  # game speed
        screen.fill(bg)

        self.eventLoop()
        snake.move()
        snake.checkForCollisions()

        apple.draw()
        snake.draw()
        pygame.display.flip()

    def end(self):
        """ This method is called when a game ends i.e. one snake dies """
        againTxt()
        self.state = self.playAgain

    def stop(self):
        """ This method completely stops the game """
        pygame.quit()
        sys.exit()

    def playAgain(self):
        """ Loop to handle keyboard input for 'State = Play Again'
    defined in the method 'End' """
        pygame.time.wait(100)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.state = self.stop
                return
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_n:
                    self.state = self.stop
                    return
                elif event.key == pygame.K_y:
                    snake.reset()
                    apple.reset()
                    self.state = self.gameLoop
                    return

    def eventLoop(self):
        """ Event Loop, - handles user input """
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.state = self.stop
                return

            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_DOWN and not snake.dir[1]:
                    snake.dir = [True, False, False, False]
                elif event.key == pygame.K_UP and not snake.dir[0]:
                    snake.dir = [False, True, False, False]
                elif event.key == pygame.K_LEFT and not snake.dir[3]:
                    snake.dir = [False, False, True, False]
                elif event.key == pygame.K_RIGHT and not snake.dir[2]:
                    snake.dir = [False, False, False, True]                


def gradient(d2, color):
    """
    A general color gradient function.
    With a color input and distance input
    a gradient color is calculated.
    """
    r, g, b = color
    return [max(255-int(d2*(255-r)//150), 0),
            max(255-int(d2*(255-g)//150), 0),
            max(255-int(d2*(255-b )//150), 0)]


def againTxt():
    """ Text at game end (snake dead) """
    txt = font.render("Snake Dies", 1, RED)
    rct = txt.get_rect(center=[X//2, Y//2])
    screen.blit(txt, rct)

    txt = font.render("Play again (y/n)?", 1, WHITE)
    rct = txt.get_rect(center=[X//2, Y//2+30])
    screen.blit(txt, rct)

    pygame.display.flip()


picApple = pygame.image.load("picture.png").convert()
picApple = pygame.transform.scale(picApple, (SIZE, SIZE))
picApple.set_colorkey(WHITE)

snake = Snake(X//2, Y//3, YELLOW, 'Yellow Player ')
apple = Apple()
game = GameState()

snake.reset()
apple.reset()

while True:
    game.state()



The code uses 4 class types: Snake, SnakeElement, Apple and Game.
I'll start explaining the code for those four classes, and end by explaining the small main code.

class Snake
The Snake class consists of a number of methods.

def __init():

In def __init__() the start position is set. The color is the color of the head. The name is primarily a preparation for a later two-player version.

def reset():

In the reset() method the first SnakeElement (the head) is defined and placed in a list, 'snakeList'.
The (tail-)length is zero and all movement is stopped – the self.dir list has 4 Boolean elements indicating movement in direction down, up, left or right; they are here all set to 'False'.

def grow():
In the grow() method, a number (defined by ‘n_invisible’) of invisible body-elements are added to the snakeList and finally a single visible (BLACK) body-element is added. All the new elements are located at the position of the last existing element – during the next few movement steps the new tail element will gradually become visible.
The number of invisible element depends of SIZE and STEP, see below under main code.

def move():
In move() we just call the move_tail() and the move_head() methods.

def move_tail():

In a for-loop starting at the end of the tail, the tail-elements are moved to the position of the preceding element.

def move_head():
The line ”head = self.snakeList[0]” reduce subsequent typing and increases the readability.
The self.dir is expanded to the Boolean variables 'down', 'up', 'left', and 'right'; one of those will be True, the others will be False.
Finally, the head position is moved according to the self.dir direction.

def checkForCollision():
This method handles three different collision situations.

First check is for collision with screen border, this should not require further explanation.

Second check is for collision with the tail. As the circular body-elements are placed in a square surface, it is inevitable that surface elements overlap when the head turns. Thus, we need to disregard collisions between the head and the first few body-elements.
Only if the number of body-element exceed a threshold do we check for collision and only for elements with higher number than this threshold. The threshold is defined in main (the ‘n_noCollision’ parameter); I have chosen a 'free zone' of three body-elements (two tail elements).

The third check is for collision with 'apple' upon which the apple.reset() and self.grow() is invoked.

def draw():
We loop through the snakeList and call the draw() function of the each body-element, both visible and invisible.
A small text is rendered and blitted to the screen.


class SnakeElement
The SnakeElement class only have two methods, a large __init__() method and a smaller draw() method.

Basically, the SnakeElement defines and draws one of two types of graphical element, a visible body-element or an invisible one.
In the call to SnakeElement, the parameter 'show' can be set to True (= visible) or False (= invisible).
The invisible elements is only constituted of basic Surface and Rect specifications.

The visible parts are more complicated, but fully described in this tutorial: http://www.dreaminco...-anti-aliasing/ (third code block)

In the draw() method, we first test
if show is False:
    return

Omitting any further handling of invisible elements.
The visible part is handled as described in the tutorial mentioned a few lines above.

class Apple()
This class has simple __init__() and draw() methods; they do not require further explanation.

The reset() method is used both upon first initialization and after a hit with Snake; it defines a random position and makes sure that this position do not collide with the existing snake – otherwise a new position is found.

class GameState()
For a in-depth explanation of the GameSate class, I will again refer to an existing tutorial: http://www.dreaminco...and-game-loops/

The GameState is constituted of a number of small methods. I think only the 'eventLoop()' method needs explanation.

def eventLoop():
The eventLoop() handles user input from keyboard.

As the Snake cannot turn 180 degree (it would instantly collide with itself), all directional input is checked for the opposite direction being active; only if this is not the case, the new direction is implemented in the snake.dir list.

The general functions
Two small general function are used: gradient() and againTxt().
The gradient() method is explained in the antialiasing tutorial mentioned above.
The againTxt() place a simple text on the screen if the snake dies.

The main code

I have placed all constants and basic Pygame declarations in the top of the code. Hopefully most of this is known stuff.
The values of 'n_invisible' and 'n_noCollision' are calculated using the two simple formulas.

In the bottom of the code I first import and transform the apple image (file attached).

Then the three class instances are declared: snake, apple and game.

snake.reset() and apple.reset() initializes the positions of snake and apple.

Finally, the simple main game-loop explained in the game-loop tutorial.

Comments
I find that this game code is quite straightforward. The 'big thing' here is not really in the code but rather in the game analysis. When I first constructed this game, it took quite a while to analyze and figure out how the snake movements are accomplished.

This is true for many other games; once the analysis is done, the actual coding is often not so difficult.

I first made this code more than two years ago – and have not touched it in this period. When I decided to make this tutorial, I was surprised by the amount of refactoring I felt necessary and beneficial. The refactoring affected large parts of the code including the core of the game.

I only mention this to underline that any program job can be coded in many ways. As long as you
can get your code to work you should be happy!
And then you should refactor! .. and refactor again.

So, I fully acknowledge the freedom of coding; I do hope, however, you find my code inspirational.

The age of the code is also explaining the use of camelCase used for some varialbe names; I have abandoned this in newer programs and now always use snake_case.
Ha, mentioning snake_case in a snake game. LOL.

The code is fairly rough, no start page or end page, text appears in the middle of the game screen.
I have left out much of the 'sugar', simply to keep the code volume down. You can see in my other tutorials how to spice-up the basic code core.

I plan to issue a part #2 of this tutorial, featuring two players each controlling a snake and competing to get to the apple(s) first.
Try to modify this tutorial to do this – it's a nice little learning challenge.

The normal attachment function do not work, please get the attachment from my dropbox.
Posted Image
https://www.dropbox....icture.png?dl=0

Is This A Good Question/Topic? 0
  • +

Page 1 of 1