3
\$\begingroup\$

I just now got around to implementing a full snake game in Pygame.

Snake game frame

The player can move his snake with the arrow keys and as in the original, the snake continues to move in the direction of the last key pressed.

  • Dark Green : Snake Head
  • Light Green : Snake Body
  • Red : Fruit

The player can adjust the snake speed based on his skill level with the use of the + and - keys.

The board size is fixed during the game but may be changed by altering the SIZE constant in snake_logic.

The code is divided in 3 files:

import sys, pygame
import random
from itertools import count


def show_grid(grid, screen, screen_size, color_decider):
    """
    Shows the `grid` on the `screen`.
    The colour of each cell is given by color_decider,
    a function of the form (cell -> rgb_triplet)
    """
    number_of_squares = len(grid)
    square_size = screen_size[0] // number_of_squares

    for y, row in enumerate(grid):
        for x, item in enumerate(row):
            pygame.draw.rect(screen, color_decider(item), (x * square_size, y * square_size, square_size, square_size), 0)

def animate_grid(grid, grid_updater, color_decider, screen_size=(600, 600), state={}):
    """
    Repeatedly calls `show_grid` to show a continually updating grid.
    """
    pygame.init()
    screen = pygame.display.set_mode( screen_size )

    for ticks in count(0):
        user_inputs = pygame.event.get()
        # if user_inputs: print(repr(user_inputs))
        show_grid(grid, screen, screen_size, color_decider)
        grid, state = grid_updater(grid, user_inputs, ticks, state)

        pygame.display.flip()
  • snake_logic: This contains the code that explains what it means for a snake to move or grow, or in general the actions related to the board. The file is \$67\$ lines long but about half of it it is tests (this code makes no contact with the outside world so it is easy to test):
import doctest
import random

SIZE = 15

def grow(head, body, heading):
    """
    >>> grow( (1, 2), [ (1, 3), (2, 3) ], (0, -1) )
    ((1, 2), [(1, 3), (2, 3), (2, 4)])
    """
    last = ([head] + body[:])[-1]
    return head, body[:] + [( (last[0] - heading[0]) % SIZE, (last[1] - heading[1]) % SIZE)]

def move(head, body, vector):
    """
    >>> move( (1, 2), [(1, 3), (1, 4), (1, 5)], (0, -1))
    ((1, 1), [(1, 2), (1, 3), (1, 4)])
    >>> move( (1, 2), [], (0, -1))
    ((1, 1), [])
    """
    nbody =  [head] + body[:-1]
    head = ((head[0] + vector[0]) % SIZE, (head[1] + vector[1]) % SIZE)
    return (head, nbody) if body else (head, [])

def remove_all_snake(matrix):
    """
    >>> remove_all_snake([ [' ', ' ', 'H'],
    ...                    [' ', ' ', 'B'],
    ...                    [' ', ' ', ' ']])
    [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
    """
    return [ [(' ' if cell in 'HB' else cell) for cell in row] for row in matrix]

def new_snake_board(size):
    """
    >>> random.seed(0)
    >>> for line in new_snake_board(4): print(line)
    [' ', ' ', ' ', ' ']
    [' ', ' ', ' ', ' ']
    [' ', ' ', 'H', ' ']
    [' ', ' ', ' ', 'F']

    """
    b = [ [' ' for _ in range(size)] for _ in range(size)]
    b[random.randint(0, size-1)][random.randint(0, size-1)] = "F"
    b[size//2][size//2] = 'H'
    return b

def spawn_fruit(board, head, body, size=SIZE):
    """
    >>> random.seed(0)
    >>> for line in spawn_fruit([ [' ', ' ', ' '],
    ...               ['H', 'B', 'B'],
    ...               [' ', ' ', ' '] ], (0, 1), [(1,1), (2,1)], size=3): print(line)
    [' ', ' ', ' ']
    ['H', 'B', 'B']
    [' ', ' ', 'F']
    """               
    new_board = board[:]
    while True:
        point = (random.randint(0, size-1),random.randint(0, size-1))
        if point not in ([head] + body):
            new_board[point[1]][point[0]] = 'F'
            return new_board

if __name__ == "__main__":
    doctest.testmod()
  • snake_main: this is the longest and most complex of the files of code. It defines the next_snake_board(board, inputs, time, state) by making use of the snake_logic functions and feeds it as argument to the grid_displayer
import pygame
import random

import snake_logic
import grid_displayer

DARK_GREEN = (0, 120, 0)
LIGHT_GREEN = (0, 255, 0)
RED = (200, 0, 0)
WHITE = (255, 255, 255)

def next_snake_board(board, inputs, time, state):

    head  = state["head"]
    body  = state["body"]

    new_head, new_body = head, body
    slowdown_offset = 0

    for event in inputs:
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_DOWN:
                state["going"] = (0, 1) if state["going"] != (0, -1) else state["going"]
            if event.key == pygame.K_UP:
                state["going"] = (0, -1) if state["going"] != (0, 1) else state["going"]
            if event.key == pygame.K_RIGHT:
                state["going"] = (1, 0) if state["going"] != (-1, 0) else state["going"]
            if event.key == pygame.K_LEFT:
                state["going"] = (-1, 0) if state["going"] != (1, 0) else state["going"]
            if event.key == pygame.K_PLUS:
                slowdown_offset -= 1 if state["speed"] != 1 else 0
            if event.key == pygame.K_MINUS:
                slowdown_offset += 1
    if time % state["slow_down"] == 0:
        new_head, new_body = snake_logic.move(head, body, state["going"])

    if board[new_head[1]][new_head[0]] == "F":
        new_head, new_body = snake_logic.grow(new_head,  new_body, state["going"])
        board = snake_logic.spawn_fruit(board, new_head, new_body)
    elif board[new_head[1]][new_head[0]] == "B":
        new_head, new_body = (snake_logic.SIZE//2, snake_logic.SIZE//2), []


    new_board = snake_logic.remove_all_snake(board[:])
    new_board[new_head[1]][new_head[0]] = 'H'
    for body_part in new_body:
        new_board[body_part[1]][body_part[0]] = 'B'
    return new_board, {"going": state["going"], "head":new_head, "body":new_body, "slow_down":state["slow_down"] + slowdown_offset}

def snake_color_decider(cell):
    kind_to_color = {
        'H' : DARK_GREEN,
        'B' : LIGHT_GREEN,
        'F' : RED,
        ' ' : WHITE
    }
    return kind_to_color[cell]

if __name__ == "__main__":
    grid_displayer.animate_grid(
        grid = snake_logic.new_snake_board(snake_logic.SIZE),
        grid_updater = next_snake_board,
        color_decider =  snake_color_decider,
        state = {"going" : (0, 1), "head" : (snake_logic.SIZE//2, snake_logic.SIZE//2), "body":[], "slow_down":5}
    )
\$\endgroup\$
5
  • 1
    \$\begingroup\$ Not sure this works as posted - no life_logic defined anywhere per this question. If you're using code from another question, that should probably still be included here so we aren't hopping around between questions... \$\endgroup\$ Commented Oct 6, 2016 at 20:44
  • \$\begingroup\$ @ThomasWard imports removed, it does not need those files \$\endgroup\$
    – Caridorc
    Commented Oct 6, 2016 at 20:47
  • \$\begingroup\$ @ThomasWard press the - button mang times to decrease the speed to playable levels \$\endgroup\$
    – Caridorc
    Commented Oct 6, 2016 at 21:26
  • \$\begingroup\$ yeah reread after that comment and read that - starting at light speed seems like a bad thing though. \$\endgroup\$ Commented Oct 6, 2016 at 22:12
  • \$\begingroup\$ Not included in my review I posted (and sorry I complain a lot about style, but... :P), but could you consider actually implementing a way to quit the game without me having to ctrl+c at the command line? There's no way to close the game from the graphical window it pops up :P \$\endgroup\$ Commented Oct 6, 2016 at 23:56

1 Answer 1

1
\$\begingroup\$

Unnecessary import of modules

There are a few cases of imported modules that aren't getting used.

  • snake_main: random is imported but never used.
  • grid_displayer: sys and random are imported but never used.

There's no need to load modules which are not needed.


Style: Only one import per line

import sys, pygame (from grid_displayer, as an example) is frowned upon by PEP8. Instead you should be separating imports into individual import lines:

import sys
import pygame
....

(Note that sys here isn't needed, see my comment on 'unnecessary import of modules' above)


Style: Follow PEP8 styling for spaces and line breaks

I know this is a long section, and normally I wouldn't be harping much on code style, but this inconsistent usage of spacing and breaks irks me enough to point out the list of issues observed. The code still works, but from a style perspective, there's some irksome inconsistencies in usage, so let's briefly review PEP8 guidelines here.

There are numerous cases where you misuse spaces or don't have enough line breaks. These're the rules of thumb you should go through and look at specifically:

  • Use of spaces with function-level variables
    • Variables should have only one space on either side around the = or assignment operators.
  • Use of spaces when defining argument variables and values within a function call:
    • In snake_main, you have grid_displayer.animate_grid( ... ). Within this, you have argname = value formatted items. Within a function call like this, you should not have spaces around the equal sign.
  • Use of spaces within dicts
    • Dictionary keys and values should be defined like this with a space after the colons. Items should also be separated by a comma followed by a space: { 'foo': 1, 'bar': 'foo', 'baz': 600 }
  • Line breaks between functions and within functions/methods
    • Top level function and class definitions should have two blank lines around them.
    • Method definitions within a class should be separated by one blank line.
    • Blank lines within a function or class or method should be used sparingly to indicate logical sections (however, this likely should not be more than one blank line between them).

Bug in gameplay: On Snake death and a brand new 'game' started, fruit location remains unchanged on board

Normally, it should be that the fruit gets updated and re-positioned on the grid when the entire map is regenerated upon snake death; this does not currently happen, and the 'last location' for the fruit is kept.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.