I created and finished a Snake using pygame (in Python) utilising feedback I had from a previous code review a year or two ago. I'm pretty happy with the current state and havn't identified any bugs but would love any feedback on this, in terms of logic, coding principals, best practice, or anything else etc.
import pygame, random, time
CELL_SIZE_PX = 15 #the size of each logical 'square' in the game (snake, food etc) in pixels
GRID_WIDTH = 20 # total number of cells wide game is
GRID_HEIGHT = 20 # total number of cells tall game is
GRID_SIZE = (GRID_HEIGHT * CELL_SIZE_PX,GRID_WIDTH * CELL_SIZE_PX) #creates tuple for easier reference of game size
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
PINK = (255, 0, 255)
BLUE = (0, 255, 255)
NUMBER_OF_FOOD = 1 #defines how many food icons appear at any moment
FOODLIST = set() #init a set for holding where food is
TICK_SPEED = 15 #speed of the game
class Snake:
def __init__(self,start: tuple, length: int, direction: tuple): #start = tuple containing y,x of the head of the snake, length = how many segments, direction = which way it will start moving, as a tuple which determines direction by adding to the location tuple
self.segments = [] #holds snake location as tuples
for i in range(length):
self.segments.append(start) #snake is bunched up on starting square
self.directionModifer = direction #a tuple that is used to determine the next square to move to based on the key presses. this tuple contains a value to add to the existing lead square tuple.
def grow(self): #adds extra segment to snake at end of the list
self.segments.append(self.segments[-1])
def move(self): #updates the self.segments list location to move the snake
tempList = [] #list to hold updated location
headLocation = self.segments[0] # the current snake lead square is stored at position zero in the list
newHeadLocation = (headLocation[0]+self.directionModifer[0],headLocation[1]+self.directionModifer[1]) #determines next location for head based on which way the snake is facing
tempList.append(newHeadLocation) # writes new head location of snake to temporary list
for index, segment in enumerate(self.segments):
tempList.append(segment) #appends remaining snake segments to temp list, essentially pushing them down the list by 1 position.
del tempList[-1] #removes last segment location as the snake didn't get longer since this temp list is 1 item too long (due to adding new head location)
self.segments = tempList
class Display():
def __init__(self):
self.display = pygame.display.set_mode(GRID_SIZE) #set display size
self.font = pygame.font.Font(pygame.font.get_default_font(), 25) #set fonts use for score message
def show(self,snake: Snake): #draws all items to screen
self.display.fill(WHITE)
self.draw_food()
self.draw_snake(snake)
pygame.display.update()
def draw_food(self): #draws a pink square for each tuple in FOODLIST set
for i in FOODLIST:
pygame.draw.rect(
self.display, PINK,
pygame.Rect(
i[1] * CELL_SIZE_PX-1,
i[0] * CELL_SIZE_PX-1,
CELL_SIZE_PX-1,
CELL_SIZE_PX-1,
)
)
def draw_snake(self,snake: Snake): #draws blue square for each snake segment from passed snake object
for i in snake.segments:
pygame.draw.rect(
self.display, BLUE,
pygame.Rect(
i[1] * CELL_SIZE_PX-1,
i[0] * CELL_SIZE_PX-1,
CELL_SIZE_PX-1,
CELL_SIZE_PX-1,
)
)
def show_score(self,score: int): #shows score (number of segments) and counts down to next game
for i in reversed(range(4)):
self.display.fill(WHITE)
score_text = self.font.render('Score: {0}'.format(score), True, BLACK)
score_rect = score_text.get_rect(center=(GRID_SIZE[1]/2, GRID_SIZE[0]/3))
self.display.blit(score_text, score_rect)
countdown_text = self.font.render(str(i+1), True, BLACK)
countdown_rect = countdown_text.get_rect(center=(GRID_SIZE[1]/2, GRID_SIZE[0]/2))
self.display.blit(countdown_text, countdown_rect)
pygame.display.update()
time.sleep(1)
def create_food(snake: Snake): #builds list of all empty space and choses one at random for food location
emptySpace = [] #list to hold areas taht are not occupied by snakes
for y in range(GRID_HEIGHT): #iterate through all possible locations in the game
for x in range(GRID_WIDTH):
tup = (y,x) #build a tuple based of each coordinate location
if tup not in snake.segments: #as long as the location is not in a snake segment...
emptySpace.append(tup) # add coordinate to list of potentional food locations
for i in range(NUMBER_OF_FOOD - len(FOODLIST)): #this is added in case more than 1 food is wanted
chosenLocation = random.choice(emptySpace) #choose a random valid location
FOODLIST.add(chosenLocation) #append it to food list
def session(screen: Display):
snake1 = Snake((5,5),5,(0,1)) #init new snake
clock = pygame.time.Clock() #init clock
create_food(snake1) #generate starting food icons
pygame.event.clear() #clears events to prevent unexpected direction change at start of game
while True:
clock.tick(TICK_SPEED)
snake1.move() #snake moves every tick, so update logical list containing segments
for event in pygame.event.get(): # returns list of events and clears queue
if event.type == pygame.QUIT:
pygame.quit()
exit()
if event.type == pygame.KEYDOWN: #checks key is pressed
if event.key == pygame.K_UP and snake1.directionModifer != (1,0): #if UP is pressed and direction modifier does not reference DOWN, so that snake cannot go back on itself
snake1.directionModifer = (-1,0)
break #necessary to prevent game reading rapid inputs and causing snake to double back on itself and crash
elif event.key == pygame.K_RIGHT and snake1.directionModifer != (0,-1): #if RIGHT is pressed and direction modifier does not reference LEFT, so that snake cannot go back on itself
snake1.directionModifer = (0,1)
break #necessary to prevent game reading rapid inputs and causing snake to double back on itself and crash
elif event.key == pygame.K_DOWN and snake1.directionModifer != (-1,0): #if DOWN is pressed and direction modifier does not reference UP, so that snake cannot go back on itself
snake1.directionModifer = (1,0)
break #necessary to prevent game reading rapid inputs and causing snake to double back on itself and crash
elif event.key == pygame.K_LEFT and snake1.directionModifer != (0,1): #if LEFT is pressed and direction modifier does not reference RIGHT, so that snake cannot go back on itself
snake1.directionModifer = (0,-1)
break #necessary to prevent game reading rapid inputs and causing snake to double back on itself and crash
if snake1.segments[0] in FOODLIST: #check if snake contacts any food
FOODLIST.remove(snake1.segments[0]) #removes this food from the set
create_food(snake1) #adds new food
snake1.grow()
head = snake1.segments[0] #get snake head tuple
if head[0] < 0 or head[0] >= GRID_HEIGHT or head[1] < 0 or head[1] >= GRID_WIDTH or head in snake1.segments[1:]: #checks for border collison
return len(snake1.segments) #returns to main with score
screen.show(snake1) #updates visuals
def main():
pygame.init()
pygame.display.set_caption("Snake")
try:
screen = Display()
while True:
score = session(screen)
screen.show_score(score)
finally:
pygame.quit()
if __name__ == '__main__':
main()