I know there's already a whole bunch of similar attempts at this and I've looked at a few of them to get some tips. My main aim with this was not to write anything fancy. I wanted to create a simple game with the most basic functionality but write it really well. In other words I'm more interested in making my code professional, efficient and of high quality. One way I've tried to do this is to keep the main function simple and use it mostly for initialisation and the game loop, whilst implementing most of the functionality in separate classes and functions. Also, I would appreciate any advice on comments and docstrings because I'm sure mine are far from perfect. I also noticed the clock speed is around 4.5 GHz while playing this, so I think this could definitely do with an efficiency boost.
#!/usr/bin/env python3
import pygame as pg
from random import randint
# Define window parameters
BLOCK_SIZE = 20
WIN_SIZE = 500
class Head():
blue = (0, 0, 255) # Colour of snake
start_params = (BLOCK_SIZE * 0.05, BLOCK_SIZE * 0.05,
BLOCK_SIZE * 0.9, BLOCK_SIZE * 0.9)
def __init__(self, pos):
"""Head of snake"""
self.x = pos[0]
self.y = pos[1]
self.last_x = self.x
self.last_y = self.y
self.direction = [0, -BLOCK_SIZE]
self.square = None
def make_block(self):
"""
Create a surface to contain a square
Draw a square Rect object onto said surface
"""
self.square = pg.Surface((BLOCK_SIZE, BLOCK_SIZE), pg.SRCALPHA)
# Draw a square onto the "square" surface
pg.draw.rect(self.square, self.blue, self.start_params)
def update_pos(self):
"""Last coords are used to update next block in the snake"""
self.last_x = self.x
self.last_y = self.y
self.x += self.direction[0]
self.y += self.direction[1]
def change_direction(self, new_dir):
"""Change direction of snake without allowing it to go backwards"""
if new_dir == 'u' and self.direction != [0, BLOCK_SIZE]:
self.direction = [0, -BLOCK_SIZE]
elif new_dir == 'd' and self.direction != [0, -BLOCK_SIZE]:
self.direction = [0, BLOCK_SIZE]
elif new_dir == 'l' and self.direction != [BLOCK_SIZE, 0]:
self.direction = [-BLOCK_SIZE, 0]
elif new_dir == 'r' and self.direction != [-BLOCK_SIZE, 0]:
self.direction = [BLOCK_SIZE, 0]
def check_collision(self, pos_list):
"""Check if snake collides with wall or itself"""
if self.x in (0, WIN_SIZE) or self.y in (0, WIN_SIZE):
return True
if (self.x, self.y) in pos_list[3:]:
return True
return False
def get_pos(self):
return (self.last_x, self.last_y)
class Block(Head):
def __init__(self, next_block):
"""Body of snake"""
self.next = next_block
pos = next_block.get_pos()
self.x = pos[0]
self.y = pos[1]
self.last_x = self.x
self.last_y = self.y
self.ready = 0
def update_pos(self):
"""Use position of next block in snake to update current position"""
self.last_x = self.x
self.last_y = self.y
next_pos = self.next.get_pos()
self.x = next_pos[0]
self.y = next_pos[1]
def add_block(snake_arr):
"""Extend snake by adding a snake block to the snake array"""
snake_arr.append(Block(snake_arr[-1]))
snake_arr[-1].make_block()
return snake_arr
def check_keypress(input_event, block_object):
"""
Take input event and change direction if arrow key
or quit game if esc key or other exit signal
"""
if input_event.type == pg.QUIT:
return True
elif input_event.type == pg.KEYDOWN:
if input_event.key == pg.K_ESCAPE:
return True
elif input_event.key == pg.K_UP:
block_object.change_direction('u')
elif input_event.key == pg.K_DOWN:
block_object.change_direction('d')
elif input_event.key == pg.K_LEFT:
block_object.change_direction('l')
elif input_event.key == pg.K_RIGHT:
block_object.change_direction('r')
return False
class Food():
def __init__(self):
"""Food block, created in the same way as a snake block"""
self.exists = False
self.x = None
self.y = None
self.square = None
def add_food(self):
"""If no food present, create a new food block with random position"""
if self.exists is False:
# Create a surface to contain a square
self.square = pg.Surface((BLOCK_SIZE, BLOCK_SIZE), pg.SRCALPHA)
# Draw a square onto the "square" surface
pg.draw.rect(self.square, (255, 0, 0),
(BLOCK_SIZE * 0.05, BLOCK_SIZE * 0.05,
BLOCK_SIZE * 0.9, BLOCK_SIZE * 0.9))
self.x = randint(1, (WIN_SIZE - BLOCK_SIZE)/BLOCK_SIZE) * BLOCK_SIZE
self.y = randint(1, (WIN_SIZE - BLOCK_SIZE)/BLOCK_SIZE) * BLOCK_SIZE
self.exists = True
def check_if_eaten(self, snake):
"""If snake head is in food block, food is eaten"""
snake_x, snake_y = snake[0]
if (self.x <= snake_x <= self.x + BLOCK_SIZE * 0.9) and (self.y <= snake_y <= self.y + BLOCK_SIZE * 0.9):
self.exists = False
return True
return False
def main():
# Initialise PyGame
pg.init()
clock = pg.time.Clock()
size = (WIN_SIZE, WIN_SIZE) # Size of window, (width, height)
black = (0, 0, 0) # Background colour of window
# Place head of snake in centre of window
start_coord = (WIN_SIZE / 2) - (BLOCK_SIZE / 2)
# Create window
screen = pg.display.set_mode(size)
head = Head([start_coord, start_coord])
head.make_block()
# Make first three blocks of snake
snake = []
snake.append(head)
snake = add_block(snake)
snake = add_block(snake)
ticker = 0
game_over = False
food = Food()
# Game loop
while game_over is False:
# Run game at 60 FPS
clock.tick(60)
# Monitor events and check for keypresses
for event in pg.event.get():
game_over = check_keypress(event, head)
if game_over is True:
continue
snake_pos = [block.get_pos() for block in snake]
game_over = head.check_collision(snake_pos)
# Update snake position every 4 frames
if ticker == 3:
for s in snake:
s.update_pos()
ticker = 0
ticker += 1
food.add_food()
eaten = food.check_if_eaten(snake_pos)
if eaten is True:
snake = add_block(snake)
# Clear the window before the next frame
screen.fill(black)
# Draw block to window
screen.blit(food.square, [food.x, food.y])
for s in snake:
screen.blit(s.square, [s.x, s.y])
# Swap buffers
pg.display.flip()
pg.quit()
if __name__ == "__main__":
main()
```
-
1\$\begingroup\$ the clock speed is around 4.5 GHz - That's not a very useful measurement of CPU occupation. Instead, check the OS-reported CPU load percentage. \$\endgroup\$Reinderien– Reinderien2020年05月25日 17:27:19 +00:00Commented May 25, 2020 at 17:27
-
\$\begingroup\$ The total CPU load percentage stays well under 10%, though the temperature and power rise considerably. Not to the point where the fans get noisy but more than I expected from such a simple program. I'm running an i9-9980HK if that helps clarify anything. \$\endgroup\$AkThao– AkThao2020年05月25日 18:11:02 +00:00Commented May 25, 2020 at 18:11
-
\$\begingroup\$ In theory, to accommodate for auto-scaling CPUs, you would want to measure operations per second, which is a linear factor of frequency and user-time percentage. \$\endgroup\$Reinderien– Reinderien2020年05月25日 18:17:04 +00:00Commented May 25, 2020 at 18:17
1 Answer 1
Unpacking arguments
This:
self.x = pos[0]
self.y = pos[1]
can be
self.x, self.y = pos
One advantage of the latter is that it will catch weird sequences that have more than two items. The easier thing to do is simply have a uniform representation of coordinates, and use x,y
everywhere instead of a random mixture of tuples and individual variables.
Type hints
def __init__(self, pos):
"""Head of snake"""
self.x = pos[0]
self.y = pos[1]
self.last_x = self.x
self.last_y = self.y
self.direction = [0, -BLOCK_SIZE]
self.square = None
can likely be
def __init__(self, pos: Tuple[int, int]):
"""Head of snake"""
self.x: int = pos[0]
self.y: int = pos[1]
self.last_x: int = self.x
self.last_y: int = self.y
self.direction: Tuple[int, int] = (0, -BLOCK_SIZE)
self.square: pg.Surface = None
Strongly-typed direction
Do not represent a direction as a u/d/l/r
string. Either represent it as an enum, or a unit vector of x,y in {(1,0), (-1,0), (0,1), (0,-1)}
.
Mystery position list
pos_list[3:]:
is spooky. It's not really a good idea to assign specific meanings to elements of a list such that you need to slice it for your business logic. What do the first three positions of the snake mean? Since they have a separate meaning, does it even make sense to keep them in the same list?
Magic constants
if ticker == 3:
should have its constant pulled out, so that you can write
if ticket == UPDATE_FRAMES - 1:
-
\$\begingroup\$ Thank you for your review Reinderien, I have added these improvements and significantly tidied up the
change_direction
method. Regarding thepos_list[3:]:
line, I've figured it out and changed it topos_list[1:]
, which hopefully isn't seemingly cryptic anymore. The reason this couldn't have worked before is because in the list of positions, I was storing the last position of every block. Since the 3 starting blocks are initialised in the same place, the body blocks' current coordinates would be the same as the head block's last coordinates. Hence, the game would quit immediately. \$\endgroup\$AkThao– AkThao2020年05月25日 19:46:17 +00:00Commented May 25, 2020 at 19:46 -
\$\begingroup\$ So to fix this, I renamed
get_pos
toget_last_pos
and added aget_current_pos
method. This way, the head block can safely check the current coordinates of the body blocks, without quitting the game straight away. Then the index of1
is simply because it doesn't make sense for the head to intersect itself, it just needs to check the rest of the body. \$\endgroup\$AkThao– AkThao2020年05月25日 19:48:37 +00:00Commented May 25, 2020 at 19:48