Python 뱀 게임 (Pygame 사용)

Oct 02 2020

그래서 이것은 제가 작업하고있는 재미있는 작은 프로젝트입니다. 이것은 현재 게임 진행 상황입니다.

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()

내가 발견 한 유일한 버그는 뱀이 몇 초마다 조금씩 지연된다는 것입니다. 왜 그런지 잘 모르겠지만 거의 눈에 띄지 않기 때문에 지금은 그대로 두었습니다.

나는 여전히 파이 게임을 처음 접했고 이것은 나의 첫 파이 게임 프로젝트 중 하나이기 때문에 코드를 검토해 주시면 정말 감사하겠습니다.

뱀과 음식에 텍스처를 추가하는 것도 고려 중이지만 어떻게 구현해야할지 모르겠습니다. 그것에 대한 의견도 감사하겠습니다!

답변

3 user985366 Oct 11 2020 at 05:38

코드는 대부분 잘 구조화되고 형식이 지정되어 있고 함수에 좋은 이름을 사용하므로 "명백한"항목은 수정할 필요가 없습니다.

논리 1 / 반복하지 마십시오

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')

여기서는 항상 똑같은 것으로 나누기 때문에 차라리

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

또한 창 크기는 이미 MARGIN의 배수이므로 처음부터 그렇게 정의하여 모든 것을 더 간단하게 만들 수 있습니다 (다소 덜 유연하게).

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

그런 다음 다른 두 개가 아닌 MARGIN 대 SNAKE_SIZE 만 확인하면됩니다.

스타일 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])

이 선은 매우 길고 따라 가거나 수정하기가 어렵습니다. 나는 파이썬 또는 일반적으로 프로그래밍의 요점이 가능한 한 적은 줄을 작성하는 것이라고 생각하지 않습니다. 이것이 읽기 쉽고 유지 관리가 가능하다고 생각되면 계속 진행하십시오. 그러나 쉽게 이해할 수 있도록 여러 줄로 나누어지는 것이 좋습니다.

스타일 2

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

여기서 부분적으로 동일한 문제입니다. 나는 생각 self.x하고 self.y훨씬 더 읽기 쉽고 self.coords[0], 특히 두 번 사용하기 때문에 rect의 크기를 별도로 지정해야합니다.

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

논리 2 / 반복하지 마십시오

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

따라서 여러 키가있을 수 있지만 마지막 하나만 저장하려고합니다. 이를 더 명확하고 덜 반복적으로 만들기 위해 먼저 키맵을 정의합니다 (이 함수 외부에서 "글로벌"또는 클래스 멤버로 정의하는 것이 더 좋을 수 있습니다.

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

그런 다음 끝부터 반복하고 첫 번째 히트를 반환하여이 코드의 의도를 명확하게하고 필요한 것보다 많은 키를 확인하지 않습니다. (속도면에서는 중요하지 않지만 더 깨끗하고 건조합니다.

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

논리 3 / 반복하지 마십시오

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

이 코드에는 비슷한 문제가 있습니다. 당신은 당신이 할 필요가 없을 때 많은 것들을 반복하고 있습니다.

각각의 4 개의 결과는 if동일 self.last_key_function = self.prev_key_function하므로 elif는 필요하지 않으며 다음과 or같은 큰 절로 다시 작성할 수 있습니다.

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

그러나 우리는 더 잘할 수 있습니다.

이 이동 함수는 숫자를 잘 사용하기 때문에 0, 1, 2, 3예를 들어 이렇게 할 수 있습니다.

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

이것이 읽기 어렵다고 생각되면 현재 or버전보다 훨씬 짧고 더 나은 버전을 사용하십시오 .

더 많은 댓글을 쓰고 싶지만 시간이 부족합니다. 도움이 되었기를 바랍니다.