Game ular Python (menggunakan Pygame)

Oct 02 2020

Jadi ini adalah proyek kecil yang menyenangkan yang sedang saya kerjakan. Ini adalah kemajuan saya saat ini dengan permainan:

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

Satu-satunya bug yang saya perhatikan adalah ular itu tertinggal sedikit setiap beberapa detik. Saya tidak yakin mengapa itu terjadi, tetapi karena hampir tidak terlihat, saya membiarkannya seperti sekarang.

Saya masih benar-benar baru mengenal pygame, dan ini adalah salah satu proyek pygame pertama saya, jadi saya akan sangat menghargai review kode saya.

Saya juga berpikir untuk menambahkan tekstur pada ular dan makanan, tapi saya tidak yakin bagaimana saya akan menerapkannya. Setiap komentar tentang itu juga akan dihargai!

Jawaban

3 user985366 Oct 11 2020 at 05:38

Kode terlihat sebagian besar terstruktur dengan baik, diformat dan menggunakan nama yang baik untuk fungsi, sehingga hal-hal yang "jelas" tidak perlu diperbaiki.

Logika 1 / Jangan ulangi dirimu sendiri

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

Karena Anda selalu membagi dengan hal yang sama di sini, saya lebih suka

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

Selain itu, karena ukuran jendela Anda sudah merupakan kelipatan MARGIN, Anda dapat membuatnya lebih sederhana (agak kurang fleksibel) dengan mendefinisikannya seperti itu dari awal.

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

Dan kemudian Anda hanya perlu memeriksa MARGIN vs SNAKE_SIZE, bukan dua lainnya.

Gaya 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])

Garis-garis ini sangat panjang dan sulit diikuti atau dimodifikasi. Saya tidak berpikir inti dari Python, atau pemrograman secara umum, adalah menulis baris sesedikit mungkin. Jika Anda merasa ini dapat dibaca dan dipelihara, lanjutkan, tetapi saya lebih suka melihatnya dibagi menjadi beberapa baris agar mudah dipahami.

Gaya 2

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

Sebagian masalahnya sama di sini. Saya pikir self.xdan self.yjauh lebih mudah dibaca daripada self.coords[0], dan Anda harus menyebutkan ukuran persegi secara terpisah, khususnya karena Anda menggunakannya dua kali.

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

Logika 2 / Jangan ulangi diri Anda sendiri

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

Jadi mungkin ada beberapa kunci, tetapi Anda hanya ingin menyimpan satu dan hanya yang terakhir. Untuk membuat ini lebih jelas dan tidak berulang, pertama-tama saya akan mendefinisikan sebuah peta kunci (mungkin lebih baik untuk mendefinisikannya di luar fungsi ini, sebagai anggota "global" atau kelas, apa pun yang cocok.

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

Kemudian lakukan iterasi dari akhir dan kembali pada klik pertama yang menjelaskan maksud kode ini, dan tidak memeriksa lebih banyak kunci daripada yang diperlukan. (Untuk kecepatan, mungkin tidak masalah, tapi lebih jelas dan KERING.

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

Logika 3 / Jangan ulangi dirimu sendiri

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

Kode ini memiliki masalah yang serupa. Anda mengulangi banyak hal saat Anda tidak perlu melakukannya.

Hasil dari masing-masing 4 ifdetik tersebut adalah sama self.last_key_function = self.prev_key_function, jadi eliftidak diperlukan dan dapat ditulis ulang menjadi orklausa besar seperti itu

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

Tapi kita bisa lebih baik.

Karena fungsi pemindahan ini dengan baik menggunakan angka, 0, 1, 2, 3kita bisa misalnya melakukan ini

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

Jika menurut Anda ini sulit untuk dibaca, gunakan saja orversinya, yang jauh lebih pendek dan lebih baik daripada versi saat ini.

Saya akan menulis lebih banyak komentar, tetapi saya kehabisan waktu. Semoga bermanfaat.