Beginner's snake game using PyGame





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{
margin-bottom:0;
}








33














$begingroup$


I am a complete beginner when it comes to programming and writing games and this is my first ever game. I made it with python 3 using pygame library. I would appreciate any feedback really.



from __future__ import annotations
from typing import Tuple, List
import pygame
import random
import sys


# screen width & height and block size
bg_width = 500
bg_height = 500
block_size = 10

# direction strings
left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"

# colors (RGB)
bg_color = black = (0, 0, 0)
yellow = (255, 255, 0)
green = (0, 128, 0)
red = (255, 0, 0)
blue = (0, 0, 255)

# pygame & font initialization
pygame.init()
window = pygame.display.set_mode((bg_width, bg_height))
pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')
fps = pygame.time.Clock()


class Snake:
"""This class represents a snake object. Every snake object consists of a head
and its body.
===Attributes===
head: snake head
color: snake color
body: snake body
direction: direction of head
size: size of snake (head and body)
"""
head: List[int, int]
color: Tuple[int, int, int]
body: List[List[int, int]]
direction: str
size: int

def __init__(self, color: Tuple[int, int, int]) -> None:
self.head = [int(10*block_size), int(5*block_size)]
self.color = color
self.body = [self.head, [9*block_size, 5*block_size]]
self.direction = right
self.size = 2

def change_dir(self, direc: str) -> None:
if self.direction != left and direc == right:
self.direction = right
elif self.direction != right and direc == left:
self.direction = left
elif self.direction != down and direc == up:
self.direction = up
elif self.direction != up and direc == down:
self.direction = down

def move(self) -> None:
if self.direction == right:
self.head[0] += block_size
elif self.direction == left:
self.head[0] -= block_size
elif self.direction == up:
self.head[1] -= block_size
elif self.direction == down:
self.head[1] += block_size
self.body.insert(0, list(self.head))
self.body.pop()
if self.body[0] != self.head:
self.head = self.body[0]

def add_to_tail(self) -> None:
new_part = [self.body[-1][0], self.body[-1][1]]
self.body.append(new_part)
self.size += 1

def get_body(self) -> List[List[int, int]]:
return self.body


class Food:
"""This class represents a food object. Each food object has an x
and a y value, a color and a state.
===Attributes===
x: x-coordinate of food object
y: y-coordinate of food object
color: color of food object
state: whether food object is on screen or not
position: x,y-coordinates pair of food object
"""
x: int
y: int
color: Tuple[int, int, int]
state: bool
position: Tuple[int, int]

def __init__(self, color: Tuple[int, int, int]) -> None:
self.x = random.randint(0, bg_width//block_size - 1)*block_size
self.y = random.randint(0, bg_height//block_size - 1)*block_size
self.color = color
self.state = True
self.position = self.x, self.y

def spawn(self) -> Tuple[int, int]:
if self.state:
return self.x, self.y
else:
self.state = True
self.x = random.randint(0, bg_width // block_size-1) * block_size
self.y = random.randint(0, bg_height // block_size-1) * block_size
return self.x, self.y

def update(self, state) -> None:
self.state = state


def collision(snake_: Snake, food_target_x: int, food_target_y: int) -> int:
snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
block_size)
if snake_rect == food_rect:
snake_.add_to_tail()
return 1
return 0


def wall_collision(s: Snake) -> bool:
if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
or (s.head[1] > bg_height-block_size):
return True
return False


def game():

# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:

# Game over Screen
while is_over:
text_on_screen = font.render("You scored: " + str(score) +
", Press R to play again or Q to quit",
True, text_colour)
window.blit(text_on_screen, [55, 225])
for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_r]:
game()
pygame.display.update()

# check events
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
snake.change_dir(right)
elif pressed[pygame.K_LEFT] or pressed[pygame.K_a]:
snake.change_dir(left)
elif pressed[pygame.K_UP] or pressed[pygame.K_w]:
snake.change_dir(up)
elif pressed[pygame.K_DOWN] or pressed[pygame.K_s]:
snake.change_dir(down)

# fill window and draw snake
window.fill(black)
for item in snake.get_body():
pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
block_size])

# move snake
snake.move()

# check for collision with wall:
collision_with_wall = wall_collision(snake)
if collision_with_wall:
is_over = True

# check if food is still on screen and draw it
food_pos = food.spawn()
collision_ = collision(snake, *food_pos)
if collision_ == 1:
score += 1
food.update(False)
pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],
block_size, block_size])

# renders display
pygame.display.flip()

# time delay
pygame.time.delay(60)
fps.tick(30)

pygame.quit()
quit()

game()









share|improve this question











$endgroup$












  • 4




    $begingroup$
    It looks good, especially for your first game :)
    $endgroup$
    – 0liveradam8
    May 28 at 0:23










  • $begingroup$
    I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
    $endgroup$
    – Peter Cordes
    May 29 at 18:44










  • $begingroup$
    Where did you get the __future__ package from? I can't install it using PyCharm.
    $endgroup$
    – Thomas Weller
    Jun 2 at 7:21




















33














$begingroup$


I am a complete beginner when it comes to programming and writing games and this is my first ever game. I made it with python 3 using pygame library. I would appreciate any feedback really.



from __future__ import annotations
from typing import Tuple, List
import pygame
import random
import sys


# screen width & height and block size
bg_width = 500
bg_height = 500
block_size = 10

# direction strings
left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"

# colors (RGB)
bg_color = black = (0, 0, 0)
yellow = (255, 255, 0)
green = (0, 128, 0)
red = (255, 0, 0)
blue = (0, 0, 255)

# pygame & font initialization
pygame.init()
window = pygame.display.set_mode((bg_width, bg_height))
pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')
fps = pygame.time.Clock()


class Snake:
"""This class represents a snake object. Every snake object consists of a head
and its body.
===Attributes===
head: snake head
color: snake color
body: snake body
direction: direction of head
size: size of snake (head and body)
"""
head: List[int, int]
color: Tuple[int, int, int]
body: List[List[int, int]]
direction: str
size: int

def __init__(self, color: Tuple[int, int, int]) -> None:
self.head = [int(10*block_size), int(5*block_size)]
self.color = color
self.body = [self.head, [9*block_size, 5*block_size]]
self.direction = right
self.size = 2

def change_dir(self, direc: str) -> None:
if self.direction != left and direc == right:
self.direction = right
elif self.direction != right and direc == left:
self.direction = left
elif self.direction != down and direc == up:
self.direction = up
elif self.direction != up and direc == down:
self.direction = down

def move(self) -> None:
if self.direction == right:
self.head[0] += block_size
elif self.direction == left:
self.head[0] -= block_size
elif self.direction == up:
self.head[1] -= block_size
elif self.direction == down:
self.head[1] += block_size
self.body.insert(0, list(self.head))
self.body.pop()
if self.body[0] != self.head:
self.head = self.body[0]

def add_to_tail(self) -> None:
new_part = [self.body[-1][0], self.body[-1][1]]
self.body.append(new_part)
self.size += 1

def get_body(self) -> List[List[int, int]]:
return self.body


class Food:
"""This class represents a food object. Each food object has an x
and a y value, a color and a state.
===Attributes===
x: x-coordinate of food object
y: y-coordinate of food object
color: color of food object
state: whether food object is on screen or not
position: x,y-coordinates pair of food object
"""
x: int
y: int
color: Tuple[int, int, int]
state: bool
position: Tuple[int, int]

def __init__(self, color: Tuple[int, int, int]) -> None:
self.x = random.randint(0, bg_width//block_size - 1)*block_size
self.y = random.randint(0, bg_height//block_size - 1)*block_size
self.color = color
self.state = True
self.position = self.x, self.y

def spawn(self) -> Tuple[int, int]:
if self.state:
return self.x, self.y
else:
self.state = True
self.x = random.randint(0, bg_width // block_size-1) * block_size
self.y = random.randint(0, bg_height // block_size-1) * block_size
return self.x, self.y

def update(self, state) -> None:
self.state = state


def collision(snake_: Snake, food_target_x: int, food_target_y: int) -> int:
snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
block_size)
if snake_rect == food_rect:
snake_.add_to_tail()
return 1
return 0


def wall_collision(s: Snake) -> bool:
if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
or (s.head[1] > bg_height-block_size):
return True
return False


def game():

# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:

# Game over Screen
while is_over:
text_on_screen = font.render("You scored: " + str(score) +
", Press R to play again or Q to quit",
True, text_colour)
window.blit(text_on_screen, [55, 225])
for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_r]:
game()
pygame.display.update()

# check events
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
snake.change_dir(right)
elif pressed[pygame.K_LEFT] or pressed[pygame.K_a]:
snake.change_dir(left)
elif pressed[pygame.K_UP] or pressed[pygame.K_w]:
snake.change_dir(up)
elif pressed[pygame.K_DOWN] or pressed[pygame.K_s]:
snake.change_dir(down)

# fill window and draw snake
window.fill(black)
for item in snake.get_body():
pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
block_size])

# move snake
snake.move()

# check for collision with wall:
collision_with_wall = wall_collision(snake)
if collision_with_wall:
is_over = True

# check if food is still on screen and draw it
food_pos = food.spawn()
collision_ = collision(snake, *food_pos)
if collision_ == 1:
score += 1
food.update(False)
pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],
block_size, block_size])

# renders display
pygame.display.flip()

# time delay
pygame.time.delay(60)
fps.tick(30)

pygame.quit()
quit()

game()









share|improve this question











$endgroup$












  • 4




    $begingroup$
    It looks good, especially for your first game :)
    $endgroup$
    – 0liveradam8
    May 28 at 0:23










  • $begingroup$
    I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
    $endgroup$
    – Peter Cordes
    May 29 at 18:44










  • $begingroup$
    Where did you get the __future__ package from? I can't install it using PyCharm.
    $endgroup$
    – Thomas Weller
    Jun 2 at 7:21
















33












33








33


5



$begingroup$


I am a complete beginner when it comes to programming and writing games and this is my first ever game. I made it with python 3 using pygame library. I would appreciate any feedback really.



from __future__ import annotations
from typing import Tuple, List
import pygame
import random
import sys


# screen width & height and block size
bg_width = 500
bg_height = 500
block_size = 10

# direction strings
left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"

# colors (RGB)
bg_color = black = (0, 0, 0)
yellow = (255, 255, 0)
green = (0, 128, 0)
red = (255, 0, 0)
blue = (0, 0, 255)

# pygame & font initialization
pygame.init()
window = pygame.display.set_mode((bg_width, bg_height))
pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')
fps = pygame.time.Clock()


class Snake:
"""This class represents a snake object. Every snake object consists of a head
and its body.
===Attributes===
head: snake head
color: snake color
body: snake body
direction: direction of head
size: size of snake (head and body)
"""
head: List[int, int]
color: Tuple[int, int, int]
body: List[List[int, int]]
direction: str
size: int

def __init__(self, color: Tuple[int, int, int]) -> None:
self.head = [int(10*block_size), int(5*block_size)]
self.color = color
self.body = [self.head, [9*block_size, 5*block_size]]
self.direction = right
self.size = 2

def change_dir(self, direc: str) -> None:
if self.direction != left and direc == right:
self.direction = right
elif self.direction != right and direc == left:
self.direction = left
elif self.direction != down and direc == up:
self.direction = up
elif self.direction != up and direc == down:
self.direction = down

def move(self) -> None:
if self.direction == right:
self.head[0] += block_size
elif self.direction == left:
self.head[0] -= block_size
elif self.direction == up:
self.head[1] -= block_size
elif self.direction == down:
self.head[1] += block_size
self.body.insert(0, list(self.head))
self.body.pop()
if self.body[0] != self.head:
self.head = self.body[0]

def add_to_tail(self) -> None:
new_part = [self.body[-1][0], self.body[-1][1]]
self.body.append(new_part)
self.size += 1

def get_body(self) -> List[List[int, int]]:
return self.body


class Food:
"""This class represents a food object. Each food object has an x
and a y value, a color and a state.
===Attributes===
x: x-coordinate of food object
y: y-coordinate of food object
color: color of food object
state: whether food object is on screen or not
position: x,y-coordinates pair of food object
"""
x: int
y: int
color: Tuple[int, int, int]
state: bool
position: Tuple[int, int]

def __init__(self, color: Tuple[int, int, int]) -> None:
self.x = random.randint(0, bg_width//block_size - 1)*block_size
self.y = random.randint(0, bg_height//block_size - 1)*block_size
self.color = color
self.state = True
self.position = self.x, self.y

def spawn(self) -> Tuple[int, int]:
if self.state:
return self.x, self.y
else:
self.state = True
self.x = random.randint(0, bg_width // block_size-1) * block_size
self.y = random.randint(0, bg_height // block_size-1) * block_size
return self.x, self.y

def update(self, state) -> None:
self.state = state


def collision(snake_: Snake, food_target_x: int, food_target_y: int) -> int:
snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
block_size)
if snake_rect == food_rect:
snake_.add_to_tail()
return 1
return 0


def wall_collision(s: Snake) -> bool:
if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
or (s.head[1] > bg_height-block_size):
return True
return False


def game():

# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:

# Game over Screen
while is_over:
text_on_screen = font.render("You scored: " + str(score) +
", Press R to play again or Q to quit",
True, text_colour)
window.blit(text_on_screen, [55, 225])
for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_r]:
game()
pygame.display.update()

# check events
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
snake.change_dir(right)
elif pressed[pygame.K_LEFT] or pressed[pygame.K_a]:
snake.change_dir(left)
elif pressed[pygame.K_UP] or pressed[pygame.K_w]:
snake.change_dir(up)
elif pressed[pygame.K_DOWN] or pressed[pygame.K_s]:
snake.change_dir(down)

# fill window and draw snake
window.fill(black)
for item in snake.get_body():
pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
block_size])

# move snake
snake.move()

# check for collision with wall:
collision_with_wall = wall_collision(snake)
if collision_with_wall:
is_over = True

# check if food is still on screen and draw it
food_pos = food.spawn()
collision_ = collision(snake, *food_pos)
if collision_ == 1:
score += 1
food.update(False)
pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],
block_size, block_size])

# renders display
pygame.display.flip()

# time delay
pygame.time.delay(60)
fps.tick(30)

pygame.quit()
quit()

game()









share|improve this question











$endgroup$




I am a complete beginner when it comes to programming and writing games and this is my first ever game. I made it with python 3 using pygame library. I would appreciate any feedback really.



from __future__ import annotations
from typing import Tuple, List
import pygame
import random
import sys


# screen width & height and block size
bg_width = 500
bg_height = 500
block_size = 10

# direction strings
left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"

# colors (RGB)
bg_color = black = (0, 0, 0)
yellow = (255, 255, 0)
green = (0, 128, 0)
red = (255, 0, 0)
blue = (0, 0, 255)

# pygame & font initialization
pygame.init()
window = pygame.display.set_mode((bg_width, bg_height))
pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')
fps = pygame.time.Clock()


class Snake:
"""This class represents a snake object. Every snake object consists of a head
and its body.
===Attributes===
head: snake head
color: snake color
body: snake body
direction: direction of head
size: size of snake (head and body)
"""
head: List[int, int]
color: Tuple[int, int, int]
body: List[List[int, int]]
direction: str
size: int

def __init__(self, color: Tuple[int, int, int]) -> None:
self.head = [int(10*block_size), int(5*block_size)]
self.color = color
self.body = [self.head, [9*block_size, 5*block_size]]
self.direction = right
self.size = 2

def change_dir(self, direc: str) -> None:
if self.direction != left and direc == right:
self.direction = right
elif self.direction != right and direc == left:
self.direction = left
elif self.direction != down and direc == up:
self.direction = up
elif self.direction != up and direc == down:
self.direction = down

def move(self) -> None:
if self.direction == right:
self.head[0] += block_size
elif self.direction == left:
self.head[0] -= block_size
elif self.direction == up:
self.head[1] -= block_size
elif self.direction == down:
self.head[1] += block_size
self.body.insert(0, list(self.head))
self.body.pop()
if self.body[0] != self.head:
self.head = self.body[0]

def add_to_tail(self) -> None:
new_part = [self.body[-1][0], self.body[-1][1]]
self.body.append(new_part)
self.size += 1

def get_body(self) -> List[List[int, int]]:
return self.body


class Food:
"""This class represents a food object. Each food object has an x
and a y value, a color and a state.
===Attributes===
x: x-coordinate of food object
y: y-coordinate of food object
color: color of food object
state: whether food object is on screen or not
position: x,y-coordinates pair of food object
"""
x: int
y: int
color: Tuple[int, int, int]
state: bool
position: Tuple[int, int]

def __init__(self, color: Tuple[int, int, int]) -> None:
self.x = random.randint(0, bg_width//block_size - 1)*block_size
self.y = random.randint(0, bg_height//block_size - 1)*block_size
self.color = color
self.state = True
self.position = self.x, self.y

def spawn(self) -> Tuple[int, int]:
if self.state:
return self.x, self.y
else:
self.state = True
self.x = random.randint(0, bg_width // block_size-1) * block_size
self.y = random.randint(0, bg_height // block_size-1) * block_size
return self.x, self.y

def update(self, state) -> None:
self.state = state


def collision(snake_: Snake, food_target_x: int, food_target_y: int) -> int:
snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
block_size)
if snake_rect == food_rect:
snake_.add_to_tail()
return 1
return 0


def wall_collision(s: Snake) -> bool:
if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
or (s.head[1] > bg_height-block_size):
return True
return False


def game():

# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:

# Game over Screen
while is_over:
text_on_screen = font.render("You scored: " + str(score) +
", Press R to play again or Q to quit",
True, text_colour)
window.blit(text_on_screen, [55, 225])
for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_r]:
game()
pygame.display.update()

# check events
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
snake.change_dir(right)
elif pressed[pygame.K_LEFT] or pressed[pygame.K_a]:
snake.change_dir(left)
elif pressed[pygame.K_UP] or pressed[pygame.K_w]:
snake.change_dir(up)
elif pressed[pygame.K_DOWN] or pressed[pygame.K_s]:
snake.change_dir(down)

# fill window and draw snake
window.fill(black)
for item in snake.get_body():
pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
block_size])

# move snake
snake.move()

# check for collision with wall:
collision_with_wall = wall_collision(snake)
if collision_with_wall:
is_over = True

# check if food is still on screen and draw it
food_pos = food.spawn()
collision_ = collision(snake, *food_pos)
if collision_ == 1:
score += 1
food.update(False)
pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1],
block_size, block_size])

# renders display
pygame.display.flip()

# time delay
pygame.time.delay(60)
fps.tick(30)

pygame.quit()
quit()

game()






python python-3.x pygame snake-game






share|improve this question















share|improve this question













share|improve this question




share|improve this question



share|improve this question








edited May 28 at 4:03









SmartManoj

1033 bronze badges




1033 bronze badges










asked May 27 at 23:53









RandomDude_123RandomDude_123

1692 silver badges5 bronze badges




1692 silver badges5 bronze badges











  • 4




    $begingroup$
    It looks good, especially for your first game :)
    $endgroup$
    – 0liveradam8
    May 28 at 0:23










  • $begingroup$
    I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
    $endgroup$
    – Peter Cordes
    May 29 at 18:44










  • $begingroup$
    Where did you get the __future__ package from? I can't install it using PyCharm.
    $endgroup$
    – Thomas Weller
    Jun 2 at 7:21
















  • 4




    $begingroup$
    It looks good, especially for your first game :)
    $endgroup$
    – 0liveradam8
    May 28 at 0:23










  • $begingroup$
    I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
    $endgroup$
    – Peter Cordes
    May 29 at 18:44










  • $begingroup$
    Where did you get the __future__ package from? I can't install it using PyCharm.
    $endgroup$
    – Thomas Weller
    Jun 2 at 7:21










4




4




$begingroup$
It looks good, especially for your first game :)
$endgroup$
– 0liveradam8
May 28 at 0:23




$begingroup$
It looks good, especially for your first game :)
$endgroup$
– 0liveradam8
May 28 at 0:23












$begingroup$
I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
$endgroup$
– Peter Cordes
May 29 at 18:44




$begingroup$
I barely know Python: would body: List [Tuple[int,int]] be better? You have XY pairs, not a list of variable-length lists.
$endgroup$
– Peter Cordes
May 29 at 18:44












$begingroup$
Where did you get the __future__ package from? I can't install it using PyCharm.
$endgroup$
– Thomas Weller
Jun 2 at 7:21






$begingroup$
Where did you get the __future__ package from? I can't install it using PyCharm.
$endgroup$
– Thomas Weller
Jun 2 at 7:21












5 Answers
5






active

oldest

votes


















43
















$begingroup$

PEP 8



PEP 8 recommends that constants be written as "all capital letters with underscores separating words.". So something like:



yellow = (255, 255, 0)


Should be:



YELLOW = (255, 255, 0)


or possibly an enum (more on this later)



Inconsistent quotations



Usually a project will stick with either "" or '' unless you have particular reason not to.1 But for instance:



pygame.display.set_caption("Snake")
font = pygame.font.SysFont('Times New Roman', 20)
text_colour = pygame.Color('White')


violates the uniformity unnecessarily.



Enums



To quote the docs on enum:




An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.




So for instance:



left = "LEFT"
right = "RIGHT"
up = "UP"
down = "DOWN"


becomes:



from enum import Enum
class Direction(Enum):
LEFT = "LEFT"
RIGHT = "RIGHT"
DOWN = "DOWN"
UP = "UP"


See here for more reasons why you should port your stringy code to enums. In addition, there are probably some integer arithmetic tricks you could use to eliminate a lot of the if-elif-else chains. (You will need to do some further refactoring to get this to run without error.)



Unnecessary Comments



Consider:



# initialize food and snake
food = Food(blue)
snake = Snake(green)

# initialize loop logic
running = True
is_over = False

# initialize score
score = 0

# game loop
while running:


I, personally, would omit all the comments. I could see where a case can be made the game loop comments in the case that someone is trying to understand the code without knowledge of game loops, but I would argue the concept of a game loop is so ubiquitous that those comments get in the way.



If you had to write comments I would instead write:



# Initialize entities/data for new game
food = Food(blue)
snake = Snake(green)
score = 0

# Init/start game loop
running = True
is_over = False
while running:
...


Even then, I'm not quite satisfied with it, but the moral it to not add redundant comments. However, there may be some debate on this.



The best example is probably:



    # move snake
snake.move()


Really think about what this comment contributes.



Unnecessary assignments



I'm not sure why you wrote:



    # check for collision with wall:
collision_with_wall = wall_collision(snake)
if collision_with_wall:
is_over = True


When:



    if wall_collision(snake):
is_over = True


suffices.



Add a main function



You should consider adding a main function to your project. This is usually done through the introduction of:



if __name__ == "__main__":
game()


This allows for me to import your project and not have it automatically execute the game function at the bottom. Increasing re-usability.



1 For instance, It may be the project defaults to "", but you need to print out " Hi " with the quotes, so print('" Hi "') may be chosen.






share|improve this answer












$endgroup$















  • $begingroup$
    Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
    $endgroup$
    – BruceWayne
    May 28 at 14:27








  • 1




    $begingroup$
    In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
    $endgroup$
    – vakus
    May 29 at 9:53






  • 2




    $begingroup$
    @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
    $endgroup$
    – Milliarde
    May 29 at 13:01



















22
















$begingroup$

In addition to another answer I will add these places to improve (I will not repeat all things from that answer, my answer is just an addition):



1. Code inconsistency



You have several places in your code where your code is inconsistent



def collision():
...
return 1
return 0


def wall_collision():
...
return True
return False


Both functions are checking collisions but one returns integer (0/1) and another - boolean (True/False).



self.head = [int(10*block_size), int(5*block_size)]
self.body = [self.head, [9*block_size, 5*block_size]]


For head you convert X*block_size to int (it is unnecessary anyway). For body you don't do it.



I recommend you to always check your code for possible inconsistency.



2. Names inconsistency



I moved it to an another point because it is not about how your code works - it is about how another programmers will read your code.



Look at this two lines:



def change_dir(self, direc: str) -> None:
if self.direction != left and direc == right:


You use three different namings for direction entity:




  • dir

  • direct

  • direction


It is far better to use consistent variable names. If you are using different direction variables, get them names with only one word for directions. Here is the example:




  • snake_direction

  • pokemon_direction

  • some_strange_direction


or:




  • enemy_dir

  • arrow_dir

  • some_another_strange_dir


3. Small code improvements



Sometimes you look at your code and think: "Hmmm, it is working but looks not good". Often in these cases you can slightly reorganize your code to make it a bit better :)



for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()


can be shortened to:



for event in pygame.event.get():
pressed_key = pygame.key.get_pressed()
if event.type == pygame.QUIT or pressed_key[pygame.K_q]:
pygame.quit()
sys.exit()


This line of code is correct too but it is hard to read:



if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
or (s.head[1] > bg_height-block_size):


But it will be far better to read if you will change it to:



if (s.head[0] < 0 or
s.head[0] > bg_width-block_size or
s.head[1] < 0 or
s.head[1] > bg_height-block_size):


or even this (sometimes I use it for really long if's):



is_wall_collision = any([
s.head[0] < 0,
s.head[0] > bg_width-block_size,
s.head[1] < 0,
s.head[1] > bg_height-block_size
])
if is_wall_collision:





share|improve this answer










$endgroup$























    10
















    $begingroup$

        size: int
    ...

    self.size = 2


    You never read size, and Python Lists already know their own length in case you did need it.



    You're just duplicating functionality that List already gives you by manually keeping size in sync with the list length.





    Separate game logic from screen rendering details



    You keep snake coordinates scaled by pixels. This seems to complicate your code everywhere with block_size instead of 1; I suspect it would be easier to keep snake coordinates in units of blocks, and only scale to pixels for drawing purposes.



    e.g. then your outer-wall check could be something like s.head[0] >= bg_width instead of
    s.head[0] > bg_width-block_size.



    1 is special for integers because it's the difference between > and >=





    I like your add_to_tail idea of putting multiple snake segments in the same place initially, so move on future turns actually makes the snake longer on screen. That high-level design might be worthy of a comment.



    The other option would be a "future growth" counter that you check (and decrement) every move to decide whether to remove the last tail block. (That's what I was expecting because I hadn't thought of your idea). Your way is more efficient and will still work if you want food to cause multiple segments of growth. You can still do them all when the food is eaten.





    Keeping a separate head: I'm not sure this is helping you. Functions that want to look at the first segment can do head = snake_.body[0].



    I don't understand why move needs to do this:



           if self.body[0] != self.head:
    self.head = self.body[0]


    when it looks to me like inserting the modified head as a new first element will already create that condition. So either this is redundant (and confusing), or else it's extra work that you wouldn't need to do if you didn't redundantly keep the head coordinates in 2 places. (Front of the List, and in its own object.)





        direction: str


    This should be an enum or integer, not a string. String compares are fairly cheap, but not as cheap as integers. There's no reason that a direction needs to be an arbitrary string when it can take only 1 of 4 possible values. A string isn't making your code any easier to read.



    You probably still want the if/elif chain to turn a direction into an x or y offset, but another option for an integer would be a lookup table of [-1, 0] , [1, 0] etc. so you look up the x and y offsets for the given direction and just add them to the head's x and y coordinates without any conditionals.



    Or direction could actually be an XY vector, removing the lookup step.



        head[0] += direction[0]
    head[1] += direction[1]


    But that might complicate change_dir. Or give you a different implementation: to check for reversing direction: if new_dir + old_dir == [0,0] then you tried to double back. Otherwise you can set dir = new_dir.



    I like your design for having the keyboard-input just call change_dir to check the game-logic of that input. That works very cleanly, and would still work if left was defined as [-1, 0] instead of "LEFT".






    share|improve this answer










    $endgroup$























      3
















      $begingroup$

      Good going on your first game. I completely agree with most of the points made by others. Here are a few more nitpicks. This answer is a bit long-winded, but take it as a complement. :-)



      Unpack the Coordinates



      This is what lines 195-197 currently look like:



              for item in snake.get_body():
      pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
      block_size])


      Instead of relying on indexing, we can directly unpack item into its respective xy-coordinates. This makes for good brevity and readability.



              for x, y in snake.get_body():
      pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


      The same can be done for lines 208-214.



      # Before:
      food_pos = food.spawn()
      collision_ = collision(snake, *food_pos) # 🤔
      if collision_ == 1:
      score += 1
      food.update(False)
      pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1], # 🤔
      block_size, block_size])


      # After:
      food_x, food_y = food.spawn()
      # the rest is left as an exercise for the reader


      On wall_collision(Snake)



      Here's what the wall_collision function currently looks like (lines 137-141):



      def wall_collision(s: Snake) -> bool:
      if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
      or (s.head[1] > bg_height-block_size):
      return True
      return False



      1. Being a predicate (i.e. a function which returns a boolean), the function can be better named. You can always find a way to name a predicate such that it is prefixed with is or has. Instead of wall_collision, I'd go with is_colliding_with_wall or collides_with_wall to communicate the intention better.

      2. It could be simplified. vurmux's answer suggests a few possible ways. However, there exists an easier (and more readable) way of writing it in one line:


          return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


      I intentionally added the extraneous parentheses to group comparisons.



      This is made possible by Python's comparison "chaining" along with De Morgan's Laws. For fun, you can try to prove that the two snippets are equivalent.



      Points and Coordinates



      It took a while for me to figure out what the following line meant:



      self.head = [int(10*block_size), int(5*block_size)]


      I'm guessing this is a point/coordinate? Looks like it.



      Lists are so prevalent in Python, that they can hold a multitude of meanings. What's more, they are variable-length constructs, which communicates something else: you might be changing the length of the object in the future. This might mean appending, erasing, or inserting into the list.



      self.body uses a list appropriately, since it will be appended to and popped from in various places. On the other hand, not once does self.head use list.append, list.pop, or list.insert and this raises a question: does it need to be a list at all? It is better made a tuple, which is a fixed-length construct and immediately communicates to the reader that we won't be modifying the length of self.head.



      I commend the usage of tuples in Food.spawn() for this very reason. Still, tuples can carry a multitude of different meanings. We can do better by using collections.namedtuple and creating a record-type Point for the purpose of representing xy-coordinates. This can greatly reduce ambiguity and improve readability.



      from collections import namedtuple
      Point = namedtuple('Point', ['x', 'y']) # we now have a Point type

      class Snake:
      ...
      def __init__(self, color: Tuple[int, int, int]) -> None:
      self.head = Point(10*block_size, 5*block_size) # foolproof: self.head is a point
      self.body = [self.head, Point(...)]
      ...
      def add_to_tail(self) -> None:
      self.body.append(Point(self.body[-1][0], self.body[-1][1]))
      self.size += 1


      The only pain with namedtuple is that we can't explicitly do assignments, which can be solved by making a new object:



      def move(self) -> None:
      if self.direction == right:

      # Before:
      self.head.x += block_size # AttributeError: can't set attribute

      # After:
      self.head = Point(self.head.x + block_size, self.head.y) # preferable
      # or:
      self.head = self.head._replace(x=self.head.x + block_size) # looks hacky


      You can also use Point as a replacement for Food.position, which isn't being used.



      class Food:
      ...
      position: Point

      def __init__(self, color: Tuple[int, int, int]) -> None:
      self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
      y=random.randint(0, bg_width//block_size - 1)*block_size)

      def spawn(self) -> Point: # more explicit
      if self.state:
      return self.position
      else:
      self.state = True
      self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
      y=random.randint(0, bg_width//block_size - 1)*block_size)
      return self.position


      I think this is especially useful when used together with Cordes's suggestion in his answer, in his Separate game logic from screen rendering details section.



      Another good thing about namedtuple is that we can still do unpacking.



              for x, y in snake.get_body():    # still works
      pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


      Refactor Random-Point Generation



      There are two places where the coordinates of the food are updated with random integers, generated via a special formula. Based on the DRY principle, I suggest refactoring the generation of random points to a single function get_random_point().



      def get_random_point():
      return Point(x=random.randint(0, bg_width//block_size - 1)*block_size
      y=random.randint(0, bg_width//block_size - 1)*block_size)


      Then you can conveniently do self.position = get_random_point() without having to painfully, repetitively type out the above.



      Refactoring Food



      Currently, Food updates its position only when spawn() is called and if state is false. Seems a bit long-winded. Even the name update seems to be untruthful as it makes a delayed update.



      I suggest updating the position immediately when update() is called.



          def spawn(self) -> Tuple[int, int]:
      return self.position

      def update(self):
      self.position = get_random_point()


      Note that Food.state and Food.spawn(self) are redundant now and can be removed. That should be three cheers (less lines of code, yes?).



      On collision(Snake, int, int)



      Logically, this section should come first, but I saved it for the last.




      1. The name collision is ambiguous. Can we improve it? Sure! Note that logically speaking, the function returns a boolean, so it's a predicate. Can we prefix it with is? Certainly! I think is_snake_on_food isn't too bad. You could also go with is_food_reached or is_snake_colliding_with_food.


      2. Having changed how positions are stored in the Food class, we can pass in the food's position directly. Thus the signature of the function can be reduced to:



        def is_snake_on_food(snake: Snake, food_target: Point):


        This also saves us from needing unpack *food_pos in line 209.



        # Before:
        collision_ = collision(snake, *food_pos)

        # After:
        collision_ = is_snake_on_food(snake, food_pos)



      3. There is no need to create pygame.Rect just to compare snake_.head and food_target. Currently, lines 128-131 are



            snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
        food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
        block_size)
        if snake_rect == food_rect:


        Instead, the coordinates can be compared directly:



            if (*snake_.head,) == (food_target_x, food_target_y):


        Having passed in food_target as a Point, we can simplify this to



            if snake_.head == food_target:



      Hope this helps!






      share|improve this answer










      $endgroup$























        1
















        $begingroup$

        Thanks for the game. I applied most changes suggested by @Dair and @vurmux as well as partially the suggestions from @Peter Cordes (some of them are not so simple). After that, there were still some things left:



        Game logic



        I'm not sure whether it was intentional, but the snake does not collide with itself. In usual snake games, if the snake bites into her tail, the game is over as well.



        Swallowed keypresses



        When playing the game, I noticed that I sometimes cannot perform a U-turn when I needed to. It seems that the first keypress got lost.



        The reason is here:



        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
        ...


        which should be changed to



        if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
        snake.change_dir(Direction.RIGHT)
        elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
        snake.change_dir(Direction.LEFT)
        elif event.key == pygame.K_UP or event.key == pygame.K_w:
        snake.change_dir(Direction.UP)
        elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
        snake.change_dir(Direction.DOWN)


        However, this brings us to an interesting other problem: doing 2 changes without moving the snake makes the snake run into the opposite direction immediately (without hitting itself as noted before).



        Since pygame.event.get() consumes all events, it would be up to you, queuing the events yourself and processing them the next frame.



        High CPU usage when game is over



        When the game is over, it uses ~14% CPU time on my machine, which means that 1 core is 100% busy. Adding a pygame.time.delay(60) into the while is_over loop helps.



        Classes in files



        In addition I tried to apply another principle, which is to have each class in a separate file. Doing so is quite simple with an IDE like Pycharm, since it has the move-refactoring. Unfortunately, it doesn't work any more, because of this:



        After the refactoring, Food has



        from game import bg_width, block_size, bg_height


        So one's left with a cycle of imports due to the use of global variables. Looking at the food class for possible resolutions, you could pass the necessary dependencies as parameters:



        def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


        After the refactoring, Snake has



        from game import block_size


        and a similar solution can apply.



        Static members



        Your classes seem to define the properties twice, once in __init__ and once as static members. I don't see any usage of the static members, so these can be removed in Snake:



        head: List[int, int]
        color: Tuple[int, int, int]
        body: List[List[int, int]]
        direction: str
        size: int


        and in Food:



        x: int
        y: int
        color: Tuple[int, int, int]
        state: bool
        position: Tuple[int, int]


        Naming



        Assuming that bg is short for background, I really wonder whether that's the correct term here. The snake is moving on a board, not on a background I'd say. In the comment for that line, you call it screen width and height, which may accidentally be the case as well.



        Considering a future version of the game where you add a nice background graphic and display the score below the board, neither background nor screen would match any more.



        Code duplication



        With the change before, I noticed that there's duplicate code in the Food class. Inside __init__, it basically spawns itself.



        This duplication can be removed and adding



        food = Food(BLUE)
        food.spawn(bg_width, bg_height, block_size)


        It can later be discussed whether or not Food needs to be spawned that early.



        Potential stack overflow in game over mode



        Once the game is over, there's a loop handling the situation:



        while is_over:


        I expected the game to get out of this loop when a new round begins. However, that's not the case. Instead there is



        if event.key == pygame.K_r:
        game()


        This is a recursive call. It's unlikely that it will cause problems in this particular game, but in general, this may cause stack overflows.



        It can be resolved by introducing another loop



        while running:
        while is_over and running:
        ...

        # Initialization code here

        while running and not is_over:
        ...


        Instead of calling game(), you can then set is_over = False.



        Unused variable / unreachable code



        The while running loop can be replaced by a while True, since there's no other assignment to running which would terminate the loop.



        This also means that the code after while running will never be reached:



        pygame.quit()
        quit()


        Changing the exit routine to running = False, you save some duplicate code and the code runs to the end. This is e.g. helpful if you later want to implement saving a highscore list etc. If you have many exit points during your program, it will be harder to implement something at the end of the game.



        You can also omit quit(), because it is not helpful as the last statement of your code.



        Smaller improvements



        food.update() is only called with False as a parameter. It's never called with True. So this argument can be omitted and go hard-coded into the update() method. The code then looks like this:



        while running:
        ...
        food_pos = food.spawn(board_width, board_height, block_size)
        if collision(snake, *food_pos):
        score += 1
        food.update()


        This reads like the food is spawning in a new place with every frame. IMHO it reads better like this:



        while running:
        ...
        food_pos = food.??? # whatever
        if collision(snake, *food_pos):
        score += 1
        food.spawn(board_width, board_height, block_size)


        Because that makes it clear that food only spaws whenever it collided with the snake aka. it was eaten.



        Snake direction change



        Note: @Peter Cordes' vector approach is even more elegant. Perhaps the following might show you a refactoring you can apply in other cases as well when a vector does not fit.



        After applying the enum suggestion, the direction check looks like this



        def change_dir(self, direction: Direction) -> None:
        if self.direction != Direction.LEFT and direction == Direction.RIGHT:
        self.direction = Direction.RIGHT
        elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
        self.direction = Direction.LEFT
        elif self.direction != Direction.DOWN and direction == Direction.UP:
        self.direction = Direction.UP
        elif self.direction != Direction.UP and direction == Direction.DOWN:
        self.direction = Direction.DOWN


        Combining self.direction = Direction.RIGHT and direction == Direction.RIGHT, we can simplify



        self.direction = direction


        This applies to all 4 cases, so we end up with



        def change_dir(self, direction: Direction) -> None:
        if self.direction != Direction.LEFT and direction == Direction.RIGHT:
        self.direction = direction
        elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
        self.direction = direction
        elif self.direction != Direction.DOWN and direction == Direction.UP:
        self.direction = direction
        elif self.direction != Direction.UP and direction == Direction.DOWN:
        self.direction = direction


        Now, we can argue that this is duplicate code and remove the duplication:



        def change_dir(self, direction: Direction) -> None:
        if (self.direction != Direction.LEFT and direction == Direction.RIGHT) or
        (self.direction != Direction.RIGHT and direction == Direction.LEFT) or
        (self.direction != Direction.DOWN and direction == Direction.UP) or
        (self.direction != Direction.UP and direction == Direction.DOWN):
        self.direction = direction


        Personally, I'd even prefer



        def change_dir(self, direction: Direction) -> None:
        if self.is_opposite_direction(direction, self.direction):
        return
        self.direction = direction





        share|improve this answer












        $endgroup$

















          Your Answer






          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/4.0/"u003ecc by-sa 4.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });















          draft saved

          draft discarded
















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221163%2fbeginners-snake-game-using-pygame%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown


























          5 Answers
          5






          active

          oldest

          votes








          5 Answers
          5






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          43
















          $begingroup$

          PEP 8



          PEP 8 recommends that constants be written as "all capital letters with underscores separating words.". So something like:



          yellow = (255, 255, 0)


          Should be:



          YELLOW = (255, 255, 0)


          or possibly an enum (more on this later)



          Inconsistent quotations



          Usually a project will stick with either "" or '' unless you have particular reason not to.1 But for instance:



          pygame.display.set_caption("Snake")
          font = pygame.font.SysFont('Times New Roman', 20)
          text_colour = pygame.Color('White')


          violates the uniformity unnecessarily.



          Enums



          To quote the docs on enum:




          An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.




          So for instance:



          left = "LEFT"
          right = "RIGHT"
          up = "UP"
          down = "DOWN"


          becomes:



          from enum import Enum
          class Direction(Enum):
          LEFT = "LEFT"
          RIGHT = "RIGHT"
          DOWN = "DOWN"
          UP = "UP"


          See here for more reasons why you should port your stringy code to enums. In addition, there are probably some integer arithmetic tricks you could use to eliminate a lot of the if-elif-else chains. (You will need to do some further refactoring to get this to run without error.)



          Unnecessary Comments



          Consider:



          # initialize food and snake
          food = Food(blue)
          snake = Snake(green)

          # initialize loop logic
          running = True
          is_over = False

          # initialize score
          score = 0

          # game loop
          while running:


          I, personally, would omit all the comments. I could see where a case can be made the game loop comments in the case that someone is trying to understand the code without knowledge of game loops, but I would argue the concept of a game loop is so ubiquitous that those comments get in the way.



          If you had to write comments I would instead write:



          # Initialize entities/data for new game
          food = Food(blue)
          snake = Snake(green)
          score = 0

          # Init/start game loop
          running = True
          is_over = False
          while running:
          ...


          Even then, I'm not quite satisfied with it, but the moral it to not add redundant comments. However, there may be some debate on this.



          The best example is probably:



              # move snake
          snake.move()


          Really think about what this comment contributes.



          Unnecessary assignments



          I'm not sure why you wrote:



              # check for collision with wall:
          collision_with_wall = wall_collision(snake)
          if collision_with_wall:
          is_over = True


          When:



              if wall_collision(snake):
          is_over = True


          suffices.



          Add a main function



          You should consider adding a main function to your project. This is usually done through the introduction of:



          if __name__ == "__main__":
          game()


          This allows for me to import your project and not have it automatically execute the game function at the bottom. Increasing re-usability.



          1 For instance, It may be the project defaults to "", but you need to print out " Hi " with the quotes, so print('" Hi "') may be chosen.






          share|improve this answer












          $endgroup$















          • $begingroup$
            Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
            $endgroup$
            – BruceWayne
            May 28 at 14:27








          • 1




            $begingroup$
            In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
            $endgroup$
            – vakus
            May 29 at 9:53






          • 2




            $begingroup$
            @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
            $endgroup$
            – Milliarde
            May 29 at 13:01
















          43
















          $begingroup$

          PEP 8



          PEP 8 recommends that constants be written as "all capital letters with underscores separating words.". So something like:



          yellow = (255, 255, 0)


          Should be:



          YELLOW = (255, 255, 0)


          or possibly an enum (more on this later)



          Inconsistent quotations



          Usually a project will stick with either "" or '' unless you have particular reason not to.1 But for instance:



          pygame.display.set_caption("Snake")
          font = pygame.font.SysFont('Times New Roman', 20)
          text_colour = pygame.Color('White')


          violates the uniformity unnecessarily.



          Enums



          To quote the docs on enum:




          An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.




          So for instance:



          left = "LEFT"
          right = "RIGHT"
          up = "UP"
          down = "DOWN"


          becomes:



          from enum import Enum
          class Direction(Enum):
          LEFT = "LEFT"
          RIGHT = "RIGHT"
          DOWN = "DOWN"
          UP = "UP"


          See here for more reasons why you should port your stringy code to enums. In addition, there are probably some integer arithmetic tricks you could use to eliminate a lot of the if-elif-else chains. (You will need to do some further refactoring to get this to run without error.)



          Unnecessary Comments



          Consider:



          # initialize food and snake
          food = Food(blue)
          snake = Snake(green)

          # initialize loop logic
          running = True
          is_over = False

          # initialize score
          score = 0

          # game loop
          while running:


          I, personally, would omit all the comments. I could see where a case can be made the game loop comments in the case that someone is trying to understand the code without knowledge of game loops, but I would argue the concept of a game loop is so ubiquitous that those comments get in the way.



          If you had to write comments I would instead write:



          # Initialize entities/data for new game
          food = Food(blue)
          snake = Snake(green)
          score = 0

          # Init/start game loop
          running = True
          is_over = False
          while running:
          ...


          Even then, I'm not quite satisfied with it, but the moral it to not add redundant comments. However, there may be some debate on this.



          The best example is probably:



              # move snake
          snake.move()


          Really think about what this comment contributes.



          Unnecessary assignments



          I'm not sure why you wrote:



              # check for collision with wall:
          collision_with_wall = wall_collision(snake)
          if collision_with_wall:
          is_over = True


          When:



              if wall_collision(snake):
          is_over = True


          suffices.



          Add a main function



          You should consider adding a main function to your project. This is usually done through the introduction of:



          if __name__ == "__main__":
          game()


          This allows for me to import your project and not have it automatically execute the game function at the bottom. Increasing re-usability.



          1 For instance, It may be the project defaults to "", but you need to print out " Hi " with the quotes, so print('" Hi "') may be chosen.






          share|improve this answer












          $endgroup$















          • $begingroup$
            Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
            $endgroup$
            – BruceWayne
            May 28 at 14:27








          • 1




            $begingroup$
            In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
            $endgroup$
            – vakus
            May 29 at 9:53






          • 2




            $begingroup$
            @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
            $endgroup$
            – Milliarde
            May 29 at 13:01














          43














          43










          43







          $begingroup$

          PEP 8



          PEP 8 recommends that constants be written as "all capital letters with underscores separating words.". So something like:



          yellow = (255, 255, 0)


          Should be:



          YELLOW = (255, 255, 0)


          or possibly an enum (more on this later)



          Inconsistent quotations



          Usually a project will stick with either "" or '' unless you have particular reason not to.1 But for instance:



          pygame.display.set_caption("Snake")
          font = pygame.font.SysFont('Times New Roman', 20)
          text_colour = pygame.Color('White')


          violates the uniformity unnecessarily.



          Enums



          To quote the docs on enum:




          An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.




          So for instance:



          left = "LEFT"
          right = "RIGHT"
          up = "UP"
          down = "DOWN"


          becomes:



          from enum import Enum
          class Direction(Enum):
          LEFT = "LEFT"
          RIGHT = "RIGHT"
          DOWN = "DOWN"
          UP = "UP"


          See here for more reasons why you should port your stringy code to enums. In addition, there are probably some integer arithmetic tricks you could use to eliminate a lot of the if-elif-else chains. (You will need to do some further refactoring to get this to run without error.)



          Unnecessary Comments



          Consider:



          # initialize food and snake
          food = Food(blue)
          snake = Snake(green)

          # initialize loop logic
          running = True
          is_over = False

          # initialize score
          score = 0

          # game loop
          while running:


          I, personally, would omit all the comments. I could see where a case can be made the game loop comments in the case that someone is trying to understand the code without knowledge of game loops, but I would argue the concept of a game loop is so ubiquitous that those comments get in the way.



          If you had to write comments I would instead write:



          # Initialize entities/data for new game
          food = Food(blue)
          snake = Snake(green)
          score = 0

          # Init/start game loop
          running = True
          is_over = False
          while running:
          ...


          Even then, I'm not quite satisfied with it, but the moral it to not add redundant comments. However, there may be some debate on this.



          The best example is probably:



              # move snake
          snake.move()


          Really think about what this comment contributes.



          Unnecessary assignments



          I'm not sure why you wrote:



              # check for collision with wall:
          collision_with_wall = wall_collision(snake)
          if collision_with_wall:
          is_over = True


          When:



              if wall_collision(snake):
          is_over = True


          suffices.



          Add a main function



          You should consider adding a main function to your project. This is usually done through the introduction of:



          if __name__ == "__main__":
          game()


          This allows for me to import your project and not have it automatically execute the game function at the bottom. Increasing re-usability.



          1 For instance, It may be the project defaults to "", but you need to print out " Hi " with the quotes, so print('" Hi "') may be chosen.






          share|improve this answer












          $endgroup$



          PEP 8



          PEP 8 recommends that constants be written as "all capital letters with underscores separating words.". So something like:



          yellow = (255, 255, 0)


          Should be:



          YELLOW = (255, 255, 0)


          or possibly an enum (more on this later)



          Inconsistent quotations



          Usually a project will stick with either "" or '' unless you have particular reason not to.1 But for instance:



          pygame.display.set_caption("Snake")
          font = pygame.font.SysFont('Times New Roman', 20)
          text_colour = pygame.Color('White')


          violates the uniformity unnecessarily.



          Enums



          To quote the docs on enum:




          An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.




          So for instance:



          left = "LEFT"
          right = "RIGHT"
          up = "UP"
          down = "DOWN"


          becomes:



          from enum import Enum
          class Direction(Enum):
          LEFT = "LEFT"
          RIGHT = "RIGHT"
          DOWN = "DOWN"
          UP = "UP"


          See here for more reasons why you should port your stringy code to enums. In addition, there are probably some integer arithmetic tricks you could use to eliminate a lot of the if-elif-else chains. (You will need to do some further refactoring to get this to run without error.)



          Unnecessary Comments



          Consider:



          # initialize food and snake
          food = Food(blue)
          snake = Snake(green)

          # initialize loop logic
          running = True
          is_over = False

          # initialize score
          score = 0

          # game loop
          while running:


          I, personally, would omit all the comments. I could see where a case can be made the game loop comments in the case that someone is trying to understand the code without knowledge of game loops, but I would argue the concept of a game loop is so ubiquitous that those comments get in the way.



          If you had to write comments I would instead write:



          # Initialize entities/data for new game
          food = Food(blue)
          snake = Snake(green)
          score = 0

          # Init/start game loop
          running = True
          is_over = False
          while running:
          ...


          Even then, I'm not quite satisfied with it, but the moral it to not add redundant comments. However, there may be some debate on this.



          The best example is probably:



              # move snake
          snake.move()


          Really think about what this comment contributes.



          Unnecessary assignments



          I'm not sure why you wrote:



              # check for collision with wall:
          collision_with_wall = wall_collision(snake)
          if collision_with_wall:
          is_over = True


          When:



              if wall_collision(snake):
          is_over = True


          suffices.



          Add a main function



          You should consider adding a main function to your project. This is usually done through the introduction of:



          if __name__ == "__main__":
          game()


          This allows for me to import your project and not have it automatically execute the game function at the bottom. Increasing re-usability.



          1 For instance, It may be the project defaults to "", but you need to print out " Hi " with the quotes, so print('" Hi "') may be chosen.







          share|improve this answer















          share|improve this answer




          share|improve this answer



          share|improve this answer








          edited May 28 at 13:56

























          answered May 28 at 4:17









          DairDair

          5,0691 gold badge13 silver badges33 bronze badges




          5,0691 gold badge13 silver badges33 bronze badges















          • $begingroup$
            Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
            $endgroup$
            – BruceWayne
            May 28 at 14:27








          • 1




            $begingroup$
            In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
            $endgroup$
            – vakus
            May 29 at 9:53






          • 2




            $begingroup$
            @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
            $endgroup$
            – Milliarde
            May 29 at 13:01


















          • $begingroup$
            Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
            $endgroup$
            – BruceWayne
            May 28 at 14:27








          • 1




            $begingroup$
            In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
            $endgroup$
            – vakus
            May 29 at 9:53






          • 2




            $begingroup$
            @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
            $endgroup$
            – Milliarde
            May 29 at 13:01
















          $begingroup$
          Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
          $endgroup$
          – BruceWayne
          May 28 at 14:27






          $begingroup$
          Nice answer! +1 also for the excellent summary of the main function. That thread you linked to is a great help, but your quick one sentence summary is one of the best TL;DR's I've seen of that.
          $endgroup$
          – BruceWayne
          May 28 at 14:27






          1




          1




          $begingroup$
          In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
          $endgroup$
          – vakus
          May 29 at 9:53




          $begingroup$
          In section "Unnecessary assignments" is there reason we would want to do if wall_collision(snake): is_over = True instead of is_over = wall_collision(snake)?
          $endgroup$
          – vakus
          May 29 at 9:53




          2




          2




          $begingroup$
          @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
          $endgroup$
          – Milliarde
          May 29 at 13:01




          $begingroup$
          @vakus, those are two different things. Perhaps there are many possible checks above that can also set is_over to True, in which case we don't want to set it back to False which your suggested code would do.
          $endgroup$
          – Milliarde
          May 29 at 13:01













          22
















          $begingroup$

          In addition to another answer I will add these places to improve (I will not repeat all things from that answer, my answer is just an addition):



          1. Code inconsistency



          You have several places in your code where your code is inconsistent



          def collision():
          ...
          return 1
          return 0


          def wall_collision():
          ...
          return True
          return False


          Both functions are checking collisions but one returns integer (0/1) and another - boolean (True/False).



          self.head = [int(10*block_size), int(5*block_size)]
          self.body = [self.head, [9*block_size, 5*block_size]]


          For head you convert X*block_size to int (it is unnecessary anyway). For body you don't do it.



          I recommend you to always check your code for possible inconsistency.



          2. Names inconsistency



          I moved it to an another point because it is not about how your code works - it is about how another programmers will read your code.



          Look at this two lines:



          def change_dir(self, direc: str) -> None:
          if self.direction != left and direc == right:


          You use three different namings for direction entity:




          • dir

          • direct

          • direction


          It is far better to use consistent variable names. If you are using different direction variables, get them names with only one word for directions. Here is the example:




          • snake_direction

          • pokemon_direction

          • some_strange_direction


          or:




          • enemy_dir

          • arrow_dir

          • some_another_strange_dir


          3. Small code improvements



          Sometimes you look at your code and think: "Hmmm, it is working but looks not good". Often in these cases you can slightly reorganize your code to make it a bit better :)



          for event in pygame.event.get():
          pressed_key = pygame.key.get_pressed()
          if event.type == pygame.QUIT:
          pygame.quit()
          sys.exit()
          if pressed_key[pygame.K_q]:
          pygame.quit()
          sys.exit()


          can be shortened to:



          for event in pygame.event.get():
          pressed_key = pygame.key.get_pressed()
          if event.type == pygame.QUIT or pressed_key[pygame.K_q]:
          pygame.quit()
          sys.exit()


          This line of code is correct too but it is hard to read:



          if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
          or (s.head[1] > bg_height-block_size):


          But it will be far better to read if you will change it to:



          if (s.head[0] < 0 or
          s.head[0] > bg_width-block_size or
          s.head[1] < 0 or
          s.head[1] > bg_height-block_size):


          or even this (sometimes I use it for really long if's):



          is_wall_collision = any([
          s.head[0] < 0,
          s.head[0] > bg_width-block_size,
          s.head[1] < 0,
          s.head[1] > bg_height-block_size
          ])
          if is_wall_collision:





          share|improve this answer










          $endgroup$




















            22
















            $begingroup$

            In addition to another answer I will add these places to improve (I will not repeat all things from that answer, my answer is just an addition):



            1. Code inconsistency



            You have several places in your code where your code is inconsistent



            def collision():
            ...
            return 1
            return 0


            def wall_collision():
            ...
            return True
            return False


            Both functions are checking collisions but one returns integer (0/1) and another - boolean (True/False).



            self.head = [int(10*block_size), int(5*block_size)]
            self.body = [self.head, [9*block_size, 5*block_size]]


            For head you convert X*block_size to int (it is unnecessary anyway). For body you don't do it.



            I recommend you to always check your code for possible inconsistency.



            2. Names inconsistency



            I moved it to an another point because it is not about how your code works - it is about how another programmers will read your code.



            Look at this two lines:



            def change_dir(self, direc: str) -> None:
            if self.direction != left and direc == right:


            You use three different namings for direction entity:




            • dir

            • direct

            • direction


            It is far better to use consistent variable names. If you are using different direction variables, get them names with only one word for directions. Here is the example:




            • snake_direction

            • pokemon_direction

            • some_strange_direction


            or:




            • enemy_dir

            • arrow_dir

            • some_another_strange_dir


            3. Small code improvements



            Sometimes you look at your code and think: "Hmmm, it is working but looks not good". Often in these cases you can slightly reorganize your code to make it a bit better :)



            for event in pygame.event.get():
            pressed_key = pygame.key.get_pressed()
            if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
            if pressed_key[pygame.K_q]:
            pygame.quit()
            sys.exit()


            can be shortened to:



            for event in pygame.event.get():
            pressed_key = pygame.key.get_pressed()
            if event.type == pygame.QUIT or pressed_key[pygame.K_q]:
            pygame.quit()
            sys.exit()


            This line of code is correct too but it is hard to read:



            if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
            or (s.head[1] > bg_height-block_size):


            But it will be far better to read if you will change it to:



            if (s.head[0] < 0 or
            s.head[0] > bg_width-block_size or
            s.head[1] < 0 or
            s.head[1] > bg_height-block_size):


            or even this (sometimes I use it for really long if's):



            is_wall_collision = any([
            s.head[0] < 0,
            s.head[0] > bg_width-block_size,
            s.head[1] < 0,
            s.head[1] > bg_height-block_size
            ])
            if is_wall_collision:





            share|improve this answer










            $endgroup$


















              22














              22










              22







              $begingroup$

              In addition to another answer I will add these places to improve (I will not repeat all things from that answer, my answer is just an addition):



              1. Code inconsistency



              You have several places in your code where your code is inconsistent



              def collision():
              ...
              return 1
              return 0


              def wall_collision():
              ...
              return True
              return False


              Both functions are checking collisions but one returns integer (0/1) and another - boolean (True/False).



              self.head = [int(10*block_size), int(5*block_size)]
              self.body = [self.head, [9*block_size, 5*block_size]]


              For head you convert X*block_size to int (it is unnecessary anyway). For body you don't do it.



              I recommend you to always check your code for possible inconsistency.



              2. Names inconsistency



              I moved it to an another point because it is not about how your code works - it is about how another programmers will read your code.



              Look at this two lines:



              def change_dir(self, direc: str) -> None:
              if self.direction != left and direc == right:


              You use three different namings for direction entity:




              • dir

              • direct

              • direction


              It is far better to use consistent variable names. If you are using different direction variables, get them names with only one word for directions. Here is the example:




              • snake_direction

              • pokemon_direction

              • some_strange_direction


              or:




              • enemy_dir

              • arrow_dir

              • some_another_strange_dir


              3. Small code improvements



              Sometimes you look at your code and think: "Hmmm, it is working but looks not good". Often in these cases you can slightly reorganize your code to make it a bit better :)



              for event in pygame.event.get():
              pressed_key = pygame.key.get_pressed()
              if event.type == pygame.QUIT:
              pygame.quit()
              sys.exit()
              if pressed_key[pygame.K_q]:
              pygame.quit()
              sys.exit()


              can be shortened to:



              for event in pygame.event.get():
              pressed_key = pygame.key.get_pressed()
              if event.type == pygame.QUIT or pressed_key[pygame.K_q]:
              pygame.quit()
              sys.exit()


              This line of code is correct too but it is hard to read:



              if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
              or (s.head[1] > bg_height-block_size):


              But it will be far better to read if you will change it to:



              if (s.head[0] < 0 or
              s.head[0] > bg_width-block_size or
              s.head[1] < 0 or
              s.head[1] > bg_height-block_size):


              or even this (sometimes I use it for really long if's):



              is_wall_collision = any([
              s.head[0] < 0,
              s.head[0] > bg_width-block_size,
              s.head[1] < 0,
              s.head[1] > bg_height-block_size
              ])
              if is_wall_collision:





              share|improve this answer










              $endgroup$



              In addition to another answer I will add these places to improve (I will not repeat all things from that answer, my answer is just an addition):



              1. Code inconsistency



              You have several places in your code where your code is inconsistent



              def collision():
              ...
              return 1
              return 0


              def wall_collision():
              ...
              return True
              return False


              Both functions are checking collisions but one returns integer (0/1) and another - boolean (True/False).



              self.head = [int(10*block_size), int(5*block_size)]
              self.body = [self.head, [9*block_size, 5*block_size]]


              For head you convert X*block_size to int (it is unnecessary anyway). For body you don't do it.



              I recommend you to always check your code for possible inconsistency.



              2. Names inconsistency



              I moved it to an another point because it is not about how your code works - it is about how another programmers will read your code.



              Look at this two lines:



              def change_dir(self, direc: str) -> None:
              if self.direction != left and direc == right:


              You use three different namings for direction entity:




              • dir

              • direct

              • direction


              It is far better to use consistent variable names. If you are using different direction variables, get them names with only one word for directions. Here is the example:




              • snake_direction

              • pokemon_direction

              • some_strange_direction


              or:




              • enemy_dir

              • arrow_dir

              • some_another_strange_dir


              3. Small code improvements



              Sometimes you look at your code and think: "Hmmm, it is working but looks not good". Often in these cases you can slightly reorganize your code to make it a bit better :)



              for event in pygame.event.get():
              pressed_key = pygame.key.get_pressed()
              if event.type == pygame.QUIT:
              pygame.quit()
              sys.exit()
              if pressed_key[pygame.K_q]:
              pygame.quit()
              sys.exit()


              can be shortened to:



              for event in pygame.event.get():
              pressed_key = pygame.key.get_pressed()
              if event.type == pygame.QUIT or pressed_key[pygame.K_q]:
              pygame.quit()
              sys.exit()


              This line of code is correct too but it is hard to read:



              if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
              or (s.head[1] > bg_height-block_size):


              But it will be far better to read if you will change it to:



              if (s.head[0] < 0 or
              s.head[0] > bg_width-block_size or
              s.head[1] < 0 or
              s.head[1] > bg_height-block_size):


              or even this (sometimes I use it for really long if's):



              is_wall_collision = any([
              s.head[0] < 0,
              s.head[0] > bg_width-block_size,
              s.head[1] < 0,
              s.head[1] > bg_height-block_size
              ])
              if is_wall_collision:






              share|improve this answer













              share|improve this answer




              share|improve this answer



              share|improve this answer










              answered May 28 at 10:15









              vurmuxvurmux

              1,2553 silver badges15 bronze badges




              1,2553 silver badges15 bronze badges


























                  10
















                  $begingroup$

                      size: int
                  ...

                  self.size = 2


                  You never read size, and Python Lists already know their own length in case you did need it.



                  You're just duplicating functionality that List already gives you by manually keeping size in sync with the list length.





                  Separate game logic from screen rendering details



                  You keep snake coordinates scaled by pixels. This seems to complicate your code everywhere with block_size instead of 1; I suspect it would be easier to keep snake coordinates in units of blocks, and only scale to pixels for drawing purposes.



                  e.g. then your outer-wall check could be something like s.head[0] >= bg_width instead of
                  s.head[0] > bg_width-block_size.



                  1 is special for integers because it's the difference between > and >=





                  I like your add_to_tail idea of putting multiple snake segments in the same place initially, so move on future turns actually makes the snake longer on screen. That high-level design might be worthy of a comment.



                  The other option would be a "future growth" counter that you check (and decrement) every move to decide whether to remove the last tail block. (That's what I was expecting because I hadn't thought of your idea). Your way is more efficient and will still work if you want food to cause multiple segments of growth. You can still do them all when the food is eaten.





                  Keeping a separate head: I'm not sure this is helping you. Functions that want to look at the first segment can do head = snake_.body[0].



                  I don't understand why move needs to do this:



                         if self.body[0] != self.head:
                  self.head = self.body[0]


                  when it looks to me like inserting the modified head as a new first element will already create that condition. So either this is redundant (and confusing), or else it's extra work that you wouldn't need to do if you didn't redundantly keep the head coordinates in 2 places. (Front of the List, and in its own object.)





                      direction: str


                  This should be an enum or integer, not a string. String compares are fairly cheap, but not as cheap as integers. There's no reason that a direction needs to be an arbitrary string when it can take only 1 of 4 possible values. A string isn't making your code any easier to read.



                  You probably still want the if/elif chain to turn a direction into an x or y offset, but another option for an integer would be a lookup table of [-1, 0] , [1, 0] etc. so you look up the x and y offsets for the given direction and just add them to the head's x and y coordinates without any conditionals.



                  Or direction could actually be an XY vector, removing the lookup step.



                      head[0] += direction[0]
                  head[1] += direction[1]


                  But that might complicate change_dir. Or give you a different implementation: to check for reversing direction: if new_dir + old_dir == [0,0] then you tried to double back. Otherwise you can set dir = new_dir.



                  I like your design for having the keyboard-input just call change_dir to check the game-logic of that input. That works very cleanly, and would still work if left was defined as [-1, 0] instead of "LEFT".






                  share|improve this answer










                  $endgroup$




















                    10
















                    $begingroup$

                        size: int
                    ...

                    self.size = 2


                    You never read size, and Python Lists already know their own length in case you did need it.



                    You're just duplicating functionality that List already gives you by manually keeping size in sync with the list length.





                    Separate game logic from screen rendering details



                    You keep snake coordinates scaled by pixels. This seems to complicate your code everywhere with block_size instead of 1; I suspect it would be easier to keep snake coordinates in units of blocks, and only scale to pixels for drawing purposes.



                    e.g. then your outer-wall check could be something like s.head[0] >= bg_width instead of
                    s.head[0] > bg_width-block_size.



                    1 is special for integers because it's the difference between > and >=





                    I like your add_to_tail idea of putting multiple snake segments in the same place initially, so move on future turns actually makes the snake longer on screen. That high-level design might be worthy of a comment.



                    The other option would be a "future growth" counter that you check (and decrement) every move to decide whether to remove the last tail block. (That's what I was expecting because I hadn't thought of your idea). Your way is more efficient and will still work if you want food to cause multiple segments of growth. You can still do them all when the food is eaten.





                    Keeping a separate head: I'm not sure this is helping you. Functions that want to look at the first segment can do head = snake_.body[0].



                    I don't understand why move needs to do this:



                           if self.body[0] != self.head:
                    self.head = self.body[0]


                    when it looks to me like inserting the modified head as a new first element will already create that condition. So either this is redundant (and confusing), or else it's extra work that you wouldn't need to do if you didn't redundantly keep the head coordinates in 2 places. (Front of the List, and in its own object.)





                        direction: str


                    This should be an enum or integer, not a string. String compares are fairly cheap, but not as cheap as integers. There's no reason that a direction needs to be an arbitrary string when it can take only 1 of 4 possible values. A string isn't making your code any easier to read.



                    You probably still want the if/elif chain to turn a direction into an x or y offset, but another option for an integer would be a lookup table of [-1, 0] , [1, 0] etc. so you look up the x and y offsets for the given direction and just add them to the head's x and y coordinates without any conditionals.



                    Or direction could actually be an XY vector, removing the lookup step.



                        head[0] += direction[0]
                    head[1] += direction[1]


                    But that might complicate change_dir. Or give you a different implementation: to check for reversing direction: if new_dir + old_dir == [0,0] then you tried to double back. Otherwise you can set dir = new_dir.



                    I like your design for having the keyboard-input just call change_dir to check the game-logic of that input. That works very cleanly, and would still work if left was defined as [-1, 0] instead of "LEFT".






                    share|improve this answer










                    $endgroup$


















                      10














                      10










                      10







                      $begingroup$

                          size: int
                      ...

                      self.size = 2


                      You never read size, and Python Lists already know their own length in case you did need it.



                      You're just duplicating functionality that List already gives you by manually keeping size in sync with the list length.





                      Separate game logic from screen rendering details



                      You keep snake coordinates scaled by pixels. This seems to complicate your code everywhere with block_size instead of 1; I suspect it would be easier to keep snake coordinates in units of blocks, and only scale to pixels for drawing purposes.



                      e.g. then your outer-wall check could be something like s.head[0] >= bg_width instead of
                      s.head[0] > bg_width-block_size.



                      1 is special for integers because it's the difference between > and >=





                      I like your add_to_tail idea of putting multiple snake segments in the same place initially, so move on future turns actually makes the snake longer on screen. That high-level design might be worthy of a comment.



                      The other option would be a "future growth" counter that you check (and decrement) every move to decide whether to remove the last tail block. (That's what I was expecting because I hadn't thought of your idea). Your way is more efficient and will still work if you want food to cause multiple segments of growth. You can still do them all when the food is eaten.





                      Keeping a separate head: I'm not sure this is helping you. Functions that want to look at the first segment can do head = snake_.body[0].



                      I don't understand why move needs to do this:



                             if self.body[0] != self.head:
                      self.head = self.body[0]


                      when it looks to me like inserting the modified head as a new first element will already create that condition. So either this is redundant (and confusing), or else it's extra work that you wouldn't need to do if you didn't redundantly keep the head coordinates in 2 places. (Front of the List, and in its own object.)





                          direction: str


                      This should be an enum or integer, not a string. String compares are fairly cheap, but not as cheap as integers. There's no reason that a direction needs to be an arbitrary string when it can take only 1 of 4 possible values. A string isn't making your code any easier to read.



                      You probably still want the if/elif chain to turn a direction into an x or y offset, but another option for an integer would be a lookup table of [-1, 0] , [1, 0] etc. so you look up the x and y offsets for the given direction and just add them to the head's x and y coordinates without any conditionals.



                      Or direction could actually be an XY vector, removing the lookup step.



                          head[0] += direction[0]
                      head[1] += direction[1]


                      But that might complicate change_dir. Or give you a different implementation: to check for reversing direction: if new_dir + old_dir == [0,0] then you tried to double back. Otherwise you can set dir = new_dir.



                      I like your design for having the keyboard-input just call change_dir to check the game-logic of that input. That works very cleanly, and would still work if left was defined as [-1, 0] instead of "LEFT".






                      share|improve this answer










                      $endgroup$



                          size: int
                      ...

                      self.size = 2


                      You never read size, and Python Lists already know their own length in case you did need it.



                      You're just duplicating functionality that List already gives you by manually keeping size in sync with the list length.





                      Separate game logic from screen rendering details



                      You keep snake coordinates scaled by pixels. This seems to complicate your code everywhere with block_size instead of 1; I suspect it would be easier to keep snake coordinates in units of blocks, and only scale to pixels for drawing purposes.



                      e.g. then your outer-wall check could be something like s.head[0] >= bg_width instead of
                      s.head[0] > bg_width-block_size.



                      1 is special for integers because it's the difference between > and >=





                      I like your add_to_tail idea of putting multiple snake segments in the same place initially, so move on future turns actually makes the snake longer on screen. That high-level design might be worthy of a comment.



                      The other option would be a "future growth" counter that you check (and decrement) every move to decide whether to remove the last tail block. (That's what I was expecting because I hadn't thought of your idea). Your way is more efficient and will still work if you want food to cause multiple segments of growth. You can still do them all when the food is eaten.





                      Keeping a separate head: I'm not sure this is helping you. Functions that want to look at the first segment can do head = snake_.body[0].



                      I don't understand why move needs to do this:



                             if self.body[0] != self.head:
                      self.head = self.body[0]


                      when it looks to me like inserting the modified head as a new first element will already create that condition. So either this is redundant (and confusing), or else it's extra work that you wouldn't need to do if you didn't redundantly keep the head coordinates in 2 places. (Front of the List, and in its own object.)





                          direction: str


                      This should be an enum or integer, not a string. String compares are fairly cheap, but not as cheap as integers. There's no reason that a direction needs to be an arbitrary string when it can take only 1 of 4 possible values. A string isn't making your code any easier to read.



                      You probably still want the if/elif chain to turn a direction into an x or y offset, but another option for an integer would be a lookup table of [-1, 0] , [1, 0] etc. so you look up the x and y offsets for the given direction and just add them to the head's x and y coordinates without any conditionals.



                      Or direction could actually be an XY vector, removing the lookup step.



                          head[0] += direction[0]
                      head[1] += direction[1]


                      But that might complicate change_dir. Or give you a different implementation: to check for reversing direction: if new_dir + old_dir == [0,0] then you tried to double back. Otherwise you can set dir = new_dir.



                      I like your design for having the keyboard-input just call change_dir to check the game-logic of that input. That works very cleanly, and would still work if left was defined as [-1, 0] instead of "LEFT".







                      share|improve this answer













                      share|improve this answer




                      share|improve this answer



                      share|improve this answer










                      answered May 28 at 20:21









                      Peter CordesPeter Cordes

                      1,7877 silver badges17 bronze badges




                      1,7877 silver badges17 bronze badges


























                          3
















                          $begingroup$

                          Good going on your first game. I completely agree with most of the points made by others. Here are a few more nitpicks. This answer is a bit long-winded, but take it as a complement. :-)



                          Unpack the Coordinates



                          This is what lines 195-197 currently look like:



                                  for item in snake.get_body():
                          pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                          block_size])


                          Instead of relying on indexing, we can directly unpack item into its respective xy-coordinates. This makes for good brevity and readability.



                                  for x, y in snake.get_body():
                          pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                          The same can be done for lines 208-214.



                          # Before:
                          food_pos = food.spawn()
                          collision_ = collision(snake, *food_pos) # 🤔
                          if collision_ == 1:
                          score += 1
                          food.update(False)
                          pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1], # 🤔
                          block_size, block_size])


                          # After:
                          food_x, food_y = food.spawn()
                          # the rest is left as an exercise for the reader


                          On wall_collision(Snake)



                          Here's what the wall_collision function currently looks like (lines 137-141):



                          def wall_collision(s: Snake) -> bool:
                          if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
                          or (s.head[1] > bg_height-block_size):
                          return True
                          return False



                          1. Being a predicate (i.e. a function which returns a boolean), the function can be better named. You can always find a way to name a predicate such that it is prefixed with is or has. Instead of wall_collision, I'd go with is_colliding_with_wall or collides_with_wall to communicate the intention better.

                          2. It could be simplified. vurmux's answer suggests a few possible ways. However, there exists an easier (and more readable) way of writing it in one line:


                              return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


                          I intentionally added the extraneous parentheses to group comparisons.



                          This is made possible by Python's comparison "chaining" along with De Morgan's Laws. For fun, you can try to prove that the two snippets are equivalent.



                          Points and Coordinates



                          It took a while for me to figure out what the following line meant:



                          self.head = [int(10*block_size), int(5*block_size)]


                          I'm guessing this is a point/coordinate? Looks like it.



                          Lists are so prevalent in Python, that they can hold a multitude of meanings. What's more, they are variable-length constructs, which communicates something else: you might be changing the length of the object in the future. This might mean appending, erasing, or inserting into the list.



                          self.body uses a list appropriately, since it will be appended to and popped from in various places. On the other hand, not once does self.head use list.append, list.pop, or list.insert and this raises a question: does it need to be a list at all? It is better made a tuple, which is a fixed-length construct and immediately communicates to the reader that we won't be modifying the length of self.head.



                          I commend the usage of tuples in Food.spawn() for this very reason. Still, tuples can carry a multitude of different meanings. We can do better by using collections.namedtuple and creating a record-type Point for the purpose of representing xy-coordinates. This can greatly reduce ambiguity and improve readability.



                          from collections import namedtuple
                          Point = namedtuple('Point', ['x', 'y']) # we now have a Point type

                          class Snake:
                          ...
                          def __init__(self, color: Tuple[int, int, int]) -> None:
                          self.head = Point(10*block_size, 5*block_size) # foolproof: self.head is a point
                          self.body = [self.head, Point(...)]
                          ...
                          def add_to_tail(self) -> None:
                          self.body.append(Point(self.body[-1][0], self.body[-1][1]))
                          self.size += 1


                          The only pain with namedtuple is that we can't explicitly do assignments, which can be solved by making a new object:



                          def move(self) -> None:
                          if self.direction == right:

                          # Before:
                          self.head.x += block_size # AttributeError: can't set attribute

                          # After:
                          self.head = Point(self.head.x + block_size, self.head.y) # preferable
                          # or:
                          self.head = self.head._replace(x=self.head.x + block_size) # looks hacky


                          You can also use Point as a replacement for Food.position, which isn't being used.



                          class Food:
                          ...
                          position: Point

                          def __init__(self, color: Tuple[int, int, int]) -> None:
                          self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                          y=random.randint(0, bg_width//block_size - 1)*block_size)

                          def spawn(self) -> Point: # more explicit
                          if self.state:
                          return self.position
                          else:
                          self.state = True
                          self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                          y=random.randint(0, bg_width//block_size - 1)*block_size)
                          return self.position


                          I think this is especially useful when used together with Cordes's suggestion in his answer, in his Separate game logic from screen rendering details section.



                          Another good thing about namedtuple is that we can still do unpacking.



                                  for x, y in snake.get_body():    # still works
                          pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                          Refactor Random-Point Generation



                          There are two places where the coordinates of the food are updated with random integers, generated via a special formula. Based on the DRY principle, I suggest refactoring the generation of random points to a single function get_random_point().



                          def get_random_point():
                          return Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                          y=random.randint(0, bg_width//block_size - 1)*block_size)


                          Then you can conveniently do self.position = get_random_point() without having to painfully, repetitively type out the above.



                          Refactoring Food



                          Currently, Food updates its position only when spawn() is called and if state is false. Seems a bit long-winded. Even the name update seems to be untruthful as it makes a delayed update.



                          I suggest updating the position immediately when update() is called.



                              def spawn(self) -> Tuple[int, int]:
                          return self.position

                          def update(self):
                          self.position = get_random_point()


                          Note that Food.state and Food.spawn(self) are redundant now and can be removed. That should be three cheers (less lines of code, yes?).



                          On collision(Snake, int, int)



                          Logically, this section should come first, but I saved it for the last.




                          1. The name collision is ambiguous. Can we improve it? Sure! Note that logically speaking, the function returns a boolean, so it's a predicate. Can we prefix it with is? Certainly! I think is_snake_on_food isn't too bad. You could also go with is_food_reached or is_snake_colliding_with_food.


                          2. Having changed how positions are stored in the Food class, we can pass in the food's position directly. Thus the signature of the function can be reduced to:



                            def is_snake_on_food(snake: Snake, food_target: Point):


                            This also saves us from needing unpack *food_pos in line 209.



                            # Before:
                            collision_ = collision(snake, *food_pos)

                            # After:
                            collision_ = is_snake_on_food(snake, food_pos)



                          3. There is no need to create pygame.Rect just to compare snake_.head and food_target. Currently, lines 128-131 are



                                snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
                            food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
                            block_size)
                            if snake_rect == food_rect:


                            Instead, the coordinates can be compared directly:



                                if (*snake_.head,) == (food_target_x, food_target_y):


                            Having passed in food_target as a Point, we can simplify this to



                                if snake_.head == food_target:



                          Hope this helps!






                          share|improve this answer










                          $endgroup$




















                            3
















                            $begingroup$

                            Good going on your first game. I completely agree with most of the points made by others. Here are a few more nitpicks. This answer is a bit long-winded, but take it as a complement. :-)



                            Unpack the Coordinates



                            This is what lines 195-197 currently look like:



                                    for item in snake.get_body():
                            pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                            block_size])


                            Instead of relying on indexing, we can directly unpack item into its respective xy-coordinates. This makes for good brevity and readability.



                                    for x, y in snake.get_body():
                            pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                            The same can be done for lines 208-214.



                            # Before:
                            food_pos = food.spawn()
                            collision_ = collision(snake, *food_pos) # 🤔
                            if collision_ == 1:
                            score += 1
                            food.update(False)
                            pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1], # 🤔
                            block_size, block_size])


                            # After:
                            food_x, food_y = food.spawn()
                            # the rest is left as an exercise for the reader


                            On wall_collision(Snake)



                            Here's what the wall_collision function currently looks like (lines 137-141):



                            def wall_collision(s: Snake) -> bool:
                            if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
                            or (s.head[1] > bg_height-block_size):
                            return True
                            return False



                            1. Being a predicate (i.e. a function which returns a boolean), the function can be better named. You can always find a way to name a predicate such that it is prefixed with is or has. Instead of wall_collision, I'd go with is_colliding_with_wall or collides_with_wall to communicate the intention better.

                            2. It could be simplified. vurmux's answer suggests a few possible ways. However, there exists an easier (and more readable) way of writing it in one line:


                                return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


                            I intentionally added the extraneous parentheses to group comparisons.



                            This is made possible by Python's comparison "chaining" along with De Morgan's Laws. For fun, you can try to prove that the two snippets are equivalent.



                            Points and Coordinates



                            It took a while for me to figure out what the following line meant:



                            self.head = [int(10*block_size), int(5*block_size)]


                            I'm guessing this is a point/coordinate? Looks like it.



                            Lists are so prevalent in Python, that they can hold a multitude of meanings. What's more, they are variable-length constructs, which communicates something else: you might be changing the length of the object in the future. This might mean appending, erasing, or inserting into the list.



                            self.body uses a list appropriately, since it will be appended to and popped from in various places. On the other hand, not once does self.head use list.append, list.pop, or list.insert and this raises a question: does it need to be a list at all? It is better made a tuple, which is a fixed-length construct and immediately communicates to the reader that we won't be modifying the length of self.head.



                            I commend the usage of tuples in Food.spawn() for this very reason. Still, tuples can carry a multitude of different meanings. We can do better by using collections.namedtuple and creating a record-type Point for the purpose of representing xy-coordinates. This can greatly reduce ambiguity and improve readability.



                            from collections import namedtuple
                            Point = namedtuple('Point', ['x', 'y']) # we now have a Point type

                            class Snake:
                            ...
                            def __init__(self, color: Tuple[int, int, int]) -> None:
                            self.head = Point(10*block_size, 5*block_size) # foolproof: self.head is a point
                            self.body = [self.head, Point(...)]
                            ...
                            def add_to_tail(self) -> None:
                            self.body.append(Point(self.body[-1][0], self.body[-1][1]))
                            self.size += 1


                            The only pain with namedtuple is that we can't explicitly do assignments, which can be solved by making a new object:



                            def move(self) -> None:
                            if self.direction == right:

                            # Before:
                            self.head.x += block_size # AttributeError: can't set attribute

                            # After:
                            self.head = Point(self.head.x + block_size, self.head.y) # preferable
                            # or:
                            self.head = self.head._replace(x=self.head.x + block_size) # looks hacky


                            You can also use Point as a replacement for Food.position, which isn't being used.



                            class Food:
                            ...
                            position: Point

                            def __init__(self, color: Tuple[int, int, int]) -> None:
                            self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                            y=random.randint(0, bg_width//block_size - 1)*block_size)

                            def spawn(self) -> Point: # more explicit
                            if self.state:
                            return self.position
                            else:
                            self.state = True
                            self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                            y=random.randint(0, bg_width//block_size - 1)*block_size)
                            return self.position


                            I think this is especially useful when used together with Cordes's suggestion in his answer, in his Separate game logic from screen rendering details section.



                            Another good thing about namedtuple is that we can still do unpacking.



                                    for x, y in snake.get_body():    # still works
                            pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                            Refactor Random-Point Generation



                            There are two places where the coordinates of the food are updated with random integers, generated via a special formula. Based on the DRY principle, I suggest refactoring the generation of random points to a single function get_random_point().



                            def get_random_point():
                            return Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                            y=random.randint(0, bg_width//block_size - 1)*block_size)


                            Then you can conveniently do self.position = get_random_point() without having to painfully, repetitively type out the above.



                            Refactoring Food



                            Currently, Food updates its position only when spawn() is called and if state is false. Seems a bit long-winded. Even the name update seems to be untruthful as it makes a delayed update.



                            I suggest updating the position immediately when update() is called.



                                def spawn(self) -> Tuple[int, int]:
                            return self.position

                            def update(self):
                            self.position = get_random_point()


                            Note that Food.state and Food.spawn(self) are redundant now and can be removed. That should be three cheers (less lines of code, yes?).



                            On collision(Snake, int, int)



                            Logically, this section should come first, but I saved it for the last.




                            1. The name collision is ambiguous. Can we improve it? Sure! Note that logically speaking, the function returns a boolean, so it's a predicate. Can we prefix it with is? Certainly! I think is_snake_on_food isn't too bad. You could also go with is_food_reached or is_snake_colliding_with_food.


                            2. Having changed how positions are stored in the Food class, we can pass in the food's position directly. Thus the signature of the function can be reduced to:



                              def is_snake_on_food(snake: Snake, food_target: Point):


                              This also saves us from needing unpack *food_pos in line 209.



                              # Before:
                              collision_ = collision(snake, *food_pos)

                              # After:
                              collision_ = is_snake_on_food(snake, food_pos)



                            3. There is no need to create pygame.Rect just to compare snake_.head and food_target. Currently, lines 128-131 are



                                  snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
                              food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
                              block_size)
                              if snake_rect == food_rect:


                              Instead, the coordinates can be compared directly:



                                  if (*snake_.head,) == (food_target_x, food_target_y):


                              Having passed in food_target as a Point, we can simplify this to



                                  if snake_.head == food_target:



                            Hope this helps!






                            share|improve this answer










                            $endgroup$


















                              3














                              3










                              3







                              $begingroup$

                              Good going on your first game. I completely agree with most of the points made by others. Here are a few more nitpicks. This answer is a bit long-winded, but take it as a complement. :-)



                              Unpack the Coordinates



                              This is what lines 195-197 currently look like:



                                      for item in snake.get_body():
                              pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                              block_size])


                              Instead of relying on indexing, we can directly unpack item into its respective xy-coordinates. This makes for good brevity and readability.



                                      for x, y in snake.get_body():
                              pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                              The same can be done for lines 208-214.



                              # Before:
                              food_pos = food.spawn()
                              collision_ = collision(snake, *food_pos) # 🤔
                              if collision_ == 1:
                              score += 1
                              food.update(False)
                              pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1], # 🤔
                              block_size, block_size])


                              # After:
                              food_x, food_y = food.spawn()
                              # the rest is left as an exercise for the reader


                              On wall_collision(Snake)



                              Here's what the wall_collision function currently looks like (lines 137-141):



                              def wall_collision(s: Snake) -> bool:
                              if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
                              or (s.head[1] > bg_height-block_size):
                              return True
                              return False



                              1. Being a predicate (i.e. a function which returns a boolean), the function can be better named. You can always find a way to name a predicate such that it is prefixed with is or has. Instead of wall_collision, I'd go with is_colliding_with_wall or collides_with_wall to communicate the intention better.

                              2. It could be simplified. vurmux's answer suggests a few possible ways. However, there exists an easier (and more readable) way of writing it in one line:


                                  return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


                              I intentionally added the extraneous parentheses to group comparisons.



                              This is made possible by Python's comparison "chaining" along with De Morgan's Laws. For fun, you can try to prove that the two snippets are equivalent.



                              Points and Coordinates



                              It took a while for me to figure out what the following line meant:



                              self.head = [int(10*block_size), int(5*block_size)]


                              I'm guessing this is a point/coordinate? Looks like it.



                              Lists are so prevalent in Python, that they can hold a multitude of meanings. What's more, they are variable-length constructs, which communicates something else: you might be changing the length of the object in the future. This might mean appending, erasing, or inserting into the list.



                              self.body uses a list appropriately, since it will be appended to and popped from in various places. On the other hand, not once does self.head use list.append, list.pop, or list.insert and this raises a question: does it need to be a list at all? It is better made a tuple, which is a fixed-length construct and immediately communicates to the reader that we won't be modifying the length of self.head.



                              I commend the usage of tuples in Food.spawn() for this very reason. Still, tuples can carry a multitude of different meanings. We can do better by using collections.namedtuple and creating a record-type Point for the purpose of representing xy-coordinates. This can greatly reduce ambiguity and improve readability.



                              from collections import namedtuple
                              Point = namedtuple('Point', ['x', 'y']) # we now have a Point type

                              class Snake:
                              ...
                              def __init__(self, color: Tuple[int, int, int]) -> None:
                              self.head = Point(10*block_size, 5*block_size) # foolproof: self.head is a point
                              self.body = [self.head, Point(...)]
                              ...
                              def add_to_tail(self) -> None:
                              self.body.append(Point(self.body[-1][0], self.body[-1][1]))
                              self.size += 1


                              The only pain with namedtuple is that we can't explicitly do assignments, which can be solved by making a new object:



                              def move(self) -> None:
                              if self.direction == right:

                              # Before:
                              self.head.x += block_size # AttributeError: can't set attribute

                              # After:
                              self.head = Point(self.head.x + block_size, self.head.y) # preferable
                              # or:
                              self.head = self.head._replace(x=self.head.x + block_size) # looks hacky


                              You can also use Point as a replacement for Food.position, which isn't being used.



                              class Food:
                              ...
                              position: Point

                              def __init__(self, color: Tuple[int, int, int]) -> None:
                              self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)

                              def spawn(self) -> Point: # more explicit
                              if self.state:
                              return self.position
                              else:
                              self.state = True
                              self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)
                              return self.position


                              I think this is especially useful when used together with Cordes's suggestion in his answer, in his Separate game logic from screen rendering details section.



                              Another good thing about namedtuple is that we can still do unpacking.



                                      for x, y in snake.get_body():    # still works
                              pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                              Refactor Random-Point Generation



                              There are two places where the coordinates of the food are updated with random integers, generated via a special formula. Based on the DRY principle, I suggest refactoring the generation of random points to a single function get_random_point().



                              def get_random_point():
                              return Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)


                              Then you can conveniently do self.position = get_random_point() without having to painfully, repetitively type out the above.



                              Refactoring Food



                              Currently, Food updates its position only when spawn() is called and if state is false. Seems a bit long-winded. Even the name update seems to be untruthful as it makes a delayed update.



                              I suggest updating the position immediately when update() is called.



                                  def spawn(self) -> Tuple[int, int]:
                              return self.position

                              def update(self):
                              self.position = get_random_point()


                              Note that Food.state and Food.spawn(self) are redundant now and can be removed. That should be three cheers (less lines of code, yes?).



                              On collision(Snake, int, int)



                              Logically, this section should come first, but I saved it for the last.




                              1. The name collision is ambiguous. Can we improve it? Sure! Note that logically speaking, the function returns a boolean, so it's a predicate. Can we prefix it with is? Certainly! I think is_snake_on_food isn't too bad. You could also go with is_food_reached or is_snake_colliding_with_food.


                              2. Having changed how positions are stored in the Food class, we can pass in the food's position directly. Thus the signature of the function can be reduced to:



                                def is_snake_on_food(snake: Snake, food_target: Point):


                                This also saves us from needing unpack *food_pos in line 209.



                                # Before:
                                collision_ = collision(snake, *food_pos)

                                # After:
                                collision_ = is_snake_on_food(snake, food_pos)



                              3. There is no need to create pygame.Rect just to compare snake_.head and food_target. Currently, lines 128-131 are



                                    snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
                                food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
                                block_size)
                                if snake_rect == food_rect:


                                Instead, the coordinates can be compared directly:



                                    if (*snake_.head,) == (food_target_x, food_target_y):


                                Having passed in food_target as a Point, we can simplify this to



                                    if snake_.head == food_target:



                              Hope this helps!






                              share|improve this answer










                              $endgroup$



                              Good going on your first game. I completely agree with most of the points made by others. Here are a few more nitpicks. This answer is a bit long-winded, but take it as a complement. :-)



                              Unpack the Coordinates



                              This is what lines 195-197 currently look like:



                                      for item in snake.get_body():
                              pygame.draw.rect(window, snake.color, [item[0], item[1], block_size,
                              block_size])


                              Instead of relying on indexing, we can directly unpack item into its respective xy-coordinates. This makes for good brevity and readability.



                                      for x, y in snake.get_body():
                              pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                              The same can be done for lines 208-214.



                              # Before:
                              food_pos = food.spawn()
                              collision_ = collision(snake, *food_pos) # 🤔
                              if collision_ == 1:
                              score += 1
                              food.update(False)
                              pygame.draw.rect(window, food.color, [food_pos[0], food_pos[1], # 🤔
                              block_size, block_size])


                              # After:
                              food_x, food_y = food.spawn()
                              # the rest is left as an exercise for the reader


                              On wall_collision(Snake)



                              Here's what the wall_collision function currently looks like (lines 137-141):



                              def wall_collision(s: Snake) -> bool:
                              if (s.head[0] < 0) or (s.head[0] > bg_width-block_size) or (s.head[1] < 0)
                              or (s.head[1] > bg_height-block_size):
                              return True
                              return False



                              1. Being a predicate (i.e. a function which returns a boolean), the function can be better named. You can always find a way to name a predicate such that it is prefixed with is or has. Instead of wall_collision, I'd go with is_colliding_with_wall or collides_with_wall to communicate the intention better.

                              2. It could be simplified. vurmux's answer suggests a few possible ways. However, there exists an easier (and more readable) way of writing it in one line:


                                  return not ((0 < s.head[0] < bg_width-block_size) and (0 < s.head[1] < bg_height-block_size))


                              I intentionally added the extraneous parentheses to group comparisons.



                              This is made possible by Python's comparison "chaining" along with De Morgan's Laws. For fun, you can try to prove that the two snippets are equivalent.



                              Points and Coordinates



                              It took a while for me to figure out what the following line meant:



                              self.head = [int(10*block_size), int(5*block_size)]


                              I'm guessing this is a point/coordinate? Looks like it.



                              Lists are so prevalent in Python, that they can hold a multitude of meanings. What's more, they are variable-length constructs, which communicates something else: you might be changing the length of the object in the future. This might mean appending, erasing, or inserting into the list.



                              self.body uses a list appropriately, since it will be appended to and popped from in various places. On the other hand, not once does self.head use list.append, list.pop, or list.insert and this raises a question: does it need to be a list at all? It is better made a tuple, which is a fixed-length construct and immediately communicates to the reader that we won't be modifying the length of self.head.



                              I commend the usage of tuples in Food.spawn() for this very reason. Still, tuples can carry a multitude of different meanings. We can do better by using collections.namedtuple and creating a record-type Point for the purpose of representing xy-coordinates. This can greatly reduce ambiguity and improve readability.



                              from collections import namedtuple
                              Point = namedtuple('Point', ['x', 'y']) # we now have a Point type

                              class Snake:
                              ...
                              def __init__(self, color: Tuple[int, int, int]) -> None:
                              self.head = Point(10*block_size, 5*block_size) # foolproof: self.head is a point
                              self.body = [self.head, Point(...)]
                              ...
                              def add_to_tail(self) -> None:
                              self.body.append(Point(self.body[-1][0], self.body[-1][1]))
                              self.size += 1


                              The only pain with namedtuple is that we can't explicitly do assignments, which can be solved by making a new object:



                              def move(self) -> None:
                              if self.direction == right:

                              # Before:
                              self.head.x += block_size # AttributeError: can't set attribute

                              # After:
                              self.head = Point(self.head.x + block_size, self.head.y) # preferable
                              # or:
                              self.head = self.head._replace(x=self.head.x + block_size) # looks hacky


                              You can also use Point as a replacement for Food.position, which isn't being used.



                              class Food:
                              ...
                              position: Point

                              def __init__(self, color: Tuple[int, int, int]) -> None:
                              self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)

                              def spawn(self) -> Point: # more explicit
                              if self.state:
                              return self.position
                              else:
                              self.state = True
                              self.position = Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)
                              return self.position


                              I think this is especially useful when used together with Cordes's suggestion in his answer, in his Separate game logic from screen rendering details section.



                              Another good thing about namedtuple is that we can still do unpacking.



                                      for x, y in snake.get_body():    # still works
                              pygame.draw.rect(window, snake.color, [x, y, block_size, block_size])


                              Refactor Random-Point Generation



                              There are two places where the coordinates of the food are updated with random integers, generated via a special formula. Based on the DRY principle, I suggest refactoring the generation of random points to a single function get_random_point().



                              def get_random_point():
                              return Point(x=random.randint(0, bg_width//block_size - 1)*block_size
                              y=random.randint(0, bg_width//block_size - 1)*block_size)


                              Then you can conveniently do self.position = get_random_point() without having to painfully, repetitively type out the above.



                              Refactoring Food



                              Currently, Food updates its position only when spawn() is called and if state is false. Seems a bit long-winded. Even the name update seems to be untruthful as it makes a delayed update.



                              I suggest updating the position immediately when update() is called.



                                  def spawn(self) -> Tuple[int, int]:
                              return self.position

                              def update(self):
                              self.position = get_random_point()


                              Note that Food.state and Food.spawn(self) are redundant now and can be removed. That should be three cheers (less lines of code, yes?).



                              On collision(Snake, int, int)



                              Logically, this section should come first, but I saved it for the last.




                              1. The name collision is ambiguous. Can we improve it? Sure! Note that logically speaking, the function returns a boolean, so it's a predicate. Can we prefix it with is? Certainly! I think is_snake_on_food isn't too bad. You could also go with is_food_reached or is_snake_colliding_with_food.


                              2. Having changed how positions are stored in the Food class, we can pass in the food's position directly. Thus the signature of the function can be reduced to:



                                def is_snake_on_food(snake: Snake, food_target: Point):


                                This also saves us from needing unpack *food_pos in line 209.



                                # Before:
                                collision_ = collision(snake, *food_pos)

                                # After:
                                collision_ = is_snake_on_food(snake, food_pos)



                              3. There is no need to create pygame.Rect just to compare snake_.head and food_target. Currently, lines 128-131 are



                                    snake_rect = pygame.Rect(*snake_.head, block_size, block_size)
                                food_rect = pygame.Rect(food_target_x, food_target_y, block_size,
                                block_size)
                                if snake_rect == food_rect:


                                Instead, the coordinates can be compared directly:



                                    if (*snake_.head,) == (food_target_x, food_target_y):


                                Having passed in food_target as a Point, we can simplify this to



                                    if snake_.head == food_target:



                              Hope this helps!







                              share|improve this answer













                              share|improve this answer




                              share|improve this answer



                              share|improve this answer










                              answered May 30 at 14:36









                              TrebledJTrebledJ

                              3509 bronze badges




                              3509 bronze badges


























                                  1
















                                  $begingroup$

                                  Thanks for the game. I applied most changes suggested by @Dair and @vurmux as well as partially the suggestions from @Peter Cordes (some of them are not so simple). After that, there were still some things left:



                                  Game logic



                                  I'm not sure whether it was intentional, but the snake does not collide with itself. In usual snake games, if the snake bites into her tail, the game is over as well.



                                  Swallowed keypresses



                                  When playing the game, I noticed that I sometimes cannot perform a U-turn when I needed to. It seems that the first keypress got lost.



                                  The reason is here:



                                  pressed = pygame.key.get_pressed()
                                  if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
                                  ...


                                  which should be changed to



                                  if event.type == pygame.KEYDOWN:
                                  if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                                  snake.change_dir(Direction.RIGHT)
                                  elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
                                  snake.change_dir(Direction.LEFT)
                                  elif event.key == pygame.K_UP or event.key == pygame.K_w:
                                  snake.change_dir(Direction.UP)
                                  elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                                  snake.change_dir(Direction.DOWN)


                                  However, this brings us to an interesting other problem: doing 2 changes without moving the snake makes the snake run into the opposite direction immediately (without hitting itself as noted before).



                                  Since pygame.event.get() consumes all events, it would be up to you, queuing the events yourself and processing them the next frame.



                                  High CPU usage when game is over



                                  When the game is over, it uses ~14% CPU time on my machine, which means that 1 core is 100% busy. Adding a pygame.time.delay(60) into the while is_over loop helps.



                                  Classes in files



                                  In addition I tried to apply another principle, which is to have each class in a separate file. Doing so is quite simple with an IDE like Pycharm, since it has the move-refactoring. Unfortunately, it doesn't work any more, because of this:



                                  After the refactoring, Food has



                                  from game import bg_width, block_size, bg_height


                                  So one's left with a cycle of imports due to the use of global variables. Looking at the food class for possible resolutions, you could pass the necessary dependencies as parameters:



                                  def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


                                  After the refactoring, Snake has



                                  from game import block_size


                                  and a similar solution can apply.



                                  Static members



                                  Your classes seem to define the properties twice, once in __init__ and once as static members. I don't see any usage of the static members, so these can be removed in Snake:



                                  head: List[int, int]
                                  color: Tuple[int, int, int]
                                  body: List[List[int, int]]
                                  direction: str
                                  size: int


                                  and in Food:



                                  x: int
                                  y: int
                                  color: Tuple[int, int, int]
                                  state: bool
                                  position: Tuple[int, int]


                                  Naming



                                  Assuming that bg is short for background, I really wonder whether that's the correct term here. The snake is moving on a board, not on a background I'd say. In the comment for that line, you call it screen width and height, which may accidentally be the case as well.



                                  Considering a future version of the game where you add a nice background graphic and display the score below the board, neither background nor screen would match any more.



                                  Code duplication



                                  With the change before, I noticed that there's duplicate code in the Food class. Inside __init__, it basically spawns itself.



                                  This duplication can be removed and adding



                                  food = Food(BLUE)
                                  food.spawn(bg_width, bg_height, block_size)


                                  It can later be discussed whether or not Food needs to be spawned that early.



                                  Potential stack overflow in game over mode



                                  Once the game is over, there's a loop handling the situation:



                                  while is_over:


                                  I expected the game to get out of this loop when a new round begins. However, that's not the case. Instead there is



                                  if event.key == pygame.K_r:
                                  game()


                                  This is a recursive call. It's unlikely that it will cause problems in this particular game, but in general, this may cause stack overflows.



                                  It can be resolved by introducing another loop



                                  while running:
                                  while is_over and running:
                                  ...

                                  # Initialization code here

                                  while running and not is_over:
                                  ...


                                  Instead of calling game(), you can then set is_over = False.



                                  Unused variable / unreachable code



                                  The while running loop can be replaced by a while True, since there's no other assignment to running which would terminate the loop.



                                  This also means that the code after while running will never be reached:



                                  pygame.quit()
                                  quit()


                                  Changing the exit routine to running = False, you save some duplicate code and the code runs to the end. This is e.g. helpful if you later want to implement saving a highscore list etc. If you have many exit points during your program, it will be harder to implement something at the end of the game.



                                  You can also omit quit(), because it is not helpful as the last statement of your code.



                                  Smaller improvements



                                  food.update() is only called with False as a parameter. It's never called with True. So this argument can be omitted and go hard-coded into the update() method. The code then looks like this:



                                  while running:
                                  ...
                                  food_pos = food.spawn(board_width, board_height, block_size)
                                  if collision(snake, *food_pos):
                                  score += 1
                                  food.update()


                                  This reads like the food is spawning in a new place with every frame. IMHO it reads better like this:



                                  while running:
                                  ...
                                  food_pos = food.??? # whatever
                                  if collision(snake, *food_pos):
                                  score += 1
                                  food.spawn(board_width, board_height, block_size)


                                  Because that makes it clear that food only spaws whenever it collided with the snake aka. it was eaten.



                                  Snake direction change



                                  Note: @Peter Cordes' vector approach is even more elegant. Perhaps the following might show you a refactoring you can apply in other cases as well when a vector does not fit.



                                  After applying the enum suggestion, the direction check looks like this



                                  def change_dir(self, direction: Direction) -> None:
                                  if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                  self.direction = Direction.RIGHT
                                  elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                  self.direction = Direction.LEFT
                                  elif self.direction != Direction.DOWN and direction == Direction.UP:
                                  self.direction = Direction.UP
                                  elif self.direction != Direction.UP and direction == Direction.DOWN:
                                  self.direction = Direction.DOWN


                                  Combining self.direction = Direction.RIGHT and direction == Direction.RIGHT, we can simplify



                                  self.direction = direction


                                  This applies to all 4 cases, so we end up with



                                  def change_dir(self, direction: Direction) -> None:
                                  if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                  self.direction = direction
                                  elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                  self.direction = direction
                                  elif self.direction != Direction.DOWN and direction == Direction.UP:
                                  self.direction = direction
                                  elif self.direction != Direction.UP and direction == Direction.DOWN:
                                  self.direction = direction


                                  Now, we can argue that this is duplicate code and remove the duplication:



                                  def change_dir(self, direction: Direction) -> None:
                                  if (self.direction != Direction.LEFT and direction == Direction.RIGHT) or
                                  (self.direction != Direction.RIGHT and direction == Direction.LEFT) or
                                  (self.direction != Direction.DOWN and direction == Direction.UP) or
                                  (self.direction != Direction.UP and direction == Direction.DOWN):
                                  self.direction = direction


                                  Personally, I'd even prefer



                                  def change_dir(self, direction: Direction) -> None:
                                  if self.is_opposite_direction(direction, self.direction):
                                  return
                                  self.direction = direction





                                  share|improve this answer












                                  $endgroup$




















                                    1
















                                    $begingroup$

                                    Thanks for the game. I applied most changes suggested by @Dair and @vurmux as well as partially the suggestions from @Peter Cordes (some of them are not so simple). After that, there were still some things left:



                                    Game logic



                                    I'm not sure whether it was intentional, but the snake does not collide with itself. In usual snake games, if the snake bites into her tail, the game is over as well.



                                    Swallowed keypresses



                                    When playing the game, I noticed that I sometimes cannot perform a U-turn when I needed to. It seems that the first keypress got lost.



                                    The reason is here:



                                    pressed = pygame.key.get_pressed()
                                    if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
                                    ...


                                    which should be changed to



                                    if event.type == pygame.KEYDOWN:
                                    if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                                    snake.change_dir(Direction.RIGHT)
                                    elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
                                    snake.change_dir(Direction.LEFT)
                                    elif event.key == pygame.K_UP or event.key == pygame.K_w:
                                    snake.change_dir(Direction.UP)
                                    elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                                    snake.change_dir(Direction.DOWN)


                                    However, this brings us to an interesting other problem: doing 2 changes without moving the snake makes the snake run into the opposite direction immediately (without hitting itself as noted before).



                                    Since pygame.event.get() consumes all events, it would be up to you, queuing the events yourself and processing them the next frame.



                                    High CPU usage when game is over



                                    When the game is over, it uses ~14% CPU time on my machine, which means that 1 core is 100% busy. Adding a pygame.time.delay(60) into the while is_over loop helps.



                                    Classes in files



                                    In addition I tried to apply another principle, which is to have each class in a separate file. Doing so is quite simple with an IDE like Pycharm, since it has the move-refactoring. Unfortunately, it doesn't work any more, because of this:



                                    After the refactoring, Food has



                                    from game import bg_width, block_size, bg_height


                                    So one's left with a cycle of imports due to the use of global variables. Looking at the food class for possible resolutions, you could pass the necessary dependencies as parameters:



                                    def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


                                    After the refactoring, Snake has



                                    from game import block_size


                                    and a similar solution can apply.



                                    Static members



                                    Your classes seem to define the properties twice, once in __init__ and once as static members. I don't see any usage of the static members, so these can be removed in Snake:



                                    head: List[int, int]
                                    color: Tuple[int, int, int]
                                    body: List[List[int, int]]
                                    direction: str
                                    size: int


                                    and in Food:



                                    x: int
                                    y: int
                                    color: Tuple[int, int, int]
                                    state: bool
                                    position: Tuple[int, int]


                                    Naming



                                    Assuming that bg is short for background, I really wonder whether that's the correct term here. The snake is moving on a board, not on a background I'd say. In the comment for that line, you call it screen width and height, which may accidentally be the case as well.



                                    Considering a future version of the game where you add a nice background graphic and display the score below the board, neither background nor screen would match any more.



                                    Code duplication



                                    With the change before, I noticed that there's duplicate code in the Food class. Inside __init__, it basically spawns itself.



                                    This duplication can be removed and adding



                                    food = Food(BLUE)
                                    food.spawn(bg_width, bg_height, block_size)


                                    It can later be discussed whether or not Food needs to be spawned that early.



                                    Potential stack overflow in game over mode



                                    Once the game is over, there's a loop handling the situation:



                                    while is_over:


                                    I expected the game to get out of this loop when a new round begins. However, that's not the case. Instead there is



                                    if event.key == pygame.K_r:
                                    game()


                                    This is a recursive call. It's unlikely that it will cause problems in this particular game, but in general, this may cause stack overflows.



                                    It can be resolved by introducing another loop



                                    while running:
                                    while is_over and running:
                                    ...

                                    # Initialization code here

                                    while running and not is_over:
                                    ...


                                    Instead of calling game(), you can then set is_over = False.



                                    Unused variable / unreachable code



                                    The while running loop can be replaced by a while True, since there's no other assignment to running which would terminate the loop.



                                    This also means that the code after while running will never be reached:



                                    pygame.quit()
                                    quit()


                                    Changing the exit routine to running = False, you save some duplicate code and the code runs to the end. This is e.g. helpful if you later want to implement saving a highscore list etc. If you have many exit points during your program, it will be harder to implement something at the end of the game.



                                    You can also omit quit(), because it is not helpful as the last statement of your code.



                                    Smaller improvements



                                    food.update() is only called with False as a parameter. It's never called with True. So this argument can be omitted and go hard-coded into the update() method. The code then looks like this:



                                    while running:
                                    ...
                                    food_pos = food.spawn(board_width, board_height, block_size)
                                    if collision(snake, *food_pos):
                                    score += 1
                                    food.update()


                                    This reads like the food is spawning in a new place with every frame. IMHO it reads better like this:



                                    while running:
                                    ...
                                    food_pos = food.??? # whatever
                                    if collision(snake, *food_pos):
                                    score += 1
                                    food.spawn(board_width, board_height, block_size)


                                    Because that makes it clear that food only spaws whenever it collided with the snake aka. it was eaten.



                                    Snake direction change



                                    Note: @Peter Cordes' vector approach is even more elegant. Perhaps the following might show you a refactoring you can apply in other cases as well when a vector does not fit.



                                    After applying the enum suggestion, the direction check looks like this



                                    def change_dir(self, direction: Direction) -> None:
                                    if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                    self.direction = Direction.RIGHT
                                    elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                    self.direction = Direction.LEFT
                                    elif self.direction != Direction.DOWN and direction == Direction.UP:
                                    self.direction = Direction.UP
                                    elif self.direction != Direction.UP and direction == Direction.DOWN:
                                    self.direction = Direction.DOWN


                                    Combining self.direction = Direction.RIGHT and direction == Direction.RIGHT, we can simplify



                                    self.direction = direction


                                    This applies to all 4 cases, so we end up with



                                    def change_dir(self, direction: Direction) -> None:
                                    if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                    self.direction = direction
                                    elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                    self.direction = direction
                                    elif self.direction != Direction.DOWN and direction == Direction.UP:
                                    self.direction = direction
                                    elif self.direction != Direction.UP and direction == Direction.DOWN:
                                    self.direction = direction


                                    Now, we can argue that this is duplicate code and remove the duplication:



                                    def change_dir(self, direction: Direction) -> None:
                                    if (self.direction != Direction.LEFT and direction == Direction.RIGHT) or
                                    (self.direction != Direction.RIGHT and direction == Direction.LEFT) or
                                    (self.direction != Direction.DOWN and direction == Direction.UP) or
                                    (self.direction != Direction.UP and direction == Direction.DOWN):
                                    self.direction = direction


                                    Personally, I'd even prefer



                                    def change_dir(self, direction: Direction) -> None:
                                    if self.is_opposite_direction(direction, self.direction):
                                    return
                                    self.direction = direction





                                    share|improve this answer












                                    $endgroup$


















                                      1














                                      1










                                      1







                                      $begingroup$

                                      Thanks for the game. I applied most changes suggested by @Dair and @vurmux as well as partially the suggestions from @Peter Cordes (some of them are not so simple). After that, there were still some things left:



                                      Game logic



                                      I'm not sure whether it was intentional, but the snake does not collide with itself. In usual snake games, if the snake bites into her tail, the game is over as well.



                                      Swallowed keypresses



                                      When playing the game, I noticed that I sometimes cannot perform a U-turn when I needed to. It seems that the first keypress got lost.



                                      The reason is here:



                                      pressed = pygame.key.get_pressed()
                                      if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
                                      ...


                                      which should be changed to



                                      if event.type == pygame.KEYDOWN:
                                      if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                                      snake.change_dir(Direction.RIGHT)
                                      elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
                                      snake.change_dir(Direction.LEFT)
                                      elif event.key == pygame.K_UP or event.key == pygame.K_w:
                                      snake.change_dir(Direction.UP)
                                      elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                                      snake.change_dir(Direction.DOWN)


                                      However, this brings us to an interesting other problem: doing 2 changes without moving the snake makes the snake run into the opposite direction immediately (without hitting itself as noted before).



                                      Since pygame.event.get() consumes all events, it would be up to you, queuing the events yourself and processing them the next frame.



                                      High CPU usage when game is over



                                      When the game is over, it uses ~14% CPU time on my machine, which means that 1 core is 100% busy. Adding a pygame.time.delay(60) into the while is_over loop helps.



                                      Classes in files



                                      In addition I tried to apply another principle, which is to have each class in a separate file. Doing so is quite simple with an IDE like Pycharm, since it has the move-refactoring. Unfortunately, it doesn't work any more, because of this:



                                      After the refactoring, Food has



                                      from game import bg_width, block_size, bg_height


                                      So one's left with a cycle of imports due to the use of global variables. Looking at the food class for possible resolutions, you could pass the necessary dependencies as parameters:



                                      def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


                                      After the refactoring, Snake has



                                      from game import block_size


                                      and a similar solution can apply.



                                      Static members



                                      Your classes seem to define the properties twice, once in __init__ and once as static members. I don't see any usage of the static members, so these can be removed in Snake:



                                      head: List[int, int]
                                      color: Tuple[int, int, int]
                                      body: List[List[int, int]]
                                      direction: str
                                      size: int


                                      and in Food:



                                      x: int
                                      y: int
                                      color: Tuple[int, int, int]
                                      state: bool
                                      position: Tuple[int, int]


                                      Naming



                                      Assuming that bg is short for background, I really wonder whether that's the correct term here. The snake is moving on a board, not on a background I'd say. In the comment for that line, you call it screen width and height, which may accidentally be the case as well.



                                      Considering a future version of the game where you add a nice background graphic and display the score below the board, neither background nor screen would match any more.



                                      Code duplication



                                      With the change before, I noticed that there's duplicate code in the Food class. Inside __init__, it basically spawns itself.



                                      This duplication can be removed and adding



                                      food = Food(BLUE)
                                      food.spawn(bg_width, bg_height, block_size)


                                      It can later be discussed whether or not Food needs to be spawned that early.



                                      Potential stack overflow in game over mode



                                      Once the game is over, there's a loop handling the situation:



                                      while is_over:


                                      I expected the game to get out of this loop when a new round begins. However, that's not the case. Instead there is



                                      if event.key == pygame.K_r:
                                      game()


                                      This is a recursive call. It's unlikely that it will cause problems in this particular game, but in general, this may cause stack overflows.



                                      It can be resolved by introducing another loop



                                      while running:
                                      while is_over and running:
                                      ...

                                      # Initialization code here

                                      while running and not is_over:
                                      ...


                                      Instead of calling game(), you can then set is_over = False.



                                      Unused variable / unreachable code



                                      The while running loop can be replaced by a while True, since there's no other assignment to running which would terminate the loop.



                                      This also means that the code after while running will never be reached:



                                      pygame.quit()
                                      quit()


                                      Changing the exit routine to running = False, you save some duplicate code and the code runs to the end. This is e.g. helpful if you later want to implement saving a highscore list etc. If you have many exit points during your program, it will be harder to implement something at the end of the game.



                                      You can also omit quit(), because it is not helpful as the last statement of your code.



                                      Smaller improvements



                                      food.update() is only called with False as a parameter. It's never called with True. So this argument can be omitted and go hard-coded into the update() method. The code then looks like this:



                                      while running:
                                      ...
                                      food_pos = food.spawn(board_width, board_height, block_size)
                                      if collision(snake, *food_pos):
                                      score += 1
                                      food.update()


                                      This reads like the food is spawning in a new place with every frame. IMHO it reads better like this:



                                      while running:
                                      ...
                                      food_pos = food.??? # whatever
                                      if collision(snake, *food_pos):
                                      score += 1
                                      food.spawn(board_width, board_height, block_size)


                                      Because that makes it clear that food only spaws whenever it collided with the snake aka. it was eaten.



                                      Snake direction change



                                      Note: @Peter Cordes' vector approach is even more elegant. Perhaps the following might show you a refactoring you can apply in other cases as well when a vector does not fit.



                                      After applying the enum suggestion, the direction check looks like this



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                      self.direction = Direction.RIGHT
                                      elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                      self.direction = Direction.LEFT
                                      elif self.direction != Direction.DOWN and direction == Direction.UP:
                                      self.direction = Direction.UP
                                      elif self.direction != Direction.UP and direction == Direction.DOWN:
                                      self.direction = Direction.DOWN


                                      Combining self.direction = Direction.RIGHT and direction == Direction.RIGHT, we can simplify



                                      self.direction = direction


                                      This applies to all 4 cases, so we end up with



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                      self.direction = direction
                                      elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                      self.direction = direction
                                      elif self.direction != Direction.DOWN and direction == Direction.UP:
                                      self.direction = direction
                                      elif self.direction != Direction.UP and direction == Direction.DOWN:
                                      self.direction = direction


                                      Now, we can argue that this is duplicate code and remove the duplication:



                                      def change_dir(self, direction: Direction) -> None:
                                      if (self.direction != Direction.LEFT and direction == Direction.RIGHT) or
                                      (self.direction != Direction.RIGHT and direction == Direction.LEFT) or
                                      (self.direction != Direction.DOWN and direction == Direction.UP) or
                                      (self.direction != Direction.UP and direction == Direction.DOWN):
                                      self.direction = direction


                                      Personally, I'd even prefer



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.is_opposite_direction(direction, self.direction):
                                      return
                                      self.direction = direction





                                      share|improve this answer












                                      $endgroup$



                                      Thanks for the game. I applied most changes suggested by @Dair and @vurmux as well as partially the suggestions from @Peter Cordes (some of them are not so simple). After that, there were still some things left:



                                      Game logic



                                      I'm not sure whether it was intentional, but the snake does not collide with itself. In usual snake games, if the snake bites into her tail, the game is over as well.



                                      Swallowed keypresses



                                      When playing the game, I noticed that I sometimes cannot perform a U-turn when I needed to. It seems that the first keypress got lost.



                                      The reason is here:



                                      pressed = pygame.key.get_pressed()
                                      if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]:
                                      ...


                                      which should be changed to



                                      if event.type == pygame.KEYDOWN:
                                      if event.key == pygame.K_RIGHT or event.key == pygame.K_d:
                                      snake.change_dir(Direction.RIGHT)
                                      elif event.key == pygame.K_LEFT or event.key == pygame.K_a:
                                      snake.change_dir(Direction.LEFT)
                                      elif event.key == pygame.K_UP or event.key == pygame.K_w:
                                      snake.change_dir(Direction.UP)
                                      elif event.key == pygame.K_DOWN or event.key == pygame.K_s:
                                      snake.change_dir(Direction.DOWN)


                                      However, this brings us to an interesting other problem: doing 2 changes without moving the snake makes the snake run into the opposite direction immediately (without hitting itself as noted before).



                                      Since pygame.event.get() consumes all events, it would be up to you, queuing the events yourself and processing them the next frame.



                                      High CPU usage when game is over



                                      When the game is over, it uses ~14% CPU time on my machine, which means that 1 core is 100% busy. Adding a pygame.time.delay(60) into the while is_over loop helps.



                                      Classes in files



                                      In addition I tried to apply another principle, which is to have each class in a separate file. Doing so is quite simple with an IDE like Pycharm, since it has the move-refactoring. Unfortunately, it doesn't work any more, because of this:



                                      After the refactoring, Food has



                                      from game import bg_width, block_size, bg_height


                                      So one's left with a cycle of imports due to the use of global variables. Looking at the food class for possible resolutions, you could pass the necessary dependencies as parameters:



                                      def spawn(self, bg_width:int, bg_height:int, block_size:int) -> Tuple[int, int]:


                                      After the refactoring, Snake has



                                      from game import block_size


                                      and a similar solution can apply.



                                      Static members



                                      Your classes seem to define the properties twice, once in __init__ and once as static members. I don't see any usage of the static members, so these can be removed in Snake:



                                      head: List[int, int]
                                      color: Tuple[int, int, int]
                                      body: List[List[int, int]]
                                      direction: str
                                      size: int


                                      and in Food:



                                      x: int
                                      y: int
                                      color: Tuple[int, int, int]
                                      state: bool
                                      position: Tuple[int, int]


                                      Naming



                                      Assuming that bg is short for background, I really wonder whether that's the correct term here. The snake is moving on a board, not on a background I'd say. In the comment for that line, you call it screen width and height, which may accidentally be the case as well.



                                      Considering a future version of the game where you add a nice background graphic and display the score below the board, neither background nor screen would match any more.



                                      Code duplication



                                      With the change before, I noticed that there's duplicate code in the Food class. Inside __init__, it basically spawns itself.



                                      This duplication can be removed and adding



                                      food = Food(BLUE)
                                      food.spawn(bg_width, bg_height, block_size)


                                      It can later be discussed whether or not Food needs to be spawned that early.



                                      Potential stack overflow in game over mode



                                      Once the game is over, there's a loop handling the situation:



                                      while is_over:


                                      I expected the game to get out of this loop when a new round begins. However, that's not the case. Instead there is



                                      if event.key == pygame.K_r:
                                      game()


                                      This is a recursive call. It's unlikely that it will cause problems in this particular game, but in general, this may cause stack overflows.



                                      It can be resolved by introducing another loop



                                      while running:
                                      while is_over and running:
                                      ...

                                      # Initialization code here

                                      while running and not is_over:
                                      ...


                                      Instead of calling game(), you can then set is_over = False.



                                      Unused variable / unreachable code



                                      The while running loop can be replaced by a while True, since there's no other assignment to running which would terminate the loop.



                                      This also means that the code after while running will never be reached:



                                      pygame.quit()
                                      quit()


                                      Changing the exit routine to running = False, you save some duplicate code and the code runs to the end. This is e.g. helpful if you later want to implement saving a highscore list etc. If you have many exit points during your program, it will be harder to implement something at the end of the game.



                                      You can also omit quit(), because it is not helpful as the last statement of your code.



                                      Smaller improvements



                                      food.update() is only called with False as a parameter. It's never called with True. So this argument can be omitted and go hard-coded into the update() method. The code then looks like this:



                                      while running:
                                      ...
                                      food_pos = food.spawn(board_width, board_height, block_size)
                                      if collision(snake, *food_pos):
                                      score += 1
                                      food.update()


                                      This reads like the food is spawning in a new place with every frame. IMHO it reads better like this:



                                      while running:
                                      ...
                                      food_pos = food.??? # whatever
                                      if collision(snake, *food_pos):
                                      score += 1
                                      food.spawn(board_width, board_height, block_size)


                                      Because that makes it clear that food only spaws whenever it collided with the snake aka. it was eaten.



                                      Snake direction change



                                      Note: @Peter Cordes' vector approach is even more elegant. Perhaps the following might show you a refactoring you can apply in other cases as well when a vector does not fit.



                                      After applying the enum suggestion, the direction check looks like this



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                      self.direction = Direction.RIGHT
                                      elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                      self.direction = Direction.LEFT
                                      elif self.direction != Direction.DOWN and direction == Direction.UP:
                                      self.direction = Direction.UP
                                      elif self.direction != Direction.UP and direction == Direction.DOWN:
                                      self.direction = Direction.DOWN


                                      Combining self.direction = Direction.RIGHT and direction == Direction.RIGHT, we can simplify



                                      self.direction = direction


                                      This applies to all 4 cases, so we end up with



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.direction != Direction.LEFT and direction == Direction.RIGHT:
                                      self.direction = direction
                                      elif self.direction != Direction.RIGHT and direction == Direction.LEFT:
                                      self.direction = direction
                                      elif self.direction != Direction.DOWN and direction == Direction.UP:
                                      self.direction = direction
                                      elif self.direction != Direction.UP and direction == Direction.DOWN:
                                      self.direction = direction


                                      Now, we can argue that this is duplicate code and remove the duplication:



                                      def change_dir(self, direction: Direction) -> None:
                                      if (self.direction != Direction.LEFT and direction == Direction.RIGHT) or
                                      (self.direction != Direction.RIGHT and direction == Direction.LEFT) or
                                      (self.direction != Direction.DOWN and direction == Direction.UP) or
                                      (self.direction != Direction.UP and direction == Direction.DOWN):
                                      self.direction = direction


                                      Personally, I'd even prefer



                                      def change_dir(self, direction: Direction) -> None:
                                      if self.is_opposite_direction(direction, self.direction):
                                      return
                                      self.direction = direction






                                      share|improve this answer















                                      share|improve this answer




                                      share|improve this answer



                                      share|improve this answer








                                      edited Jun 2 at 17:51

























                                      answered Jun 2 at 9:25









                                      Thomas WellerThomas Weller

                                      4803 silver badges12 bronze badges




                                      4803 silver badges12 bronze badges


































                                          draft saved

                                          draft discarded



















































                                          Thanks for contributing an answer to Code Review Stack Exchange!


                                          • Please be sure to answer the question. Provide details and share your research!

                                          But avoid



                                          • Asking for help, clarification, or responding to other answers.

                                          • Making statements based on opinion; back them up with references or personal experience.


                                          Use MathJax to format equations. MathJax reference.


                                          To learn more, see our tips on writing great answers.




                                          draft saved


                                          draft discarded














                                          StackExchange.ready(
                                          function () {
                                          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221163%2fbeginners-snake-game-using-pygame%23new-answer', 'question_page');
                                          }
                                          );

                                          Post as a guest















                                          Required, but never shown





















































                                          Required, but never shown














                                          Required, but never shown












                                          Required, but never shown







                                          Required, but never shown

































                                          Required, but never shown














                                          Required, but never shown












                                          Required, but never shown







                                          Required, but never shown









                                          Popular posts from this blog

                                          He _____ here since 1970 . Answer needed [closed]What does “since he was so high” mean?Meaning of “catch birds for”?How do I ensure “since” takes the meaning I want?“Who cares here” meaningWhat does “right round toward” mean?the time tense (had now been detected)What does the phrase “ring around the roses” mean here?Correct usage of “visited upon”Meaning of “foiled rail sabotage bid”It was the third time I had gone to Rome or It is the third time I had been to Rome

                                          Bunad

                                          Færeyskur hestur Heimild | Tengill | Tilvísanir | LeiðsagnarvalRossið - síða um færeyska hrossið á færeyskuGott ár hjá færeyska hestinum