Jogo de Empurrar Caixas com Pygame

PythonBeginner
Pratique Agora

Introdução

Este projeto é o desenvolvimento do jogo clássico Sokoban utilizando a linguagem Python e a biblioteca Pygame.

Os pontos de conhecimento abordados neste projeto incluem:

  • Sintaxe básica do Python
  • Desenvolvimento básico de jogos com Pygame

Este curso tem um nível de dificuldade moderado e é adequado para usuários que possuem um conhecimento básico de Python e desejam aprofundar seus conhecimentos.

O código fonte sokoban.py.zip é lançado sob a licença GNU GPL v3, e a skin foi criada por Borgar.

👀 Pré-visualização

Animação de pré-visualização do jogo Sokoban

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como inicializar o jogo usando Pygame
  • Como lidar com eventos do jogo e operações de teclado
  • Como implementar o mapa do jogo
  • Como implementar operações de movimento para o jogador e as caixas
  • Como implementar operações de desfazer e refazer
  • Como testar a interface do jogo

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Inicializar o Pygame e configurar a janela do jogo
  • Lidar com eventos do jogo e entradas de teclado no Pygame
  • Implementar o mapa do jogo e exibi-lo usando Pygame
  • Implementar operações de movimento para o jogador e as caixas
  • Implementar operações de desfazer e refazer no jogo
  • Testar e executar a interface do jogo

Descrição do Jogo

No jogo Sokoban, existe uma parede fechada que forma uma área poligonal irregular. O jogador e as caixas só podem se mover dentro desta área. Dentro da área, há uma pessoa, várias caixas e pontos de destino. O objetivo do jogo é usar as setas do teclado para controlar o movimento da pessoa e empurrar as caixas para os pontos de destino. Apenas uma caixa pode ser movida por vez, e se uma caixa ficar presa em um canto, o jogo não poderá continuar.

Personagens

A partir da descrição acima, podemos abstrair os seguintes personagens no jogo:

  1. Paredes: Áreas fechadas que bloqueiam os caminhos de movimento.
  2. Espaços: Áreas onde a pessoa pode andar e empurrar caixas.
  3. Pessoa: O personagem controlado pelo jogador.
  4. Caixas
  5. Pontos de destino

A pessoa, as caixas e os pontos de destino devem ser inicializados dentro da área de espaço, e outros personagens não devem aparecer dentro da área da parede.

Controles

No jogo Sokoban, o único personagem que podemos controlar é a pessoa. Usamos as setas do teclado para controlar o movimento da pessoa, tanto para mover a pessoa quanto para empurrar as caixas. Existem dois tipos de movimentos para a pessoa, e precisamos lidar com cada caso separadamente:

  1. Mover a pessoa sozinha
  2. Mover a pessoa enquanto empurra uma caixa

Além disso, o jogo suporta as seguintes duas operações:

  1. Desfazer (Undo): Desfazer o movimento anterior, controlado pela tecla Backspace.
  2. Refazer (Redo): Refazer o movimento desfeito anteriormente, controlado pela barra de espaço.

Em resumo, precisamos suportar os eventos de teclado para as quatro setas, a tecla Backspace para desfazer e a barra de espaço para refazer. Na próxima seção de implementação do Pygame, precisaremos lidar com esses seis eventos de teclado.

Preparação para o Desenvolvimento

Para poder usar o Pygame no ambiente, abra o terminal no ambiente experimental e digite o seguinte comando para instalar o Pygame:

sudo pip install pygame

Existem muitos módulos no Pygame, incluindo mouse, dispositivos de exibição, gráficos, eventos, fontes, imagens, teclados, som, vídeo, áudio, etc. No jogo Sokoban, usaremos os seguintes módulos:

  • pygame.display: Acessa dispositivos de exibição para exibir imagens.
  • pygame.image: Carrega e armazena imagens, usado para lidar com sprite sheets.
  • pygame.key: Lê as entradas do teclado.
  • pygame.event: Gerencia eventos, lida com eventos de teclado no jogo.
  • pygame.time: Gerencia o tempo e exibe informações de quadros.

A introdução acima mencionou sprite sheets. Sprite sheet é um método comum de mesclagem de imagens no desenvolvimento de jogos, que mescla pequenos ícones e imagens de fundo em uma única imagem e, em seguida, usa o posicionamento de imagem do Pygame para exibir a parte necessária da imagem.

No jogo Sokoban, usamos um sprite sheet pronto. Não entrarei em detalhes sobre como recortar imagens e mesclar sprite sheets aqui, pois existem inúmeros métodos disponíveis online.

Os elementos de imagem no sprite sheet do Sokoban usados neste projeto são de borgar, e o arquivo pode ser encontrado em ~/project/borgar.png.

Os elementos de imagem do jogo incluem:

  • Cor de fundo da interface do jogo
  • Jogador
  • Caixa normal
  • Ponto de destino
  • Efeito de sobreposição do jogador e ponto de destino
  • Efeito de sobreposição da caixa atingindo o ponto de destino
  • Parede

Duas imagens de caixa no sprite sheet não são necessárias em nossa implementação. Explicaremos em detalhes como usar o método blit no Pygame para carregar e exibir o conteúdo do sprite sheet na parte de implementação subsequente.

Desenvolvimento do Jogo

Primeiro, crie um arquivo sokoban.py no diretório ~/project e, em seguida, insira o seguinte conteúdo no arquivo:

  1. Inicialize o Pygame
import pygame, sys, os
from pygame.locals import *

from collections import deque


pygame.init()
  1. Defina o objeto de exibição
## Defina o tamanho da janela de exibição do pygame para 400 pixels de largura e 300 pixels de altura
screen = pygame.display.set_mode((400,300))
  1. Carregue os elementos da imagem
## Carregue os elementos da imagem de um único arquivo
skinfilename = os.path.join('borgar.png')

try:
    skin = pygame.image.load(skinfilename)
except pygame.error as msg:
    print('cannot load skin')
    raise SystemExit(msg)

skin = skin.convert()

## Defina a cor de fundo da janela para o elemento nas coordenadas (0,0) no arquivo skin
screen.fill(skin.get_at((0,0)))
  1. Defina o relógio e o tempo de repetição para eventos de teclado. Use key.set_repeat para definir o intervalo de tempo para eventos de repetição com os parâmetros (delay, interval).
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. Inicie o loop principal
## Loop principal do jogo
while True:
    clock.tick(60)
    pass
  1. Lide com eventos do jogo e operações do teclado. No loop principal, precisamos lidar com eventos de teclado, como mencionado anteriormente, precisamos suportar seis teclas: cima, baixo, esquerda, direita, backspace e espaço.
## Obtenha eventos do jogo
for event in pygame.event.get():
    ## Evento de sair do jogo
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## Operação do teclado
    elif event.type == KEYDOWN:
        ## Mover para a esquerda
        if event.key == K_LEFT:
            pass
        ## Mover para cima
        elif event.key == K_UP:
            pass
        ## Mover para a direita
        elif event.key == K_RIGHT:
            pass
        ## Mover para baixo
        elif event.key == K_DOWN:
            pass
        ## Operação desfazer (Undo)
        elif event.key == K_BACKSPACE:
            pass
        ## Operação refazer (Redo)
        elif event.key == K_SPACE:
            pass

Agora, concluímos a estrutura do jogo baseada em Pygame. Vamos começar a implementar a lógica do jogo.

Implementação do Mapa

Primeiro, precisamos definir o objeto Sokoban. Usamos uma classe para conter toda a lógica relacionada ao jogo.

class Sokoban:

    ## Inicializa o jogo Sokoban
    def __init__(self):
        pass

O jogo Sokoban requer uma área operacional, que é a área do mapa. Usamos uma lista de caracteres para representar o mapa, onde diferentes caracteres representam diferentes elementos no jogo:

  1. Parede: ## símbolo
  2. Espaço: - símbolo
  3. Jogador: @ símbolo
  4. Caixa: $ símbolo
  5. Ponto de destino: . símbolo`
  6. Jogador no ponto de destino: + símbolo
  7. Caixa no ponto de destino: * símbolo

Quando o jogo começa, precisamos definir uma lista de caracteres padrão para o mapa. Ao mesmo tempo, precisamos saber a largura e a altura do mapa para gerar um mapa 2D a partir desta lista unidimensional.

A representação do mapa é semelhante ao código a seguir. Você consegue imaginar como seria depois de iniciar com base neste código?

class Sokoban:

    ## Inicializa o jogo Sokoban
    def __init__(self):
        ## Define o mapa
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## Define a largura e a altura do mapa e a posição do jogador no mapa (valor do índice na lista do mapa)
        ## Total de 19 colunas
        self.w = 19

        ## Total de 11 linhas
        self.h = 11

        ## A posição inicial do jogador está em self.level[163]
        self.man = 163

O mapa é exibido digitalizando a lista de caracteres e exibindo diferentes elementos nas posições correspondentes com base nos caracteres.

Como a exibição é 2D, a largura e a altura são usadas para determinar a posição de cada caractere na área de exibição 2D. Precisamos passar screen e skin mencionados no Pygame como parâmetros para a função de desenho draw.

É importante notar que a função de desenho que implementamos usa blit do Pygame, que extrai a imagem do sprite sheet e a exibe na posição especificada:

screen.blit(skin, (i*w, j*w), (0,0,w,w))

A implementação completa da função draw é a seguinte. Primeiro, a varredura é realizada e, em seguida, a imagem correspondente a cada caractere é exibida com base no sprite sheet:

class Sokoban:

    ## Desenha o mapa na janela do pygame com base no nível do mapa
    def draw(self, screen, skin):

        ## Obtém a largura de cada elemento da imagem
        w = skin.get_width() / 4

        ## Itera por cada elemento de caractere no nível do mapa
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## Obtém o caractere na j-ésima linha e i-ésima coluna no mapa
                item = self.level[j*self.w + i]

                ## Exibe como uma parede(#) nesta posição
                if item == '#':
                    ## Use o método blit do pygame para exibir a imagem na posição especificada,
                    ## com as coordenadas da posição (i*w, j*w) e as coordenadas e comprimento-largura da imagem na skin como (0,2*w,w,w)
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## Exibe como um espaço(-) nesta posição
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## Exibe como um jogador(@) nesta posição
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## Exibe como uma caixa($) nesta posição
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## Exibe como um ponto de destino(.) nesta posição
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## Exibe como o jogador em um efeito de ponto de destino
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## Exibe como a caixa colocada em um efeito de ponto de destino
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))

Implementando a Operação de Movimento

A operação de movimento usa as setas do teclado para controlar o movimento em quatro direções: esquerda, direita, cima e baixo. Usamos quatro caracteres 'l' (esquerda), 'r' (direita), 'u' (cima) e 'd' (baixo) para especificar a direção do movimento.

Como o processo necessário para a operação de refazer (redo) e a operação de movimento é semelhante, definimos uma função interna, _move(), para lidar com o movimento na classe Sokoban:

class Sokoban:

    ## Função interna de movimento: usada para atualizar as mudanças de posição dos elementos no mapa após a operação de movimento, onde d representa a direção do movimento
    def _move(self, d):
        ## Obtém o deslocamento no mapa para o movimento
        h = get_offset(d, self.w)

        ## Se a área de destino do movimento for espaço vazio ou um ponto de destino, apenas o jogador precisa se mover
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## Move o jogador para a posição de destino
            move_man(self.level, self.man + h)
            ## Define a posição original do jogador após o movimento
            move_floor(self.level, self.man)
            ## A nova posição do jogador
            self.man += h
            ## Adiciona a operação de movimento à solução
            self.solution += d

        ## Se a área de destino do movimento for uma caixa, tanto a caixa quanto o jogador precisam se mover
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## O deslocamento da caixa e a posição do jogador
            h2 = h * 2
            ## A caixa só pode ser movida se a próxima posição for espaço vazio ou um ponto de destino
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## Move a caixa para o ponto de destino
                move_box(self.level, self.man + h2)
                ## Move o jogador para o ponto de destino
                move_man(self.level, self.man + h)
                ## Redefine a posição atual do jogador
                move_floor(self.level, self.man)
                ## Define a nova posição do jogador
                self.man += h
                ## Marca a operação de movimento como um caractere maiúsculo para indicar que uma caixa foi empurrada nesta etapa
                self.solution += d.upper()
                ## Incrementa o número de etapas para empurrar a caixa
                self.push += 1

Na função _move, precisamos usar as seguintes funções:

  • get_offset(d, width): Obtém o deslocamento do movimento no mapa. d representa a direção do movimento e width representa a largura da janela do jogo.
  • move_man(level, i): Move a posição do jogador no mapa. level é a lista do mapa e i é a posição do jogador.
  • move_floor(level, i): Redefine a posição após o movimento. Depois que o jogador se move de uma posição, ela precisa ser redefinida como espaço vazio ou um ponto de destino.
  • move_box(level, i): Move a posição da caixa no mapa. level é a lista do mapa e i é a posição da caixa.

A implementação dessas funções pode ser vista no código completo. É importante considerar qual é o elemento original na posição de destino ao mover cada elemento para determinar qual elemento deve ser definido após o movimento.

Para realizar a operação de movimento, basta chamar _move e definir todo[] como vazio (a lista de refazer é ativada apenas ao realizar operações de desfazer).

Implementar Desfazer (Undo)

Desfazer (Undo) é a operação reversa de um movimento. Ele recupera a etapa anterior de solution e realiza a operação reversa. Veja o código detalhado:

class Sokoban:

    ## Operação de desfazer: desfaz o movimento anterior
    def undo(self):
        ## Verifica se há um registro de movimento
        if self.solution.__len__()>0:
            ## Armazena o registro de movimento na lista todo para a operação de refazer (redo)
            self.todo.append(self.solution[-1])
            ## Exclui o registro de movimento
            self.solution.pop()

            ## Obtém o deslocamento a ser movido para a operação de desfazer: o negativo do deslocamento do último movimento
            h = get_offset(self.todo[-1],self.w) * -1

            ## Verifica se esta operação move apenas o personagem sem empurrar uma caixa
            if self.todo[-1].islower():
                ## Move o personagem de volta para sua posição original
                move_man(self.level, self.man + h)
                ## Define a posição atual do personagem
                move_floor(self.level, self.man)
                ## Define a posição do personagem no mapa
                self.man += h
            else:
                ## Se esta etapa empurra uma caixa, move o personagem, a caixa e realiza operações relacionadas em _move
                move_floor(self.level, self.man - h)
                move_box(self.level, self.man)
                move_man(self.level, self.man + h)
                self.man += h
                self.push -= 1

Operação Refazer (Redo)

Quando o comando desfazer (undo) é executado, o conteúdo é movido de solution[] para todo[], e só precisamos extrair e chamar a função _move.

    ## Operação refazer: Quando a operação desfazer é executada e ativada, move de volta para a posição antes do desfazer
    def redo(self):
        ## Verifica se há uma operação desfazer registrada
        if self.todo.__len__() > 0:
            ## Move de volta as etapas desfeitas
            self._move(self.todo[-1].lower())
            ## Exclui este registro
            self.todo.pop()

Com as etapas acima, o conteúdo principal do jogo foi concluído. Por favor, continue a completar independentemente o código completo do jogo, teste as capturas de tela e faça quaisquer perguntas na seção de Perguntas e Respostas da Sala de Experimentos (Experiment Room) se tiver algum ponto obscuro. A equipe da Sala de Experimentos e os professores responderão prontamente a quaisquer perguntas que você possa ter.

Funções Adicionais e Refatoração de Código

Agora temos um jogo básico, mas ele não é perfeito. Precisamos adicionar algumas funções adicionais para torná-lo mais jogável.

Também precisamos refatorar o código para torná-lo mais legível e fácil de manter.

Clique para ver o código completo
import pygame, sys, os
from pygame.locals import *

from collections import deque


def to_box(level, index):
    if level[index] == "-" or level[index] == "@":
        level[index] = "$"
    else:
        level[index] = "*"


def to_man(level, i):
    if level[i] == "-" or level[i] == "$":
        level[i] = "@"
    else:
        level[i] = "+"


def to_floor(level, i):
    if level[i] == "@" or level[i] == "$":
        level[i] = "-"
    else:
        level[i] = "."


def to_offset(d, width):
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    return d4[m4.index(d.lower())]


def b_manto(level, width, b, m, t):
    maze = list(level)
    maze[b] = "#"
    if m == t:
        return 1
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ["-", "."]:
                if newpos == t:
                    return 1
                maze[newpos] = i
                queue.append(newpos)
    return 0


def b_manto_2(level, width, b, m, t):
    maze = list(level)
    maze[b] = "#"
    maze[m] = "@"
    if m == t:
        return []
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ["-", "."]:
                maze[newpos] = i
                queue.append(newpos)
                if newpos == t:
                    path = []
                    while maze[t] != "@":
                        path.append(m4[maze[t]])
                        t = t - d4[maze[t]]
                    return path

    return []


class Sokoban:
    def __init__(self):
        self.level = list(
            "----#####--------------#---#--------------#$--#------------###--$##-----------#--$-$-#---------###-#-##-#---#######---#-##-#####--..##-$--$----------..######-###-#@##--..#----#-----#########----#######--------"
        )
        self.w = 19
        self.h = 11
        self.man = 163
        self.hint = list(self.level)
        self.solution = []
        self.push = 0
        self.todo = []
        self.auto = 0
        self.sbox = 0
        self.queue = []

    def draw(self, screen, skin):
        w = skin.get_width() / 4
        offset = (w - 4) / 2
        for i in range(0, self.w):
            for j in range(0, self.h):
                if self.level[j * self.w + i] == "#":
                    screen.blit(skin, (i * w, j * w), (0, 2 * w, w, w))
                elif self.level[j * self.w + i] == "-":
                    screen.blit(skin, (i * w, j * w), (0, 0, w, w))
                elif self.level[j * self.w + i] == "@":
                    screen.blit(skin, (i * w, j * w), (w, 0, w, w))
                elif self.level[j * self.w + i] == "$":
                    screen.blit(skin, (i * w, j * w), (2 * w, 0, w, w))
                elif self.level[j * self.w + i] == ".":
                    screen.blit(skin, (i * w, j * w), (0, w, w, w))
                elif self.level[j * self.w + i] == "+":
                    screen.blit(skin, (i * w, j * w), (w, w, w, w))
                elif self.level[j * self.w + i] == "*":
                    screen.blit(skin, (i * w, j * w), (2 * w, w, w, w))
                if self.sbox != 0 and self.hint[j * self.w + i] == "1":
                    screen.blit(
                        skin, (i * w + offset, j * w + offset), (3 * w, 3 * w, 4, 4)
                    )

    def move(self, d):
        self._move(d)
        self.todo = []

    def _move(self, d):
        self.sbox = 0
        h = to_offset(d, self.w)
        h2 = 2 * h
        if self.level[self.man + h] == "-" or self.level[self.man + h] == ".":
            ## move
            to_man(self.level, self.man + h)
            to_floor(self.level, self.man)
            self.man += h
            self.solution += d
        elif self.level[self.man + h] == "*" or self.level[self.man + h] == "$":
            if self.level[self.man + h2] == "-" or self.level[self.man + h2] == ".":
                ## push
                to_box(self.level, self.man + h2)
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
                self.solution += d.upper()
                self.push += 1

    def undo(self):
        if self.solution.__len__() > 0:
            self.todo.append(self.solution[-1])
            self.solution.pop()

            h = to_offset(self.todo[-1], self.w) * -1
            if self.todo[-1].islower():
                ## undo a move
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
            else:
                ## undo a push
                to_floor(self.level, self.man - h)
                to_box(self.level, self.man)
                to_man(self.level, self.man + h)
                self.man += h
                self.push -= 1

    def redo(self):
        if self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()

    def manto(self, x, y):
        maze = list(self.level)
        maze[self.man] = "@"
        queue = deque([])
        queue.append(self.man)
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        while len(queue) > 0:
            pos = queue.popleft()
            for i in range(4):
                newpos = pos + d4[i]
                if maze[newpos] in ["-", "."]:
                    maze[newpos] = i
                    queue.append(newpos)

        t = y * self.w + x
        if maze[t] in range(4):
            self.todo = []
            while maze[t] != "@":
                self.todo.append(m4[maze[t]])
                t = t - d4[maze[t]]

        self.auto = 1

    def automove(self):
        if self.auto == 1 and self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()
        else:
            self.auto = 0

    def boxhint(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, b)
        to_floor(maze, self.man)
        mark = maze * 4
        size = self.w * self.h
        self.queue = []
        head = 0
        for i in range(4):
            if b_manto(maze, self.w, b, self.man, b + d4[i]):
                if len(self.queue) == 0:
                    self.queue.append((b, i, -1))
                mark[i * size + b] = "1"

        while head < len(self.queue):
            pos = self.queue[head]
            head += 1

            for i in range(4):
                if mark[pos[0] + i * size] == "1" and maze[pos[0] - d4[i]] in [
                    "-",
                    ".",
                ]:
                    if mark[pos[0] - d4[i] + i * size] != "1":
                        self.queue.append((pos[0] - d4[i], i, head - 1))
                        for j in range(4):
                            if b_manto(
                                maze,
                                self.w,
                                pos[0] - d4[i],
                                pos[0],
                                pos[0] - d4[i] + d4[j],
                            ):
                                mark[j * size + pos[0] - d4[i]] = "1"
        for i in range(size):
            self.hint[i] = "0"
            for j in range(4):
                if mark[j * size + i] == "1":
                    self.hint[i] = "1"

    def boxto(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        om4 = ["r", "d", "l", "u"]
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, self.sbox)
        to_floor(
            maze, self.man
        )  ## make a copy of working maze by removing the selected box and the man
        for i in range(len(self.queue)):
            if self.queue[i][0] == b:
                self.todo = []
                j = i
                while self.queue[j][2] != -1:
                    self.todo.append(om4[self.queue[j][1]].upper())
                    k = self.queue[j][2]
                    if self.queue[k][2] != -1:
                        self.todo += b_manto_2(
                            maze,
                            self.w,
                            self.queue[k][0],
                            self.queue[k][0] + d4[self.queue[k][1]],
                            self.queue[k][0] + d4[self.queue[j][1]],
                        )
                    else:
                        self.todo += b_manto_2(
                            maze,
                            self.w,
                            self.queue[k][0],
                            self.man,
                            self.queue[k][0] + d4[self.queue[j][1]],
                        )
                    j = k

                self.auto = 1
                return
        print("not found!")

    def mouse(self, x, y):
        if x >= self.w or y >= self.h:
            return
        m = y * self.w + x
        if self.level[m] in ["-", "."]:
            if self.sbox == 0:
                self.manto(x, y)
            else:
                self.boxto(x, y)
        elif self.level[m] in ["$", "*"]:
            if self.sbox == m:
                self.sbox = 0
            else:
                self.sbox = m
                self.boxhint(x, y)
        elif self.level[m] in ["-", ".", "@", "+"]:
            self.boxto(x, y)


## start pygame
pygame.init()
screen = pygame.display.set_mode((400, 300))

## load skin
skinfilename = os.path.join("borgar.png")
try:
    skin = pygame.image.load(skinfilename)
except pygame.error as msg:
    print("cannot load skin")
    raise SystemExit(msg)
skin = skin.convert()

## screen.fill((255,255,255))
screen.fill(skin.get_at((0, 0)))
pygame.display.set_caption("sokoban.py")

## create Sokoban object
skb = Sokoban()
skb.draw(screen, skin)

clock = pygame.time.Clock()
pygame.key.set_repeat(200, 50)

## main game loop
while True:
    clock.tick(60)

    if skb.auto == 0:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if event.key == K_LEFT:
                    skb.move("l")
                    skb.draw(screen, skin)
                elif event.key == K_UP:
                    skb.move("u")
                    skb.draw(screen, skin)
                elif event.key == K_RIGHT:
                    skb.move("r")
                    skb.draw(screen, skin)
                elif event.key == K_DOWN:
                    skb.move("d")
                    skb.draw(screen, skin)
                elif event.key == K_BACKSPACE:
                    skb.undo()
                    skb.draw(screen, skin)
                elif event.key == K_SPACE:
                    skb.redo()
                    skb.draw(screen, skin)
            elif event.type == MOUSEBUTTONUP and event.button == 1:
                mousex, mousey = event.pos
                mousex /= skin.get_width() / 4
                mousey /= skin.get_width() / 4
                skb.mouse(mousex, mousey)
                skb.draw(screen, skin)
    else:
        skb.automove()
        skb.draw(screen, skin)

    pygame.display.update()
    pygame.display.set_caption(
        skb.solution.__len__().__str__() + "/" + skb.push.__str__() + " - sokoban.py"
    )

Execução e Testes

Para executar no terminal:

cd ~/project
python sokoban.py

Se tudo estiver normal, você verá a seguinte interface do jogo:

Sokoban game interface preview

Resumo

Este projeto implementou apenas uma funcionalidade básica de um jogo Sokoban. Com base no experimento, pode-se considerar a expansão deste código por meio de:

  1. Descobrir como extrair os dados do mapa do código escrito e salvá-los em um arquivo.
  2. Implementar controles do mouse para mover rapidamente o personagem para uma posição específica.
  3. Desenvolver um algoritmo para determinar automaticamente se um mapa é solucionável.
✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar