I know there's already a whole bunch of similar attempts at this and I've looked at a few of them to get some tips. My main aim with this was not to write anything fancy. I wanted to create a simple game with the most basic functionality but write it really well. In other words I'm more interested in making my code professional, efficient and of high quality. One way I've tried to do this is to keep the main function simple and use it mostly for initialisation and the game loop, whilst implementing most of the functionality in separate classes and functions. Also, I would appreciate any advice on comments and docstrings because I'm sure mine are far from perfect. I also noticed the clock speed is around 4.5 GHz while playing this, so I think this could definitely do with an efficiency boost.
#!/usr/bin/env python3
import pygame as pg
from random import randint
# Define window parameters
BLOCK_SIZE = 20
WIN_SIZE = 500
class Head():
blue = (0, 0, 255) # Colour of snake
start_params = (BLOCK_SIZE * 0.05, BLOCK_SIZE * 0.05,
BLOCK_SIZE * 0.9, BLOCK_SIZE * 0.9)
def __init__(self, pos):
"""Head of snake"""
self.x = pos[0]
self.y = pos[1]
self.last_x = self.x
self.last_y = self.y
self.direction = [0, -BLOCK_SIZE]
self.square = None
def make_block(self):
"""
Create a surface to contain a square
Draw a square Rect object onto said surface
"""
self.square = pg.Surface((BLOCK_SIZE, BLOCK_SIZE), pg.SRCALPHA)
# Draw a square onto the "square" surface
pg.draw.rect(self.square, self.blue, self.start_params)
def update_pos(self):
"""Last coords are used to update next block in the snake"""
self.last_x = self.x
self.last_y = self.y
self.x += self.direction[0]
self.y += self.direction[1]
def change_direction(self, new_dir):
"""Change direction of snake without allowing it to go backwards"""
if new_dir == 'u' and self.direction != [0, BLOCK_SIZE]:
self.direction = [0, -BLOCK_SIZE]
elif new_dir == 'd' and self.direction != [0, -BLOCK_SIZE]:
self.direction = [0, BLOCK_SIZE]
elif new_dir == 'l' and self.direction != [BLOCK_SIZE, 0]:
self.direction = [-BLOCK_SIZE, 0]
elif new_dir == 'r' and self.direction != [-BLOCK_SIZE, 0]:
self.direction = [BLOCK_SIZE, 0]
def check_collision(self, pos_list):
"""Check if snake collides with wall or itself"""
if self.x in (0, WIN_SIZE) or self.y in (0, WIN_SIZE):
return True
if (self.x, self.y) in pos_list[3:]:
return True
return False
def get_pos(self):
return (self.last_x, self.last_y)
class Block(Head):
def __init__(self, next_block):
"""Body of snake"""
self.next = next_block
pos = next_block.get_pos()
self.x = pos[0]
self.y = pos[1]
self.last_x = self.x
self.last_y = self.y
self.ready = 0
def update_pos(self):
"""Use position of next block in snake to update current position"""
self.last_x = self.x
self.last_y = self.y
next_pos = self.next.get_pos()
self.x = next_pos[0]
self.y = next_pos[1]
def add_block(snake_arr):
"""Extend snake by adding a snake block to the snake array"""
snake_arr.append(Block(snake_arr[-1]))
snake_arr[-1].make_block()
return snake_arr
def check_keypress(input_event, block_object):
"""
Take input event and change direction if arrow key
or quit game if esc key or other exit signal
"""
if input_event.type == pg.QUIT:
return True
elif input_event.type == pg.KEYDOWN:
if input_event.key == pg.K_ESCAPE:
return True
elif input_event.key == pg.K_UP:
block_object.change_direction('u')
elif input_event.key == pg.K_DOWN:
block_object.change_direction('d')
elif input_event.key == pg.K_LEFT:
block_object.change_direction('l')
elif input_event.key == pg.K_RIGHT:
block_object.change_direction('r')
return False
class Food():
def __init__(self):
"""Food block, created in the same way as a snake block"""
self.exists = False
self.x = None
self.y = None
self.square = None
def add_food(self):
"""If no food present, create a new food block with random position"""
if self.exists is False:
# Create a surface to contain a square
self.square = pg.Surface((BLOCK_SIZE, BLOCK_SIZE), pg.SRCALPHA)
# Draw a square onto the "square" surface
pg.draw.rect(self.square, (255, 0, 0),
(BLOCK_SIZE * 0.05, BLOCK_SIZE * 0.05,
BLOCK_SIZE * 0.9, BLOCK_SIZE * 0.9))
self.x = randint(1, (WIN_SIZE - BLOCK_SIZE)/BLOCK_SIZE) * BLOCK_SIZE
self.y = randint(1, (WIN_SIZE - BLOCK_SIZE)/BLOCK_SIZE) * BLOCK_SIZE
self.exists = True
def check_if_eaten(self, snake):
"""If snake head is in food block, food is eaten"""
snake_x, snake_y = snake[0]
if (self.x <= snake_x <= self.x + BLOCK_SIZE * 0.9) and (self.y <= snake_y <= self.y + BLOCK_SIZE * 0.9):
self.exists = False
return True
return False
def main():
# Initialise PyGame
pg.init()
clock = pg.time.Clock()
size = (WIN_SIZE, WIN_SIZE) # Size of window, (width, height)
black = (0, 0, 0) # Background colour of window
# Place head of snake in centre of window
start_coord = (WIN_SIZE / 2) - (BLOCK_SIZE / 2)
# Create window
screen = pg.display.set_mode(size)
head = Head([start_coord, start_coord])
head.make_block()
# Make first three blocks of snake
snake = []
snake.append(head)
snake = add_block(snake)
snake = add_block(snake)
ticker = 0
game_over = False
food = Food()
# Game loop
while game_over is False:
# Run game at 60 FPS
clock.tick(60)
# Monitor events and check for keypresses
for event in pg.event.get():
game_over = check_keypress(event, head)
if game_over is True:
continue
snake_pos = [block.get_pos() for block in snake]
game_over = head.check_collision(snake_pos)
# Update snake position every 4 frames
if ticker == 3:
for s in snake:
s.update_pos()
ticker = 0
ticker += 1
food.add_food()
eaten = food.check_if_eaten(snake_pos)
if eaten is True:
snake = add_block(snake)
# Clear the window before the next frame
screen.fill(black)
# Draw block to window
screen.blit(food.square, [food.x, food.y])
for s in snake:
screen.blit(s.square, [s.x, s.y])
# Swap buffers
pg.display.flip()
pg.quit()
if __name__ == "__main__":
main()
```