5
\$\begingroup\$

So this is a fun little project I am working on. This is my current progress with the game:

import random
import time

import pygame

pygame.init()
pygame.font.init()
pygame.display.set_caption('Parsel Tongue')

MARGIN = 60
WINDOW_SIZE = (600, 600 + MARGIN)

RED = (255, 0, 0)
BLACK = (0, 0, 0)
GREEN = (0, 255, 100)
WHITE = (255, 255, 255)

SNAKE_SIZE = 15
SNAKE_SPEED = 1

FONT_SIZE = 30

if WINDOW_SIZE[0] % SNAKE_SIZE != 0 or WINDOW_SIZE[1] % SNAKE_SIZE != 0 or MARGIN % SNAKE_SIZE != 0:
    raise Exception('Size of grid should be divisible by SNAKE_SIZE')


class Food:
    foods = []

    def __init__(self):
        self.add_to_list()
        self.coords = None
        self.count = 0
        self.create_food()

    def add_to_list(self):
        self.foods.append(self)

    def remove_from_list(self):
        self.foods.remove(self)

    def create_food(self):
        self.count += 1
        check = set(tuple(part[0]) for snake in Snake.snakes for part in snake.body)
        self.coords = random.choice([(i, j) for i in range(0, WINDOW_SIZE[0], SNAKE_SIZE) for j in range(MARGIN, WINDOW_SIZE[1], SNAKE_SIZE) if (i, j) not in check])

    def display(self, canvas):
        pygame.draw.rect(canvas, RED, (self.coords[0] + 1, self.coords[1] + 1, SNAKE_SIZE - 2, SNAKE_SIZE - 2))


class Snake:
    snakes = []

    def __init__(self, initial_body):
        self.add_to_list()
        self.body = initial_body.copy()
        self.lock_last = False
        self.last_key_function = self.prev_key_function = -1

    def add_to_list(self):
        self.snakes.append(self)

    def remove_from_list(self):
        self.snakes.remove(self)

    def update_key_function(self, keys):
        for key in keys:
            if key in (pygame.K_w, pygame.K_UP):
                self.last_key_function = 0

            if key in (pygame.K_s, pygame.K_DOWN):
                self.last_key_function = 2

            if key in (pygame.K_a, pygame.K_LEFT):
                self.last_key_function = 1

            if key in (pygame.K_d, pygame.K_RIGHT):
                self.last_key_function = 3

    def update(self):
        if self.head_in_block():
            self.lock_last = False

            if self.last_key_function == 0:
                if not self.move_up():
                    self.last_key_function = self.prev_key_function

            elif self.last_key_function == 1:
                if not self.move_left():
                    self.last_key_function = self.prev_key_function

            elif self.last_key_function == 2:
                if not self.move_down():
                    self.last_key_function = self.prev_key_function

            elif self.last_key_function == 3:
                if not self.move_right():
                    self.last_key_function = self.prev_key_function

            self.prev_key_function = self.last_key_function

            for food in Food.foods:
                if self.body[0][0] == food.coords:
                    self.add_part(food)

        self.move()

    def add_part(self, food):
        self.lock_last = True
        self.body.append(self.body[-1].copy())
        food.create_food()

    def move(self):
        if self.last_key_function != -1:
            for part_index, (part_coords, part_velocity) in enumerate(self.body if not self.lock_last else self.body[:-1]):
                new_part_coords = (part_coords[0] + part_velocity[0], part_coords[1] + part_velocity[1])
                self.body[part_index][0] = new_part_coords

            if self.head_in_block():
                for part_index in range(len(self.body) - 1 - int(self.lock_last), 0, -1):
                    for new_part_index in range(part_index - 1, -1, -1):
                        if self.body[new_part_index][0] != self.body[part_index][0]:
                            self.body[part_index][1] = self.body[new_part_index][1]
                            break

    def move_up(self):
        if len(self.body) <= 1 or self.body[0][0][1] <= self.body[1][0][1]:
            self.body[0][1] = (0, -SNAKE_SPEED)
            return True
        return False

    def move_left(self):
        if len(self.body) <= 1 or self.body[0][0][0] <= self.body[1][0][0]:
            self.body[0][1] = (-SNAKE_SPEED, 0)
            return True
        return False

    def move_down(self):
        if len(self.body) <= 1 or self.body[0][0][1] >= self.body[1][0][1]:
            self.body[0][1] = (0, SNAKE_SPEED)
            return True
        return False

    def move_right(self):
        if len(self.body) <= 1 or self.body[0][0][0] >= self.body[1][0][0]:
            self.body[0][1] = (SNAKE_SPEED, 0)
            return True
        return False

    def head_in_block(self):
        return self.coords_in_block(self.body[0][0])

    @staticmethod
    def coords_in_block(coords):
        return coords[0] % SNAKE_SIZE == coords[1] % SNAKE_SIZE == 0

    def display(self, canvas):
        for part_index, (part_coords, part_velocity) in enumerate(self.body):
            pygame.draw.rect(canvas, WHITE, (part_coords[0] + 1, part_coords[1] + 1, SNAKE_SIZE - 2, SNAKE_SIZE - 2))

            if part_index != 0:
                while True:
                    part_coords = (part_coords[0] + part_velocity[0], part_coords[1] + part_velocity[1])
                    pygame.draw.rect(canvas, WHITE, (part_coords[0] + 1, part_coords[1] + 1, SNAKE_SIZE - 2, SNAKE_SIZE - 2))

                    if self.coords_in_block(part_coords):
                        break

            if part_index != len(self.body) - 1:
                while True:
                    part_coords = (part_coords[0] - part_velocity[0], part_coords[1] - part_velocity[1])
                    pygame.draw.rect(canvas, WHITE, (part_coords[0] + 1, part_coords[1] + 1, SNAKE_SIZE - 2, SNAKE_SIZE - 2))

                    if self.coords_in_block(part_coords):
                        break

    def collided(self):
        if not (0 <= self.body[0][0][0] < WINDOW_SIZE[0] - SNAKE_SIZE + 1) or \
                not (MARGIN <= self.body[0][0][1] < WINDOW_SIZE[1] - SNAKE_SIZE + 1):
            return True

        if self.head_in_block():
            for part_index, (part_coords, part_velocity) in enumerate(self.body[1:], 1):
                if abs(self.body[0][0][0] - part_coords[0]) < SNAKE_SIZE and \
                        abs(self.body[0][0][1] - part_coords[1]) < SNAKE_SIZE:
                    return True

        else:
            return False


class Game:
    def __init__(self):
        clock = pygame.time.Clock()
        self.canvas = pygame.display.set_mode(WINDOW_SIZE)
        self.font = pygame.font.SysFont('Arial', FONT_SIZE)

        self.finished = False
        self.lost = self.paused = False
        self.lose_time = self.pause_time = None

        self.init_head = (WINDOW_SIZE[0] // 2 // 10 * 10, WINDOW_SIZE[1] // 2 // 10 * 10)
        self.init_body = [[self.init_head, (0, 0)]]

        self.snake = Snake(self.init_body)
        self.food = Food()

        while not self.finished:
            self.canvas.fill(BLACK)
            self.__update()

            clock.tick(180)

            pygame.draw.rect(self.canvas, WHITE, ((0, MARGIN - 1), (WINDOW_SIZE[0], 1)))
            pygame.display.update()

        self.__reset()

    def __reset(self):
        self.finished = False
        self.lost = self.paused = False
        self.lose_time = self.pause_time = None

        self.init_head = (WINDOW_SIZE[0] // 2 // 10 * 10, WINDOW_SIZE[1] // 2 // 10 * 10)
        self.init_body = [[self.init_head, (0, 0)]]

        self.snake.remove_from_list()
        self.food.remove_from_list()

        self.snake = Snake(self.init_body)
        self.food = Food()

    def __update(self):
        snake_update_keys = []

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.finished = True

            if event.type == pygame.KEYDOWN:
                if not self.lost and (event.key == pygame.K_p or event.key == pygame.K_ESCAPE):
                    self.paused = not self.paused
                    self.pause_time = time.time() if self.paused else None

                snake_update_keys.append(event.key)

                if self.lost and event.key == pygame.K_r:
                    self.__reset()

        if not self.lost and self.snake.collided():
            self.lost = True
            self.lose_time = time.time()

        if not self.lost and not self.paused:
            self.snake.update_key_function(snake_update_keys)
            self.snake.update()

        self.food.display(self.canvas)
        self.snake.display(self.canvas)

        self.__update_score()
        self.__update_pause()
        self.__update_fail()

    def __update_score(self):
        self.score = self.food.count - 1
        self.score_text = self.font.render('Score: ' + str(self.score), True, GREEN)
        self.canvas.blit(self.score_text, (10, 10))

    def __update_fail(self):
        if self.lost:
            fail_text = self.font.render('You lost!', True, GREEN)
            self.canvas.blit(fail_text, (WINDOW_SIZE[0] - 10 - fail_text.get_rect().width, 10))

            if (time.time() - self.lose_time) % 1 > 0.5:
                restart = self.font.render('Press R to restart', True, GREEN)
                self.canvas.blit(restart, (WINDOW_SIZE[0] // 2 - restart.get_rect().width // 2,
                                           WINDOW_SIZE[1] // 2 - restart.get_rect().height // 2))

    def __update_pause(self):
        if self.paused:
            if (time.time() - self.pause_time) % 1 > 0.5:
                self.pause_text = self.font.render('Press P or Esc to resume', True, GREEN)
                self.canvas.blit(self.pause_text, (WINDOW_SIZE[0] - 10 - self.pause_text.get_rect().width, 10))

        elif not self.lost:
            self.pause_text = self.font.render('Press P or Esc to pause', True, GREEN)
            self.canvas.blit(self.pause_text, (WINDOW_SIZE[0] - 10 - self.pause_text.get_rect().width, 10))


game = Game()

The only bug I noticed is that the snake lags a tiny bit every few seconds. I'm not sure why that's happening, but since it's barely noticeable, I left it as it is for now.

I'm still really new to pygame, and this is one of my first pygame projects, so I'd really appreciate a review of my code.

I'm also thinking about adding textures to the snake and the food, but I'm not sure how I'd implement it. Any comments on that will also be appreciated!

\$\endgroup\$
1
  • 1
    \$\begingroup\$ absolutely love the game, just ran it on my machine \$\endgroup\$
    – user228914
    Commented Oct 11, 2020 at 3:55

1 Answer 1

2
\$\begingroup\$

The code looks mostly well structured, formatted and using good names for functions, so those "obvious" things don't need fixing.

Logic 1 / Don't repeat yourself

if WINDOW_SIZE[0] % SNAKE_SIZE != 0 or WINDOW_SIZE[1] % SNAKE_SIZE != 0 or MARGIN % SNAKE_SIZE != 0:
    raise Exception('Size of grid should be divisible by SNAKE_SIZE')

Since you're always dividing by the same thing here, I'd rather

for x in [WINDOW_SIZE[0], WINDOW_SIZE[1], MARGIN]:
    if x % SNAKE_SIZE != 0:
        raise Exception('...')

Also, since your window sizes are a multiple of MARGIN already, you could make it all simpler (somewhat less flexible) by defining them like that from the start.

MARGIN = 60
WINDOW_SIZE = (10 * MARGIN, 11 * MARGIN)

And then you would only have to check MARGIN vs SNAKE_SIZE, not the other two.

Style 1

def create_food(self):
    self.count += 1
    check = set(tuple(part[0]) for snake in Snake.snakes for part in snake.body)
    self.coords = random.choice([(i, j) for i in range(0, WINDOW_SIZE[0], SNAKE_SIZE) for j in range(MARGIN, WINDOW_SIZE[1], SNAKE_SIZE) if (i, j) not in check])

These lines are extremely long and are hard to follow or modify. I don't think the point of Python, or programming in general, is to write as few lines as possible. If you find this readable and maintainable, go ahead, but I'd rather see it split up over multiple lines to be easily understood.

Style 2

pygame.draw.rect(canvas, RED, (self.coords[0] + 1, self.coords[1] + 1, SNAKE_SIZE - 2, SNAKE_SIZE - 2))

Partly the same problem here. I think self.x and self.y is much more readable than self.coords[0] , and you should name the size of the rect separately, in particular since you're using it twice.

size = SNAKE_SIZE - 2
pygame.draw.rect(canvas, RED, (self.x + 1, self.y + 1, size, size))

Logic 2 / Don't repeat yourself

def update_key_function(self, keys):
    for key in keys:
        if key in (pygame.K_w, pygame.K_UP):
            self.last_key_function = 0

        if key in (pygame.K_s, pygame.K_DOWN):
            self.last_key_function = 2

        if key in (pygame.K_a, pygame.K_LEFT):
            self.last_key_function = 1

        if key in (pygame.K_d, pygame.K_RIGHT):
            self.last_key_function = 3

So there may be multiple keys, but you only want to save one and only the last one. To make this clearer and less repetitive, I would first define a keymap (it might be better to define it outside of this function, as a "global" or class member, whatever suits.

direction_keys = {
    pygame.K_w: 0,
    pygame.K_UP: 0,
    ...
    pygame.K_d: 3,
    pygame.K_RIGHT: 3
} 

Then iterate from the end and return on the first hit which makes it clear what this code intends, and doesn't check more keys than needed. (For speed, it probably doesn't matter, but it's clearer and DRYer.

for k in reversed(keys):
    if k in direction_keys:
        self.last_key_function = direction_keys[k]
        return

Logic 3 / Don't repeat yourself

if self.last_key_function == 0:
        if not self.move_up():
                self.last_key_function = self.prev_key_function

        elif self.last_key_function == 1:
            if not self.move_left():
                self.last_key_function = self.prev_key_function

        elif self.last_key_function == 2:
            if not self.move_down():
                self.last_key_function = self.prev_key_function

        elif self.last_key_function == 3:
            if not self.move_right():
                self.last_key_function = self.prev_key_function

This code has a similar problem. You're repeating a bunch of things when you don't have to.

The outcome of each of those 4 ifs is the same, self.last_key_function = self.prev_key_function, so the elif isn't needed and they could be rewritten as a big or clause like so

if A or B or C or D:
    self.last_key_function = self.prev_key_function

But we can do better.

Since these move functions nicely use the numbers 0, 1, 2, 3 we can for example do this

for number, func in enumerate ([self.move_up, self.move_left, self.move_down, self.move_right]):
    if self.last_key_function == number and not func():
        self.last_key_function = self.prev_key_function
        break

If you think this is hard to read, just use the or version, which is a lot shorter and better than the current one.

I would write more comments, but I'm running out of time. Hope this is helpful.

\$\endgroup\$

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