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}
)
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\$