Beginner's snake game using PyGame
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{
margin-bottom:0;
}
$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()
python python-3.x pygame snake-game
$endgroup$
add a comment
|
$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()
python python-3.x pygame snake-game
$endgroup$
4
$begingroup$
It looks good, especially for your first game :)
$endgroup$
– 0liveradam8
May 28 at 0:23
$begingroup$
I barely know Python: wouldbody: 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
add a comment
|
$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()
python python-3.x pygame snake-game
$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
python python-3.x pygame snake-game
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: wouldbody: 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
add a comment
|
4
$begingroup$
It looks good, especially for your first game :)
$endgroup$
– 0liveradam8
May 28 at 0:23
$begingroup$
I barely know Python: wouldbody: 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
add a comment
|
5 Answers
5
active
oldest
votes
$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.
$endgroup$
$begingroup$
Nice answer! +1 also for the excellent summary of themain
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 doif wall_collision(snake): is_over = True
instead ofis_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
add a comment
|
$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:
$endgroup$
add a comment
|
$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 ofs.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"
.
$endgroup$
add a comment
|
$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
- 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
orhas
. Instead ofwall_collision
, I'd go withis_colliding_with_wall
orcollides_with_wall
to communicate the intention better. - 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.
- 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 withis
? Certainly! I thinkis_snake_on_food
isn't too bad. You could also go withis_food_reached
oris_snake_colliding_with_food
.
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)
There is no need to create
pygame.Rect
just to comparesnake_.head
andfood_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 aPoint
, we can simplify this to
if snake_.head == food_target:
Hope this helps!
$endgroup$
add a comment
|
$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 spawn
s 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
$endgroup$
add a comment
|
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
$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.
$endgroup$
$begingroup$
Nice answer! +1 also for the excellent summary of themain
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 doif wall_collision(snake): is_over = True
instead ofis_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
add a comment
|
$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.
$endgroup$
$begingroup$
Nice answer! +1 also for the excellent summary of themain
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 doif wall_collision(snake): is_over = True
instead ofis_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
add a comment
|
$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.
$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.
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 themain
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 doif wall_collision(snake): is_over = True
instead ofis_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
add a comment
|
$begingroup$
Nice answer! +1 also for the excellent summary of themain
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 doif wall_collision(snake): is_over = True
instead ofis_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
add a comment
|
$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:
$endgroup$
add a comment
|
$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:
$endgroup$
add a comment
|
$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:
$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:
answered May 28 at 10:15
vurmuxvurmux
1,2553 silver badges15 bronze badges
1,2553 silver badges15 bronze badges
add a comment
|
add a comment
|
$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 ofs.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"
.
$endgroup$
add a comment
|
$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 ofs.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"
.
$endgroup$
add a comment
|
$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 ofs.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"
.
$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 ofs.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"
.
answered May 28 at 20:21
Peter CordesPeter Cordes
1,7877 silver badges17 bronze badges
1,7877 silver badges17 bronze badges
add a comment
|
add a comment
|
$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
- 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
orhas
. Instead ofwall_collision
, I'd go withis_colliding_with_wall
orcollides_with_wall
to communicate the intention better. - 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.
- 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 withis
? Certainly! I thinkis_snake_on_food
isn't too bad. You could also go withis_food_reached
oris_snake_colliding_with_food
.
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)
There is no need to create
pygame.Rect
just to comparesnake_.head
andfood_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 aPoint
, we can simplify this to
if snake_.head == food_target:
Hope this helps!
$endgroup$
add a comment
|
$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
- 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
orhas
. Instead ofwall_collision
, I'd go withis_colliding_with_wall
orcollides_with_wall
to communicate the intention better. - 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.
- 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 withis
? Certainly! I thinkis_snake_on_food
isn't too bad. You could also go withis_food_reached
oris_snake_colliding_with_food
.
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)
There is no need to create
pygame.Rect
just to comparesnake_.head
andfood_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 aPoint
, we can simplify this to
if snake_.head == food_target:
Hope this helps!
$endgroup$
add a comment
|
$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
- 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
orhas
. Instead ofwall_collision
, I'd go withis_colliding_with_wall
orcollides_with_wall
to communicate the intention better. - 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.
- 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 withis
? Certainly! I thinkis_snake_on_food
isn't too bad. You could also go withis_food_reached
oris_snake_colliding_with_food
.
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)
There is no need to create
pygame.Rect
just to comparesnake_.head
andfood_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 aPoint
, we can simplify this to
if snake_.head == food_target:
Hope this helps!
$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
- 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
orhas
. Instead ofwall_collision
, I'd go withis_colliding_with_wall
orcollides_with_wall
to communicate the intention better. - 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.
- 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 withis
? Certainly! I thinkis_snake_on_food
isn't too bad. You could also go withis_food_reached
oris_snake_colliding_with_food
.
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)
There is no need to create
pygame.Rect
just to comparesnake_.head
andfood_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 aPoint
, we can simplify this to
if snake_.head == food_target:
Hope this helps!
answered May 30 at 14:36
TrebledJTrebledJ
3509 bronze badges
3509 bronze badges
add a comment
|
add a comment
|
$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 spawn
s 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
$endgroup$
add a comment
|
$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 spawn
s 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
$endgroup$
add a comment
|
$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 spawn
s 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
$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 spawn
s 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
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
add a comment
|
add a comment
|
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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
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