Jeu de la boîte à pousser avec Pygame

PythonPythonBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Ce projet est le développement du jeu classique Sokoban en utilisant le langage Python et Pygame.

Les points de connaissances couverts dans ce projet sont les suivants :

  • Syntaxe de base de Python
  • Développement de base de jeux avec Pygame

Ce cours a un niveau de difficulté modéré et est adapté aux utilisateurs ayant une compréhension de base de Python et souhaitant approfondir leurs connaissances.

Le code source sokoban.py.zip, est publié sous la licence GNU GPL v3, et la peau a été créée par Borgar.

👀 Aperçu

Animation d'apéçu du jeu Sokoban

🎯 Tâches

Dans ce projet, vous allez apprendre :

  • Comment initialiser le jeu avec Pygame
  • Comment gérer les événements du jeu et les opérations clavier
  • Comment implémenter la carte du jeu
  • Comment implémenter les opérations de mouvement pour le joueur et les caisses
  • Comment implémenter les opérations d'annulation et de refaisabilité
  • Comment tester l'interface du jeu

🏆 Réalisations

Après avoir terminé ce projet, vous serez capable de :

  • Initialiser Pygame et configurer la fenêtre de jeu
  • Gérer les événements du jeu et les entrées clavier dans Pygame
  • Implémenter la carte du jeu et l'afficher à l'aide de Pygame
  • Implémenter les opérations de mouvement pour le joueur et les caisses
  • Implémenter les opérations d'annulation et de refaisabilité dans le jeu
  • Tester et exécuter l'interface du jeu

Description du jeu

Dans le jeu de Sokoban, il y a un mur fermé qui forme une zone polygonale irrégulière. Le joueur et les caisses ne peuvent se déplacer que dans cette zone. À l'intérieur de la zone, il y a une personne, plusieurs caisses et des points cibles. L'objectif du jeu est d'utiliser les flèches directionnelles pour contrôler le mouvement de la personne et pousser les caisses sur les points cibles. Seule une caisse peut être déplacée à la fois, et si une caisse est coincée dans un coin, le jeu ne peut pas continuer.

Personnages

À partir de la description ci-dessus, on peut extraire les personnages suivants dans le jeu :

  1. Murs : Zones fermées qui bloquent les chemins de déplacement.
  2. Espaces libres : Zones où la personne peut marcher et pousser les caisses.
  3. Personne : Le personnage contrôlé par le joueur.
  4. Caisses
  5. Points cibles

La personne, les caisses et les points cibles doivent tous être initialisés à l'intérieur de la zone d'espace libre, et aucun autre personnage ne devrait apparaître dans la zone du mur.

Contrôles

Dans le jeu de Sokoban, le seul personnage que nous pouvons contrôler est la personne. Nous utilisons les flèches directionnelles pour contrôler le mouvement de la personne, tant pour déplacer la personne que pour pousser les caisses. Il y a deux types de mouvements pour la personne, et nous devons gérer chaque cas séparément :

  1. Déplacer la personne seule
  2. Déplacer la personne en poussant une caisse

En outre, le jeu prend en charge les deux opérations suivantes :

  1. Annulation : Annuler le mouvement précédent, contrôlé par la touche Suppr.
  2. Refaisabilité : Refaire le mouvement précédemment annulé, contrôlé par la barre d'espace.

En résumé, nous devons prendre en charge les événements clavier pour les quatre flèches directionnelles, la touche Suppr pour l'annulation et la barre d'espace pour la refaisabilité. Dans la section suivante sur l'implémentation de pygame, nous devrons gérer ces six événements clavier.

Préparatifs du développement

Pour pouvoir utiliser pygame dans l'environnement, ouvrez le terminal dans l'environnement expérimental et entrez la commande suivante pour installer pygame :

sudo pip install pygame

Il existe de nombreux modules dans pygame, notamment pour la souris, les dispositifs d'affichage, les graphiques, les événements, les polices, les images, les claviers, le son, la vidéo, l'audio, etc. Dans le jeu Sokoban, nous utiliserons les modules suivants :

  • pygame.display : Accéder aux dispositifs d'affichage pour afficher des images.
  • pygame.image : Charger et stocker des images, utilisé pour gérer les feuilles d'images.
  • pygame.key : Lire les entrées clavier.
  • pygame.event : Gérer les événements, gérer les événements clavier dans le jeu.
  • pygame.time : Gérer le temps et afficher des informations sur les trames.

La présentation ci-dessus a mentionné les feuilles d'images. Une feuille d'images est une méthode commune de fusion d'images dans le développement de jeux, qui fusionne de petits icônes et des images d'arrière-plan en une seule image, puis utilise le positionnement d'images de pygame pour afficher la partie requise de l'image.

Dans le jeu Sokoban, nous utilisons une feuille d'images prête. Je ne rentrerai pas dans les détails sur la façon de découper des images et de fusionner des feuilles d'images ici, car il existe innombrables méthodes disponibles en ligne.

Les éléments d'image dans la feuille d'images Sokoban utilisée dans ce projet sont issus de borgar, et le fichier peut être trouvé à ~/project/borgar.png.

Les éléments d'image du jeu sont les suivants :

  • Couleur de fond de l'interface du jeu
  • Joueur
  • Caisse normale
  • Point cible
  • Effet de chevauchement entre le joueur et le point cible
  • Effet de chevauchement lorsque la caisse atteint le point cible
  • Mur

Deux images de caisses dans la feuille d'images ne sont pas nécessaires dans notre implémentation. Nous expliquerons en détail comment utiliser la méthode blit de pygame pour charger et afficher le contenu de la feuille d'images dans la partie d'implémentation suivante.

Développement du jeu

Tout d'abord, créez un fichier sokoban.py dans le répertoire ~/project, puis entrez le contenu suivant dans le fichier :

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

from collections import deque


pygame.init()
  1. Configurer l'objet d'affichage
## Configurer la taille de la fenêtre d'affichage de pygame à 400 pixels de large et 300 pixels de haut
screen = pygame.display.set_mode((400,300))
  1. Charger les éléments d'image
## Charger les éléments d'image à partir d'un seul fichier
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()

## Configurer la couleur d'arrière-plan de la fenêtre à l'élément aux coordonnées (0,0) dans le fichier skin
screen.fill(skin.get_at((0,0)))
  1. Configurer l'horloge et le temps de répétition pour les événements clavier. Utilisez key.set_repeat pour définir l'intervalle de temps pour les événements de répétition avec les paramètres (delay, interval).
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. Démarrer la boucle principale
## Boucle principale du jeu
while True:
    clock.tick(60)
    pass
  1. Gérer les événements du jeu et les opérations clavier. Dans la boucle principale, nous devons gérer les événements clavier, comme mentionné précédemment, nous devons prendre en charge six touches : haut, bas, gauche, droite, Suppr et Espace.
## Obtenir les événements du jeu
for event in pygame.event.get():
    ## Événement de fermeture du jeu
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## Opération clavier
    elif event.type == KEYDOWN:
        ## Se déplacer vers la gauche
        if event.key == K_LEFT:
            pass
        ## Se déplacer vers le haut
        elif event.key == K_UP:
            pass
        ## Se déplacer vers la droite
        elif event.key == K_RIGHT:
            pass
        ## Se déplacer vers le bas
        elif event.key == K_DOWN:
            pass
        ## Opération d'annulation
        elif event.key == K_BACKSPACE:
            pass
        ## Opération de refaisabilité
        elif event.key == K_SPACE:
            pass

Maintenant, nous avons terminé le cadre du jeu basé sur pygame. Commençons à implémenter la logique du jeu.

✨ Vérifier la solution et pratiquer

Implémentation de la carte

Tout d'abord, nous devons définir l'objet Sokoban. Nous utilisons une classe pour contenir toute la logique liée au jeu.

class Sokoban:

    ## Initialiser le jeu Sokoban
    def __init__(self):
        pass

Le jeu Sokoban nécessite une zone de jeu, qui est la zone de la carte. Nous utilisons une liste de caractères pour représenter la carte, où différents caractères représentent différents éléments dans le jeu :

  1. Mur : ## symbole
  2. Espace libre : - symbole
  3. Joueur : @ symbole
  4. Caisse : $ symbole
  5. Point cible : . symbole
  6. Joueur sur le point cible : + symbole
  7. Caisse sur le point cible : * symbole

Lorsque le jeu commence, nous devons définir une liste de caractères par défaut pour la carte. En même temps, nous devons connaître la largeur et la hauteur de la carte afin de générer une carte 2D à partir de cette liste unidimensionnelle.

La représentation de la carte est similaire au code suivant. Pouvez-vous imaginer à quoi elle ressemblera après avoir été lancée à partir de ce code?

class Sokoban:

    ## Initialiser le jeu Sokoban
    def __init__(self):
        ## Définir la carte
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## Définir la largeur et la hauteur de la carte et la position du joueur dans la carte (valeur d'index dans la liste de la carte)
        ## Total 19 colonnes
        self.w = 19

        ## Total 11 lignes
        self.h = 11

        ## La position initiale du joueur est à self.level[163]
        self.man = 163

La carte est affichée en parcourant la liste de caractères et en affichant différents éléments aux positions correspondantes en fonction des caractères.

Puisque l'affichage est 2D, la largeur et la hauteur sont utilisées pour déterminer la position de chaque caractère dans la zone d'affichage 2D. Nous devons passer screen et skin mentionnés dans pygame en tant que paramètres à la fonction de dessin draw.

Il est important de noter que la fonction de dessin que nous avons implémentée utilise blit de pygame, qui extrait l'image de la feuille d'images et l'affiche à la position spécifiée :

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

L'implémentation complète de la fonction draw est la suivante. Tout d'abord, le balayage est effectué, puis l'image correspondant à chaque caractère est affichée en fonction de la feuille d'images :

class Sokoban:

    ## Dessiner la carte sur la fenêtre pygame en fonction du niveau de la carte
    def draw(self, screen, skin):

        ## Obtenir la largeur de chaque élément d'image
        w = skin.get_width() / 4

        ## Itérer à travers chaque élément de caractère dans le niveau de la carte
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## Obtenir le caractère à la ligne j et la colonne i dans la carte
                item = self.level[j*self.w + i]

                ## Afficher comme un mur (#) à cette position
                if item == '#':
                    ## Utiliser la méthode blit de pygame pour afficher l'image à la position spécifiée,
                    ## avec les coordonnées de position (i*w, j*w), et les coordonnées et la largeur-hauteur de l'image dans la peau comme (0,2*w,w,w)
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## Afficher comme un espace libre (-) à cette position
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## Afficher comme un joueur (@) à cette position
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## Afficher comme une caisse ($) à cette position
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## Afficher comme un point cible (.) à cette position
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## Afficher comme l'effet du joueur sur un point cible
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## Afficher comme l'effet de la caisse placée sur un point cible
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))
✨ Vérifier la solution et pratiquer

Implémentation de l'opération de déplacement

L'opération de déplacement utilise les flèches directionnelles pour contrôler le déplacement dans les quatre directions : gauche, droite, haut et bas. Nous utilisons quatre caractères 'l' (gauche), 'r' (droite), 'u' (haut) et 'd' (bas) pour spécifier la direction de déplacement.

Puisque le processus requis pour l'opération de refaisabilité et l'opération de déplacement est similaire, nous définissons une fonction interne, _move(), pour gérer le déplacement dans la classe Sokoban :

class Sokoban:

    ## Fonction interne de déplacement : utilisée pour mettre à jour les changements de position des éléments dans la carte après l'opération de déplacement, où d représente la direction de déplacement
    def _move(self, d):
        ## Obtenir le déplacement dans la carte pour le mouvement
        h = get_offset(d, self.w)

        ## Si la zone cible du mouvement est un espace libre ou un point cible, seul le joueur doit se déplacer
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## Déplacer le joueur vers la position cible
            move_man(self.level, self.man + h)
            ## Définir la position d'origine du joueur après le mouvement
            move_floor(self.level, self.man)
            ## La nouvelle position du joueur
            self.man += h
            ## Ajouter l'opération de déplacement à la solution
            self.solution += d

        ## Si la zone cible du mouvement est une caisse, à la fois la caisse et le joueur doivent se déplacer
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## Le déplacement de la caisse et la position du joueur
            h2 = h * 2
            ## La caisse ne peut être déplacée que si la position suivante est un espace libre ou un point cible
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## Déplacer la caisse vers le point cible
                move_box(self.level, self.man + h2)
                ## Déplacer le joueur vers le point cible
                move_man(self.level, self.man + h)
                ## Réinitialiser la position actuelle du joueur
                move_floor(self.level, self.man)
                ## Définir la nouvelle position du joueur
                self.man += h
                ## Marquer l'opération de déplacement comme un caractère en majuscule pour indiquer qu'une caisse a été poussée dans cette étape
                self.solution += d.upper()
                ## Incrémenter le nombre d'étapes pour pousser la caisse
                self.push += 1

Dans la fonction _move, nous devons utiliser les fonctions suivantes :

  • get_offset(d, width) : Obtenir le déplacement du mouvement dans la carte. d représente la direction de mouvement et width représente la largeur de la fenêtre de jeu.
  • move_man(level, i) : Déplacer la position du joueur dans la carte. level est la liste de la carte et i est la position du joueur.
  • move_floor(level, i) : Réinitialiser la position après le mouvement. Après que le joueur se soit déplacé d'une position, elle doit être réinitialisée comme un espace libre ou un point cible.
  • move_box(level, i) : Déplacer la position de la caisse dans la carte. level est la liste de la carte et i est la position de la caisse.

L'implémentation de ces fonctions peut être vue dans le code complet. Il est important de considérer quel est l'élément original à la position cible lorsqu'on déplace chaque élément pour déterminer quel élément devrait être défini après le mouvement.

Pour effectuer l'opération de déplacement, appelez simplement _move et définissez todo[] sur vide (la liste de refaisabilité n'est activée que lors de l'exécution des opérations d'annulation).

✨ Vérifier la solution et pratiquer

Implémentation de l'annulation

L'annulation est l'opération inverse d'un mouvement. Elle récupère l'étape précédente dans solution et effectue l'opération inverse. Consultez le code détaillé :

class Sokoban:

    ## Opération d'annulation : annuler le mouvement précédent
    def undo(self):
        ## Vérifier s'il existe un enregistrement de mouvement
        if self.solution.__len__()>0:
            ## Stockez l'enregistrement de mouvement dans la liste todo pour l'opération de refaisabilité
            self.todo.append(self.solution[-1])
            ## Supprimez l'enregistrement de mouvement
            self.solution.pop()

            ## Obtenez le déplacement à effectuer pour l'opération d'annulation : le négatif du déplacement du dernier mouvement
            h = get_offset(self.todo[-1],self.w) * -1

            ## Vérifiez si cette opération ne déplace que le caractère sans pousser de caisse
            if self.todo[-1].islower():
                ## Ramenez le caractère à sa position d'origine
                move_man(self.level, self.man + h)
                ## Définissez la position actuelle du caractère
                move_floor(self.level, self.man)
                ## Définissez la position du caractère sur la carte
                self.man += h
            else:
                ## Si cette étape pousse une caisse, déplacez le caractère, la caisse et effectuez les opérations connexes dans _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
✨ Vérifier la solution et pratiquer

Opération de refaisabilité

Lorsque la commande d'annulation est exécutée, le contenu est déplacé de solution[] vers todo[], et nous n'avons qu'à extraire et appeler la fonction _move.

    ## Opération de refaisabilité : Lorsque l'opération d'annulation est exécutée et activée, revenir à la position avant l'annulation
    def redo(self):
        ## Vérifier s'il existe une opération d'annulation enregistrée
        if self.todo.__len__() > 0:
            ## Revenir en arrière les étapes annulées
            self._move(self.todo[-1].lower())
            ## Supprimer cet enregistrement
            self.todo.pop()

Avec les étapes ci-dessus, le contenu principal du jeu a été terminé. Veuillez continuer à compléter indépendamment le code complet du jeu, tester les captures d'écran et poser toutes les questions dans la section Q&A de la salle d'expérience si vous avez des points non clairs. L'équipe et les enseignants de la salle d'expérience répondront rapidement à toutes les questions que vous pourriez avoir.

✨ Vérifier la solution et pratiquer

Fonctions supplémentaires et refactoring du code

Maintenant, nous avons un jeu de base, mais il n'est pas parfait. Nous devons ajouter quelques fonctions supplémentaires pour le rendre plus jouable.

Nous devons également refactoriser le code pour le rendre plus lisible et maintenable.

Cliquez pour voir le code complet
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"
    )
✨ Vérifier la solution et pratiquer

Exécution et test

Pour exécuter dans le terminal :

cd ~/projet
python sokoban.py

Si tout est normal, vous verrez l'interface de jeu suivante :

Aperçu de l'interface de jeu Sokoban
✨ Vérifier la solution et pratiquer

Sommaire

Ce projet n'a implémenté que les fonctionnalités de base d'un jeu de Sokoban. Sur la base de l'expérience, on peut envisager d'élargir ce code en :

  1. Découvrant comment extraire les données de la carte à partir du code écrit et les enregistrer dans un fichier.
  2. Implantant des contrôles de souris pour déplacer rapidement le personnage vers une position spécifique.
  3. Développant un algorithme pour déterminer automatiquement si une carte est résoluble.