The usual way that "snake" games work is that when the snake eats some food, it does not grow a new tail segment immediately. Instead, it waits one tickuntil the next time it moves and grows a new tail segment in the position where its old tail used to be. This is easily implemented by incrementing a counter each time the snake eats food:
The Python style guide (PEP8) says that method names should "Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability". So you should consider writing
collidesWithSelfinstead ofrenamingcollidesWithSelfascollides_with_selfand so on. (You're not obliged to follow PEP8 but it makes it easier for other Python programmers to read your code.)The organization of the code into the files
game.pyandclasses.pydoesn't seem to be motivated by any principle, and the vague nameclasses.pyconfirms this. For a small game like this, I don't think there's anything to be lost by putting all the code in one file. (And if it grows to the point where you want to split it, the obvious thing to do would be to put theSnakeclass in its own module.)It's not clear to me what you gain from separating your game initialization code into
mainandSnakeGame.__init__. Why not put all the setupinitialization into the latter?You don't need
sys.exit()at the end ofmain: Python quits automatically when it finishes running your program. Adding this line just makes it hard to test your program from the interactive interpreter, because when you quit from the game, it exits the interactive interpreter too.You have unnecessary parentheses in many places. A line like:
(headX, headY) = self.getHead()
from collections import deque
import pygame
from random import randrange
import sys
from pygame.locals import *
class Vector(tuple):
"""A tuple that supports some vector operations.
>>> v, w = Vector((1, 2)), Vector((3, 4))
>>> v + w, w - v, v * 10, 100 * v, -v
((4, 6), (2, 2), (10, 20), (100, 200), (-1, -2))
"""
def __add__(self, other): return Vector(v + w for v, w in zip(self, other))
def __radd__(self, other): return Vector(w + v for v, w in zip(self, other))
def __sub__(self, other): return Vector(v - w for v, w in zip(self, other))
def __rsub__(self, other): return Vector(w - v for v, w in zip(self, other))
def __mul__(self, s): return Vector(v * s for v in self)
def __rmul__(self, s): return Vector(v * s for v in self)
def __neg__(self): return -1 * self
FPS = 60 # Game frames per second
SEGMENT_SCORE = 50 # Score per segment
SNAKE_SPEED_INITIAL = 4.0 # Initial snake speed (squares per second)
SNAKE_SPEED_INCREMENT = 0.25 # Snake speeds up this much each time it grows
SNAKE_START_LENGTH = 4 # Initial snake length in segments
WORLD_SIZE = Vector((20, 20)) # World size, in blocks
BLOCK_SIZE = 24 # Block size, in pixels
BACKGROUND_COLOR = 45, 45, 45
SNAKE_COLOR = 0, 255, 0
FOOD_COLOR = 255, 0, 0
DEATH_COLOR = 255, 0, 0
TEXT_COLOR = 255, 255, 255
DIRECTION_UP = Vector(( 0, -1))
DIRECTION_DOWN = Vector(( 0, 1))
DIRECTION_LEFT = Vector((-1, 0))
DIRECTION_RIGHT = Vector(( 1, 0))
DIRECTION_DR = DIRECTION_DOWN + DIRECTION_RIGHT
# Map from PyGame key event to the corresponding direction.
KEY_DIRECTION = {
K_w: DIRECTION_UP, K_UP: DIRECTION_UP,
K_s: DIRECTION_DOWN, K_DOWN: DIRECTION_DOWN,
K_a: DIRECTION_LEFT, K_LEFT: DIRECTION_LEFT,
K_d: DIRECTION_RIGHT, K_RIGHT: DIRECTION_RIGHT,
}
# Snake class
class Snake(object):
def __init__(self, start, start_length):
self.speed = SNAKE_SPEED_INITIAL # Speed in squares per second.
self.timer = 1.0 / self.speed # Time remaining to next movement.
self.growth_pending = 0 # Number of segments still to grow.
self.direction = DIRECTION_UP # Current movement direction.
self.segments = deque([start - self.direction * i
for i in xrange(start_length)])
def __iter__(self):
return iter(self.segments)
def __len__(self):
return len(self.segments)
def change_direction(self, direction):
"""Update the direction of the snake."""
# Moving in the opposite direction of current movement is not allowed.
if self.direction != -direction:
self.direction = direction
def head(self):
"""Return the position of the snake's head."""
return self.segments[0]
def update(self, dt, direction):
"""Update the snake by dt seconds and possibly set direction."""
self.timer -= dt
if self.timer > 0:
# Nothing to do yet.
return
# Moving in the opposite direction of current movement is not allowed.
if self.direction != -direction:
self.direction = direction
self.timer += 1 / self.speed
# Add a new head.
self.segments.appendleft(self.head() + self.direction)
if self.growth_pending > 0:
self.growth_pending -= 1
else:
# Remove tail.
self.segments.pop()
def grow(self):
"""Grow snake by one segment and speed up."""
self.growth_pending += 1
self.speed += SNAKE_SPEED_INCREMENT
def self_intersecting(self):
"""Is the snake currently self-intersecting?"""
it = iter(self.segments)
head = next(it)
return head in it
class SnakeGame(object):
def __init__(self):
pygame.display.set_caption('PyGame Snake')
self.block_size = BLOCK_SIZE
self.window = pygame.display.set_mode(WORLD_SIZE * self.block_size)
self.screen = pygame.display.get_surface()
self.clock = pygame.time.Clock()
self.font = pygame.font.Font('freesansbold.ttf', 20)
self.world = Rect((0, 0), WORLD_SIZE)
self.reset()
def reset(self):
"""Start a new game."""
self.playing = True
self.next_direction = DIRECTION_UP
self.score = 0
self.snake = Snake(self.world.center, SNAKE_START_LENGTH)
self.food = set()
self.add_food()
def add_food(self):
"""Place"""Ensure athat newthere is at least one piece of food.
on a random empty square (And, with small probability, more than one.)
"""
while lennot (self.food) == 0 or notand randrange(4)):
food = Vector(map(randrange, self.world.bottomright))
if food not in self.food and food not in self.snake.segments:
self.food.add(food)
def input(self, e):
"""Process keyboard event e."""
if e.key in KEY_DIRECTION:
self.next_direction = KEY_DIRECTION[e.key]
elif e.key == K_SPACE and not self.playing:
self.reset()
def update(self, dt):
"""Update the game by dt seconds."""
self.snake.update(dt, self.next_direction)
# If snake hits a food block, then consume the food, add new
# food and grow the snake.
head = self.snake.head()
if head in self.food:
self.food.remove(head)
self.add_food()
self.snake.grow()
self.score += len(self.snake.segments) * SEGMENT_SCORE
# If snake collides with self or the screen boundaries, then
# it's game over.
if self.snake.self_intersecting() or not self.world.collidepoint(self.snake.head()):
self.playing = False
def block(self, p):
"""Return the screen rectangle corresponding to the position p."""
return Rect(p * self.block_size, DIRECTION_DR * self.block_size)
def draw_text(self, text, p):
"""Draw text at position p."""
self.screen.blit(self.font.render(text, 1, TEXT_COLOR), p)
def draw(self):
"""Draw game (while playing)."""
self.screen.fill(BACKGROUND_COLOR)
for p in self.snake.segments:
pygame.draw.rect(self.screen, SNAKE_COLOR, self.block(p))
for f in self.food:
pygame.draw.rect(self.screen, FOOD_COLOR, self.block(f))
self.draw_text("Score: {}".format(self.score), (20, 20))
def draw_death(self):
"""Draw game (after game over)."""
self.screen.fill(DEATH_COLOR)
self.draw_text("Game over! Press Space to start a new game", (20, 150))
self.draw_text("Your score is: {}".format(self.score), (140, 180))
def play(self):
"""Play game until the QUIT event is received."""
while True:
dt = self.clock.tick(FPS) / 1000.0 # convert to seconds
for e in pygame.event.get():
if e.type == QUIT:
return
elif e.type == KEYUP:
self.input(e)
if self.playing:
self.update(dt)
self.draw()
else:
self.draw_death()
pygame.display.flip()
def main():
pygame.init()
SnakeGame().play()
pygame.quit()