4
\$\begingroup\$

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()
\$\endgroup\$
2
\$\begingroup\$

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 the snake_logic and the snake_drawing would seriously increase readibility

\$\endgroup\$
  • \$\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\$ – Markus Meskanen Aug 9 '15 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 Aug 9 '15 at 20:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.