Un gioco di serpente Python (usando Pygame)

Oct 02 2020

Quindi questo è un piccolo progetto divertente su cui sto lavorando. Questo è il mio attuale progresso con il gioco:

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

L'unico bug che ho notato è che il serpente è leggermente in ritardo ogni pochi secondi. Non sono sicuro del motivo per cui sta succedendo, ma dal momento che è appena percettibile, per ora l'ho lasciato così com'è.

Sono ancora davvero nuovo su pygame, e questo è uno dei miei primi progetti pygame, quindi apprezzerei davvero una revisione del mio codice.

Sto anche pensando di aggiungere trame al serpente e al cibo, ma non sono sicuro di come implementarlo. Anche qualsiasi commento su questo sarà apprezzato!

Risposte

3 user985366 Oct 11 2020 at 05:38

Il codice sembra per lo più ben strutturato, formattato e utilizza buoni nomi per le funzioni, quindi quelle cose "ovvie" non hanno bisogno di essere aggiustate.

Logica 1 / Non ripetere te stesso

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

Dato che qui dividi sempre per la stessa cosa, preferirei

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

Inoltre, poiché le dimensioni della finestra sono già un multiplo di MARGIN, potresti rendere tutto più semplice (un po 'meno flessibile) definendole in questo modo dall'inizio.

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

E poi dovresti solo controllare MARGIN vs SNAKE_SIZE, non gli altri due.

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

Queste righe sono estremamente lunghe e difficili da seguire o modificare. Non credo che il punto di Python, o della programmazione in generale, sia scrivere il minor numero di righe possibile. Se trovi questo leggibile e manutenibile, vai avanti, ma preferirei vederlo diviso su più righe per essere facilmente compreso.

Stile 2

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

In parte lo stesso problema qui. Penso self.xed self.yè molto più leggibile di self.coords[0], e dovresti nominare la dimensione del rect separatamente, in particolare poiché lo stai usando due volte.

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

Logica 2 / Non ripetere te stesso

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

Quindi potrebbero esserci più chiavi, ma vuoi salvare solo una e solo l'ultima. Per renderlo più chiaro e meno ripetitivo, definirei prima una mappa di tasti (potrebbe essere meglio definirla al di fuori di questa funzione, come un membro "globale" o di classe, qualunque cosa si adatti.

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

Quindi iterare dalla fine e tornare al primo colpo che rende chiaro cosa intende questo codice e non controlla più chiavi del necessario. (Per la velocità, probabilmente non importa, ma è più chiaro e più ASCIUTTO.

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

Logica 3 / Non ripetere te stesso

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

Questo codice presenta un problema simile. Stai ripetendo un sacco di cose quando non devi.

Il risultato di ciascuno di questi 4 ifè lo stesso, self.last_key_function = self.prev_key_functionquindi elifnon è necessario e potrebbero essere riscritti come una grande orclausola in questo modo

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

Ma possiamo fare di meglio.

Poiché queste funzioni di spostamento utilizzano bene i numeri, 0, 1, 2, 3possiamo farlo, ad esempio

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

Se pensi che sia difficile da leggere, usa la orversione, che è molto più breve e migliore di quella attuale.

Vorrei scrivere più commenti, ma il tempo è scaduto. Spero che questo sia utile.