Juego de Caja Empujable con Pygame

PythonPythonBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Este proyecto es el desarrollo del clásico juego Sokoban utilizando el lenguaje de programación Python y la librería Pygame.

Los conceptos cubiertos en este proyecto son:

  • Sintaxis básica de Python
  • Desarrollo básico de juegos con Pygame

Este curso tiene un nivel de dificultad moderado y es adecuado para usuarios que tienen un conocimiento básico de Python y desean profundizar sus conocimientos.

El código fuente sokoban.py.zip, se libera bajo la licencia GNU GPL v3, y la skin fue creada por Borgar.

👀 Vista previa

Animación de vista previa del juego Sokoban

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo inicializar el juego utilizando Pygame
  • Cómo manejar los eventos del juego y las operaciones del teclado
  • Cómo implementar el mapa del juego
  • Cómo implementar las operaciones de movimiento para el jugador y las cajas
  • Cómo implementar las operaciones de deshacer y rehacer
  • Cómo probar la interfaz del juego

🏆 Logros

Después de completar este proyecto, serás capaz de:

  • Inicializar Pygame y configurar la ventana del juego
  • Manejar los eventos del juego y las entradas del teclado en Pygame
  • Implementar el mapa del juego y mostrarlo utilizando Pygame
  • Implementar las operaciones de movimiento para el jugador y las cajas
  • Implementar las operaciones de deshacer y rehacer en el juego
  • Probar y ejecutar la interfaz del juego

Descripción del juego

En el juego de Sokoban, hay un muro cerrado que forma un área poligonal irregular. El jugador y las cajas solo pueden moverse dentro de esta área. Dentro de la área, hay una persona, varias cajas y puntos objetivo. El objetivo del juego es usar las flechas del teclado para controlar el movimiento de la persona y empujar las cajas hacia los puntos objetivo. Solo se puede mover una caja a la vez, y si una caja se queda atrapada en una esquina, el juego no puede continuar.

Personajes

A partir de la descripción anterior, podemos abstraer los siguientes personajes en el juego:

  1. Muros: Áreas cerradas que bloquean los caminos de movimiento.
  2. Espacios: Áreas donde la persona puede caminar y empujar cajas.
  3. Persona: El personaje controlado por el jugador.
  4. Cajas
  5. Puntos objetivo

La persona, las cajas y los puntos objetivo deben inicializarse dentro del área de espacio, y no deben aparecer otros personajes dentro del área del muro.

Controles

En el juego de Sokoban, el único personaje que podemos controlar es la persona. Usamos las flechas del teclado para controlar el movimiento de la persona, tanto para mover a la persona como para empujar cajas. Hay dos tipos de movimientos para la persona, y necesitamos manejar cada caso por separado:

  1. Mover a la persona sola
  2. Mover a la persona mientras empuja una caja

Además, el juego admite las siguientes dos operaciones:

  1. Deshacer: Deshacer el movimiento anterior, controlado por la tecla de retroceso.
  2. Rehacer: Rehacer el movimiento previamente deshecho, controlado por la barra espaciadora.

En resumen, necesitamos admitir los eventos del teclado para las cuatro flechas del teclado, la tecla de retroceso para deshacer y la barra espaciadora para rehacer. En la siguiente sección de implementación de pygame, necesitaremos manejar estos seis eventos del teclado.

Preparación para el desarrollo

Para poder utilizar pygame en el entorno, abre la terminal en el entorno experimental y escribe el siguiente comando para instalar pygame:

sudo pip install pygame

Hay muchos módulos en pygame, incluyendo el mouse, dispositivos de visualización, gráficos, eventos, fuentes, imágenes, teclados, sonido, video, audio, etc. En el juego de Sokoban, utilizaremos los siguientes módulos:

  • pygame.display: Acceder a dispositivos de visualización para mostrar imágenes.
  • pygame.image: Cargar y almacenar imágenes, utilizado para manejar hojas de sprites.
  • pygame.key: Leer entradas del teclado.
  • pygame.event: Gestionar eventos, manejar eventos del teclado en el juego.
  • pygame.time: Gestionar el tiempo y mostrar información de fotogramas.

La introducción anterior mencionó las hojas de sprites. La hoja de sprites es un método común de combinación de imágenes en el desarrollo de juegos, que combina pequeñas iconos y imágenes de fondo en una sola imagen, y luego utiliza la localización de imágenes de pygame para mostrar la parte requerida de la imagen.

En el juego de Sokoban, utilizamos una hoja de sprites prefabricada. No entraré en detalles sobre cómo recortar imágenes y combinar hojas de sprites aquí, ya que hay innumerables métodos disponibles en línea.

Los elementos de imagen en la hoja de sprites de Sokoban utilizada en este proyecto son de borgar, y el archivo se puede encontrar en ~/project/borgar.png.

Los elementos de imagen del juego incluyen:

  • Color de fondo de la interfaz del juego
  • Jugador
  • Caja normal
  • Punto objetivo
  • Efecto de superposición del jugador y el punto objetivo
  • Efecto de superposición cuando la caja alcanza el punto objetivo
  • Muro

En nuestra implementación, no se necesitan dos imágenes de caja en la hoja de sprites. Explicaremos en detalle cómo utilizar el método blit de pygame para cargar y mostrar el contenido de la hoja de sprites en la parte de implementación subsiguiente.

Desarrollo del juego

Primero, crea un archivo sokoban.py en el directorio ~/project, luego ingresa el siguiente contenido en el archivo:

  1. Inicializar pygame
import pygame, sys, os
from pygame.locals import *

from collections import deque


pygame.init()
  1. Establecer el objeto de visualización
## Establece el tamaño de la ventana de visualización de pygame a 400 píxeles de ancho y 300 píxeles de alto
screen = pygame.display.set_mode((400,300))
  1. Cargar elementos de imagen
## Cargar elementos de imagen desde un solo archivo
skinfilename = os.path.join('borgar.png')

try:
    skin = pygame.image.load(skinfilename)
except pygame.error as msg:
    print('no se puede cargar la skin')
    raise SystemExit(msg)

skin = skin.convert()

## Establece el color de fondo de la ventana al elemento en las coordenadas (0,0) del archivo de skin
screen.fill(skin.get_at((0,0)))
  1. Establecer el reloj y el tiempo de repetición de los eventos del teclado. Utiliza key.set_repeat para establecer el intervalo de tiempo para los eventos de repetición con los parámetros (delay, interval).
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. Iniciar el bucle principal
## Bucle principal del juego
while True:
    clock.tick(60)
    pass
  1. Manejar eventos del juego y operaciones del teclado. En el bucle principal, necesitamos manejar los eventos del teclado, como se mencionó anteriormente, necesitamos admitir seis teclas: arriba, abajo, izquierda, derecha, retroceso y espacio.
## Obtener eventos del juego
for event in pygame.event.get():
    ## Evento de salida del juego
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## Operación del teclado
    elif event.type == KEYDOWN:
        ## Moverse hacia la izquierda
        if event.key == K_LEFT:
            pass
        ## Moverse hacia arriba
        elif event.key == K_UP:
            pass
        ## Moverse hacia la derecha
        elif event.key == K_RIGHT:
            pass
        ## Moverse hacia abajo
        elif event.key == K_DOWN:
            pass
        ## Operación de deshacer
        elif event.key == K_BACKSPACE:
            pass
        ## Operación de rehacer
        elif event.key == K_SPACE:
            pass

Ahora hemos completado el marco de juego basado en pygame. Comencemos a implementar la lógica del juego.

✨ Revisar Solución y Practicar

Implementación del mapa

Primero, necesitamos definir el objeto Sokoban. Utilizamos una clase para contener toda la lógica relacionada con el juego.

class Sokoban:

    ## Inicializar el juego de Sokoban
    def __init__(self):
        pass

El juego de Sokoban requiere un área de operación, que es el área del mapa. Utilizamos una lista de caracteres para representar el mapa, donde diferentes caracteres representan diferentes elementos en el juego:

  1. Muro: ## símbolo
  2. Espacio: - símbolo
  3. Jugador: @ símbolo
  4. Caja: $ símbolo
  5. Punto objetivo: . símbolo
  6. Jugador en el punto objetivo: + símbolo
  7. Caja en el punto objetivo: * símbolo

Cuando el juego comienza, necesitamos establecer una lista de caracteres predeterminada para el mapa. Al mismo tiempo, necesitamos conocer el ancho y el alto del mapa para poder generar un mapa bidimensional a partir de esta lista unidimensional.

La representación del mapa es similar al siguiente código. ¿Puedes imaginar cómo se vería después de iniciar basado en este código?

class Sokoban:

    ## Inicializar el juego de Sokoban
    def __init__(self):
        ## Establecer el mapa
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## Establecer el ancho y el alto del mapa y la posición del jugador en el mapa (valor de índice en la lista del mapa)
        ## En total 19 columnas
        self.w = 19

        ## En total 11 filas
        self.h = 11

        ## La posición inicial del jugador está en self.level[163]
        self.man = 163

El mapa se muestra escaneando la lista de caracteres y mostrando diferentes elementos en las posiciones correspondientes según los caracteres.

Dado que la visualización es bidimensional, el ancho y el alto se utilizan para determinar la posición de cada carácter en el área de visualización bidimensional. Necesitamos pasar screen y skin mencionados en pygame como parámetros a la función de dibujo draw.

Es importante destacar que la función de dibujo que implementamos utiliza blit de pygame, que extrae la imagen de la hoja de sprites y la muestra en la posición especificada:

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

La implementación completa de la función draw es la siguiente. Primero, se realiza el escaneo, y luego se muestra la imagen correspondiente a cada carácter basada en la hoja de sprites:

class Sokoban:

    ## Dibujar el mapa en la ventana de pygame basado en el nivel del mapa
    def draw(self, screen, skin):

        ## Obtener el ancho de cada elemento de imagen
        w = skin.get_width() / 4

        ## Iterar a través de cada elemento de carácter en el nivel del mapa
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## Obtener el carácter en la fila j y la columna i del mapa
                item = self.level[j*self.w + i]

                ## Mostrar como un muro (#) en esta posición
                if item == '#':
                    ## Utilizar el método blit de pygame para mostrar la imagen en la posición especificada,
                    ## con las coordenadas de posición (i*w, j*w), y las coordenadas y el ancho-altura de la imagen en la skin como (0,2*w,w,w)
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## Mostrar como un espacio (-) en esta posición
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## Mostrar como un jugador (@) en esta posición
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## Mostrar como una caja ($) en esta posición
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## Mostrar como un punto objetivo (.) en esta posición
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## Mostrar como el efecto del jugador en un punto objetivo
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## Mostrar como el efecto de la caja colocada en un punto objetivo
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))
✨ Revisar Solución y Practicar

Implementando la operación de movimiento

La operación de movimiento utiliza las flechas del teclado para controlar el movimiento en cuatro direcciones: izquierda, derecha, arriba y abajo. Utilizamos cuatro caracteres 'l' (izquierda), 'r' (derecha), 'u' (arriba) y 'd' (abajo) para especificar la dirección de movimiento.

Dado que el proceso requerido para la operación de rehacer y la operación de movimiento es similar, definimos una función interna, _move(), para manejar el movimiento en la clase Sokoban:

class Sokoban:

    ## Función interna de movimiento: se utiliza para actualizar los cambios de posición de los elementos en el mapa después de la operación de movimiento, donde d representa la dirección de movimiento
    def _move(self, d):
        ## Obtener el desplazamiento en el mapa para el movimiento
        h = get_offset(d, self.w)

        ## Si el área objetivo del movimiento es un espacio vacío o un punto objetivo, solo el jugador necesita moverse
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## Mover el jugador a la posición objetivo
            move_man(self.level, self.man + h)
            ## Establecer la posición original del jugador después del movimiento
            move_floor(self.level, self.man)
            ## La nueva posición del jugador
            self.man += h
            ## Agregar la operación de movimiento a la solución
            self.solution += d

        ## Si el área objetivo del movimiento es una caja, tanto la caja como el jugador deben moverse
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## El desplazamiento de la caja y la posición del jugador
            h2 = h * 2
            ## La caja solo se puede mover si la siguiente posición es un espacio vacío o un punto objetivo
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## Mover la caja al punto objetivo
                move_box(self.level, self.man + h2)
                ## Mover el jugador al punto objetivo
                move_man(self.level, self.man + h)
                ## Restablecer la posición actual del jugador
                move_floor(self.level, self.man)
                ## Establecer la nueva posición del jugador
                self.man += h
                ## Marcar la operación de movimiento como un carácter en mayúsculas para indicar que se empujó una caja en este paso
                self.solution += d.upper()
                ## Incrementar el número de pasos para empujar la caja
                self.push += 1

En la función _move, necesitamos utilizar las siguientes funciones:

  • get_offset(d, width): Obtener el desplazamiento del movimiento en el mapa. d representa la dirección de movimiento y width representa el ancho de la ventana del juego.
  • move_man(level, i): Mover la posición del jugador en el mapa. level es la lista del mapa y i es la posición del jugador.
  • move_floor(level, i): Restablecer la posición después del movimiento. Después de que el jugador se mueve de una posición, debe ser restablecida como un espacio vacío o un punto objetivo.
  • move_box(level, i): Mover la posición de la caja en el mapa. level es la lista del mapa y i es la posición de la caja.

La implementación de estas funciones se puede ver en el código completo. Es importante considerar cuál es el elemento original en la posición objetivo cuando se mueve cada elemento para determinar qué elemento debe ser establecido después del movimiento.

Para realizar la operación de movimiento, simplemente llamar a _move y establecer todo[] como vacío (la lista de rehacer solo se activa cuando se realiza la operación de deshacer).

✨ Revisar Solución y Practicar

Implementar la operación de deshacer

La operación de deshacer es la operación inversa de un movimiento. Recupera el paso anterior de solution y realiza la operación inversa. Ver el código detallado:

class Sokoban:

    ## Operación de deshacer: deshacer el movimiento anterior
    def undo(self):
        ## Verificar si hay un registro de movimiento
        if self.solution.__len__()>0:
            ## Almacenar el registro de movimiento en la lista todo para la operación de rehacer
            self.todo.append(self.solution[-1])
            ## Eliminar el registro de movimiento
            self.solution.pop()

            ## Obtener el desplazamiento que se debe mover para la operación de deshacer: el negativo del desplazamiento del último movimiento
            h = get_offset(self.todo[-1],self.w) * -1

            ## Verificar si esta operación solo mueve el carácter sin empujar una caja
            if self.todo[-1].islower():
                ## Mover el carácter de vuelta a su posición original
                move_man(self.level, self.man + h)
                ## Establecer la posición actual del carácter
                move_floor(self.level, self.man)
                ## Establecer la posición del carácter en el mapa
                self.man += h
            else:
                ## Si este paso empuja una caja, mover el carácter, la caja y realizar las operaciones relacionadas en _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
✨ Revisar Solución y Practicar

Operación de rehacer

Cuando se ejecuta el comando de deshacer, el contenido se mueve de solution[] a todo[], y solo necesitamos extraer y llamar a la función _move.

    ## Operación de rehacer: Cuando se ejecuta y se activa la operación de deshacer, volver a la posición antes del deshacer
    def redo(self):
        ## Verificar si hay una operación de deshacer registrada
        if self.todo.__len__() > 0:
            ## Volver atrás los pasos deshechos
            self._move(self.todo[-1].lower())
            ## Eliminar este registro
            self.todo.pop()

Con los pasos anteriores, se ha completado el contenido principal del juego. Continúe por favor completando el código completo del juego de forma independiente, probando las capturas de pantalla y haga cualquier pregunta en la sección de preguntas y respuestas de la Sala de Experimentación si tiene alguna duda. El equipo y los profesores de la Sala de Experimentación responderán rápidamente a cualquier pregunta que pueda tener.

✨ Revisar Solución y Practicar

Funciones adicionales y refactorización del código

Ahora tenemos un juego básico, pero no es perfecto. Necesitamos agregar algunas funciones adicionales para que sea más jugable.

También necesitamos refactorizar el código para que sea más legible y mantenible.

Haga clic para ver el 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"
    )
✨ Revisar Solución y Practicar

Ejecución y prueba

Para ejecutar en la terminal:

cd ~/project
python sokoban.py

Si todo está normal, verás la siguiente interfaz de juego:

Vista previa de la interfaz de juego de Sokoban
✨ Revisar Solución y Practicar

Resumen

Este proyecto solo ha implementado una funcionalidad básica del juego de Sokoban. Basado en la experimentación, se puede considerar ampliar este código mediante:

  1. Determinar cómo extraer los datos del mapa del código escrito y guardarlos en un archivo.
  2. Implementar controles de ratón para mover rápidamente el personaje a una posición específica.
  3. Desarrollar un algoritmo para determinar automáticamente si un mapa es resoluble.