I've been reading Dive Into Python and I needed to actually program something before I forget everything I learned. So, this is my first Python project (apart from trying the basics and playing with console).
I first wrote this using lots of globals but that looked stupid so I rewrote it as a class. It was more-or-less main loop that called restart_game()
which reset variables. But, I had self
everywhere (eg. before every x
, is this normal?) and that seemed weird so I rewrote it. Now I have run_game()
which runs single game and is called from infinite loop.
Still, this being my first "real" Python program, I'm sure there's room for improvement.
snake.py
'''
Simple snake game using pygame.
'''
import pygame
import random
# R G B
WHITE = (255, 255, 255)
BLACK = ( 0, 0, 0)
RED = (255, 0, 0)
GREEN = ( 0, 210, 0)
DARK_GREEN = ( 0, 155, 0)
DARK_GRAY = ( 40, 40, 40)
BG_COLOR = BLACK
HEAD_COLOR = GREEN
SNAKE_COLOR = DARK_GREEN
FONT_SIZE = 36
# (dx, dy)
LEFT = (-1, 0)
RIGHT = (1, 0)
UP = (0, -1)
DOWN = (0, 1)
key_mapping = { pygame.K_LEFT : LEFT, pygame.K_a : LEFT,
pygame.K_RIGHT : RIGHT, pygame.K_d : RIGHT,
pygame.K_UP : UP, pygame.K_w : UP,
pygame.K_DOWN : DOWN, pygame.K_s : DOWN }
class Snake():
"""Simple snake game.
title - game title
width - window width
height - window height
cell_size - size of one tile
height and width need to be multiples of cell_size
game_speed - must be >1
"""
def __init__(self, title="Snake", width=640, height=480, cell_size=20, game_speed=8):
self.width = width
self.height = height
self.cell_size = cell_size
if height % cell_size != 0 or width % cell_size != 0:
raise ValueError("height and width need to be multiples of cell_size")
self.game_speed = game_speed
if game_speed < 1:
raise ValueError("game_speed must be bigger than 0")
pygame.init()
self.font = pygame.font.Font(None, FONT_SIZE)
self.clock = pygame.time.Clock()
self.display = pygame.display.set_mode((width, height))
pygame.display.set_caption(title)
self.display.fill(BG_COLOR)
def loop_games(self):
"""Start new game and keep restarting until player quits."""
while True:
self.run_game()
def run_game(self):
"""Run one game."""
row_count = self.height/self.cell_size
col_count = self.width/self.cell_size
x, y = self.get_starting_point()
snake = [(x, y), (x - 1, y), (x - 2, y)]
dx, dy = RIGHT
apple_x, apple_y = self.generate_apple()
while True:
current_dx, current_dy = dx, dy
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit(0)
elif event.type == pygame.KEYDOWN:
if event.key in key_mapping:
new_dx, new_dy = key_mapping[event.key]
# So you can't go from right to left and eat yourself.
# You can't check against new_dx/new_dy because it
# could be changed multiple times (eg. going right
# and pressing up and left quickly).
if new_dx != -current_dx and new_dy != -current_dy:
dx, dy = new_dx, new_dy
# move head
x += dx;
y += dy
snake.insert(0, (x, y))
if x == apple_x and y == apple_y:
apple_x, apple_y = self.generate_apple()
else:
# remove last part of snake
snake.pop()
self.display.fill(BG_COLOR)
# apple
apple_rect = pygame.Rect(apple_x * self.cell_size,
apple_y * self.cell_size,
self.cell_size, self.cell_size)
pygame.draw.rect(self.display, RED, apple_rect)
# body
game_over = False
for snake_x, snake_y in snake[1:]:
body_rect = pygame.Rect(snake_x * self.cell_size, snake_y * self.cell_size,
self.cell_size, self.cell_size)
pygame.draw.rect(self.display, SNAKE_COLOR, body_rect)
# border
pygame.draw.rect(self.display, DARK_GRAY, body_rect, 1)
if x == snake_x and y == snake_y:
game_over = True
# head
head_rect = pygame.Rect(x * self.cell_size, y * self.cell_size,
self.cell_size, self.cell_size)
pygame.draw.rect(self.display, HEAD_COLOR, head_rect)
pygame.display.flip()
self.clock.tick(self.game_speed)
if x < 0 or x >= col_count \
or y < 0 or y >= row_count \
or game_over:
self.show_game_over()
return
def show_game_over(self):
"""Show "game over" text."""
text = self.font.render("Game Over", True, WHITE)
text_rect = text.get_rect()
text_x = self.width / 2 - text_rect.width / 2
text_y = self.height / 2 - text_rect.height / 2
self.display.blit(text, (text_x, text_y))
pygame.display.flip()
pygame.time.wait(300)
def generate_apple(self):
"""Generate new apple coordinates (x, y)."""
return (random.randrange(0, self.width / self.cell_size),
random.randrange(0, self.height / self.cell_size))
def get_starting_point(self):
"""Return starting location of snake's head (x, y)."""
return (random.randrange(5, (self.width / self.cell_size) - 5),
random.randrange(5, (self.height / self.cell_size) - 5))
if __name__ == '__main__':
snake = Snake()
snake.loop_games()
1 Answer 1
Three minor remarks:
key_mapping
should be uppercase as all the oher constants are for readibility.The error reporting should all be grouped at the start of init, so I can understand which input are valid at a first glance.
run_game
handles both logic and drawing, separating thesnake_logic
and thesnake_drawing
would seriously increase readibility
-
\$\begingroup\$ I disagree about the
key_mapping
. It's not really a constant since the key mapping are usually there so the player can change, add, and remove key bindings. How many games do you know where you can't change the key bindings? \$\endgroup\$Mahi– Mahi2015年08月09日 20:18:13 +00:00Commented Aug 9, 2015 at 20:18 -
\$\begingroup\$ @MarkusMeskanen But in this code it is never changed, if the game is extended, it will become variable, right now it is constant \$\endgroup\$Caridorc– Caridorc2015年08月09日 20:54:49 +00:00Commented Aug 9, 2015 at 20:54
Explore related questions
See similar questions with these tags.