I just now got around to implementing a full snake game in Pygame.
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:
grid_displayer
: this shows the grid continually updating as dictatated by thegrid_updater
function. This is already up for review at Grid displayer: Game of Life and Langton's Ant. The used part of the module is \35ドル\$ lines.
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 tomove
orgrow
, 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 thenext_snake_board(board, inputs, time, state)
by making use of thesnake_logic
functions and feeds it as argument to thegrid_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}
)
1 Answer 1
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
andrandom
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.
- Variables should have only one space on either side around the
- Use of spaces when defining argument variables and values within a function call:
- In
snake_main
, you havegrid_displayer.animate_grid( ... )
. Within this, you haveargname = value
formatted items. Within a function call like this, you should not have spaces around the equal sign.
- In
- 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 }
- 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:
- 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.
Explore related questions
See similar questions with these tags.
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\$-
button mang times to decrease the speed to playable levels \$\endgroup\$