0
\$\begingroup\$

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()

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Pretty okay!

You have the start of type hints, but tuple needs to be filled out with an element type, i.e. start: tuple[int, int].

Snake.__init__ needs that comment to be moved from inline to a """ """ docstring on the first line in the function.

Use snake_case for your member variable names and locals, i.e. direction_modifier.

index, segment in enumerate(self.segments) doesn't use index, so delete the enumerate.

Don't append () to class definitions.

Functions like show() need to return-hint -> None.

for i in FOODLIST needs to be for y, x in FOODLIST to unpack the tuples and avoid [1], [0].

('Score: {0}'.format(score) is better-expressed with string interpolation, i.e. f'Score: {score}'.

FOODLIST is not a constant; don't make it look like one. The best place for this would be a non-display Game logic class.

Don't reversed(range(4)); instead use range(3, -1, -1).

When constructing Snake((5, 5), 5, (0, 1)), those parameters would be much clearer as named parameters.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I've been through your feedback and incorporated into my code, thank you :) \$\endgroup\$
    – blib
    Oct 15, 2022 at 23:17

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.