Sokoban-Spiel mit Pygame

PythonPythonBeginner
Jetzt üben

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Dieses Projekt ist die Entwicklung des klassischen Spiels Sokoban mit der Programmiersprache Python und Pygame.

Die in diesem Projekt behandelten Kenntnisse umfassen:

  • Die Grundlagen der Python-Syntax
  • Die Grundlagen der Spielentwicklung mit Pygame

Dieser Kurs hat ein mittleres Schwierigkeitsniveau und eignet sich für Benutzer, die eine grundlegende Kenntnis von Python haben und ihre Kenntnisse erweitern möchten.

Der Quellcode sokoban.py.zip wird unter der GNU GPL v3 Lizenz veröffentlicht, und die Oberfläche wurde von Borgar erstellt.

👀 Vorschau

Sokoban-Spielvorschauanimation

🎯 Aufgaben

In diesem Projekt lernen Sie:

  • Wie man das Spiel mit Pygame initialisiert
  • Wie man Spielereignisse und Tastatureingaben behandelt
  • Wie man die Karte für das Spiel implementiert
  • Wie man Bewegungsvorgänge für den Spieler und die Kisten implementiert
  • Wie man Rückgängig- und Wiederholvorgänge implementiert
  • Wie man die Spieloberfläche testet

🏆 Errungenschaften

Nach Abschluss dieses Projekts können Sie:

  • Pygame initialisieren und das Spielfenster einrichten
  • Spielereignisse und Tastatureingaben in Pygame behandeln
  • Die Spielkarte implementieren und mit Pygame anzeigen
  • Bewegungsvorgänge für den Spieler und die Kisten implementieren
  • Rückgängig- und Wiederholvorgänge im Spiel implementieren
  • Die Spieloberfläche testen und ausführen

Spielbeschreibung

Im Spiel Sokoban gibt es eine geschlossene Wand, die einen unregelmäßigen polygonalen Bereich bildet. Der Spieler und die Kisten können sich nur innerhalb dieses Bereichs bewegen. Innerhalb des Bereichs befindet sich eine Person, mehrere Kisten und Zielpunkte. Das Ziel des Spiels ist es, mit den Pfeiltasten die Bewegung der Person zu steuern und die Kisten auf die Zielpunkte zu schieben. Es kann nur eine Kiste gleichzeitig bewegt werden, und wenn eine Kiste in einer Ecke steckt, kann das Spiel nicht fortgesetzt werden.

Charaktere

Aus der obigen Beschreibung können wir die folgenden Charaktere im Spiel abstrahieren:

  1. Wände: Geschlossene Bereiche, die die Bewegungswege blockieren.
  2. Räume: Bereiche, in denen die Person laufen und Kisten schieben kann.
  3. Person: Der vom Spieler gesteuerte Charakter.
  4. Kisten
  5. Zielpunkte

Die Person, die Kisten und die Zielpunkte sollten alle innerhalb des Raumbereichs initialisiert werden, und keine anderen Charaktere sollten innerhalb des Wandbereichs erscheinen.

Bedienelemente

Im Spiel Sokoban ist der einzige Charakter, den wir steuern können, die Person. Wir verwenden die Pfeiltasten, um die Bewegung der Person zu steuern, sowohl um die Person zu bewegen als auch um Kisten zu schieben. Es gibt zwei Arten von Bewegungen für die Person, und wir müssen jede Situation separat behandeln:

  1. Das alleinige Bewegen der Person
  2. Das Bewegen der Person, während eine Kiste geschoben wird

Zusätzlich unterstützt das Spiel die folgenden beiden Operationen:

  1. Rückgängig: Der vorherige Zugang rückgängig machen, gesteuert durch die Backspace-Taste.
  2. Wiederholen: Den zuvor rückgängig gemachten Zugang wiederholen, gesteuert durch die Leertaste.

Zusammenfassend müssen wir die Tastaturevents für die vier Pfeiltasten, die Backspace-Taste für das Rückgängigmachen und die Leertaste für das Wiederholen unterstützen. Im nächsten Abschnitt zur Implementierung von Pygame müssen wir diese sechs Tastaturevents behandeln.

Entwicklungsvorbereitung

Um in der Umgebung pygame verwenden zu können, öffnen Sie das Terminal in der experimentellen Umgebung und geben Sie den folgenden Befehl ein, um pygame zu installieren:

sudo pip install pygame

Es gibt viele Module in pygame, darunter Maus, Anzeigegeräte, Grafiken, Ereignisse, Schriftarten, Bilder, Tastaturen, Sound, Video, Audio usw. Im Sokoban-Spiel werden wir die folgenden Module verwenden:

  • pygame.display: Zugang zu Anzeigegeräten, um Bilder anzuzeigen.
  • pygame.image: Bilder laden und speichern, um Spritesheets zu verarbeiten.
  • pygame.key: Tastatureingaben lesen.
  • pygame.event: Ereignisse verwalten, um Tastaturevents im Spiel zu behandeln.
  • pygame.time: Zeit verwalten und Frameinformationen anzeigen.

In der obigen Einführung wurde von Spritesheets die Rede. Ein Spritesheet ist eine übliche Methode der Bildzusammenführung in der Spielentwicklung, bei der kleine Symbole und Hintergrundbilder zu einem Bild zusammengeführt werden und dann die Bildpositionierung in pygame verwendet wird, um den erforderlichen Teil des Bilds anzuzeigen.

Im Sokoban-Spiel verwenden wir ein fertiges Spritesheet. Ich werde hier nicht im Detail auf die Bildausschnitte und die Zusammenführung von Spritesheets eingehen, da es unzählige Methoden im Internet gibt.

Die Bildelemente im Sokoban-Spritesheet, das in diesem Projekt verwendet wird, stammen von borgar, und die Datei kann unter ~/project/borgar.png gefunden werden.

Die Spielbildelemente umfassen:

  • Hintergrundfarbe der Spieloberfläche
  • Spieler
  • Normalbox
  • Zielpunkt
  • Überlappungseffekt zwischen Spieler und Zielpunkt
  • Überlappungseffekt, wenn die Box den Zielpunkt erreicht
  • Wand

Zwei Boxbilder im Spritesheet werden in unserer Implementierung nicht benötigt. Wir werden im späteren Implementierungsteil im Detail erklären, wie die blit-Methode in pygame verwendet wird, um den Inhalt des Spritesheets zu laden und anzuzeigen.

Spielentwicklung

Erstellen Sie zunächst eine Datei sokoban.py im Verzeichnis ~/project, und geben Sie dann den folgenden Inhalt in die Datei ein:

  1. Initialisieren von Pygame
import pygame, sys, os
from pygame.locals import *

from collections import deque


pygame.init()
  1. Festlegen des Anzeigeobjekts
## Setzen der Größe des Pygame-Anzeigefensters auf 400 Pixel breit und 300 Pixel hoch
screen = pygame.display.set_mode((400,300))
  1. Laden von Bildelementen
## Laden von Bildelementen aus einer einzelnen Datei
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()

## Setzen der Hintergrundfarbe des Fensters auf das Element an den Koordinaten (0,0) in der Skin-Datei
screen.fill(skin.get_at((0,0)))
  1. Festlegen der Uhr und der Wiederholzeit für Tastaturevents. Verwenden Sie key.set_repeat, um das Zeitintervall für Wiederholereignisse mit den Parametern (delay, interval) festzulegen.
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. Starten der Hauptschleife
## Spielhauptschleife
while True:
    clock.tick(60)
    pass
  1. Behandeln von Spielereignissen und Tastatureingaben. In der Hauptschleife müssen wir Tastaturevents behandeln. Wie bereits erwähnt, müssen wir sechs Tasten unterstützen: nach oben, nach unten, nach links, nach rechts, Backspace und Leertaste.
## Abrufen von Spielereignissen
for event in pygame.event.get():
    ## Spiel beenden Ereignis
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## Tastatureingabe
    elif event.type == KEYDOWN:
        ## Links bewegen
        if event.key == K_LEFT:
            pass
        ## Hoch bewegen
        elif event.key == K_UP:
            pass
        ## Rechts bewegen
        elif event.key == K_RIGHT:
            pass
        ## Runter bewegen
        elif event.key == K_DOWN:
            pass
        ## Rückgängigmachen der Operation
        elif event.key == K_BACKSPACE:
            pass
        ## Wiederholen der Operation
        elif event.key == K_SPACE:
            pass

Jetzt haben wir das auf Pygame basierende Spielframework abgeschlossen. Lassen Sie uns mit der Implementierung der Spiellogik beginnen.

✨ Lösung prüfen und üben

Implementierung der Karte

Zunächst müssen wir das Sokoban-Objekt definieren. Wir verwenden eine Klasse, um alle spielrelevanten Logik zu kapseln.

class Sokoban:

    ## Initialisieren des Sokoban-Spiels
    def __init__(self):
        pass

Das Sokoban-Spiel erfordert einen Arbeitsbereich, nämlich den Kartierungsbereich. Wir verwenden eine Zeichenliste, um die Karte darzustellen, wobei verschiedene Zeichen verschiedene Elemente im Spiel repräsentieren:

  1. Wand: ## Symbol
  2. Raum: - Symbol
  3. Spieler: @ Symbol
  4. Kiste: $ Symbol
  5. Zielpunkt: . Symbol
  6. Spieler auf Zielpunkt: + Symbol
  7. Kiste auf Zielpunkt: * Symbol

Wenn das Spiel startet, müssen wir eine Standardzeichenliste für die Karte festlegen. Gleichzeitig müssen wir die Breite und Höhe der Karte kennen, um aus dieser eindimensionalen Liste eine 2D-Karte zu generieren.

Die Kartendarstellung ähnelt dem folgenden Code. Können Sie sich vorstellen, wie es danach aussehen wird, wenn es gestartet wird, basierend auf diesem Code?

class Sokoban:

    ## Initialisieren des Sokoban-Spiels
    def __init__(self):
        ## Setzen der Karte
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## Setzen der Breite und Höhe der Karte und der Position des Spielers in der Karte (Indexwert in der Karteliste)
        ## Insgesamt 19 Spalten
        self.w = 19

        ## Insgesamt 11 Zeilen
        self.h = 11

        ## Die Anfangsposition des Spielers befindet sich bei self.level[163]
        self.man = 163

Die Karte wird durch das Scannen der Zeichenliste und das Anzeigen verschiedener Elemente an den entsprechenden Positionen basierend auf den Zeichen dargestellt.

Da die Anzeige 2D ist, werden die Breite und Höhe verwendet, um die Position jedes Zeichens im 2D-Anzeigebereich zu bestimmen. Wir müssen die in Pygame erwähnten screen und skin als Parameter an die Zeichnungsfunktion draw übergeben.

Es ist wichtig zu beachten, dass die von uns implementierte Zeichnungsfunktion blit aus Pygame verwendet, um das Bild aus dem Spritesheet zu extrahieren und es an der angegebenen Position anzuzeigen:

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

Die vollständige Implementierung der draw-Funktion lautet wie folgt. Zunächst wird der Scan durchgeführt, und dann wird das entsprechende Bild jedes Zeichens basierend auf dem Spritesheet angezeigt:

class Sokoban:

    ## Zeichnen der Karte auf das Pygame-Fenster basierend auf dem Kartierungslevel
    def draw(self, screen, skin):

        ## Erhalten der Breite jedes Bildelements
        w = skin.get_width() / 4

        ## Iterieren durch jedes Zeichenelement im Kartierungslevel
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## Erhalten des Zeichens an der j-ten Zeile und i-ten Spalte in der Karte
                item = self.level[j*self.w + i]

                ## Anzeigen als Wand(#) an dieser Position
                if item == '#':
                    ## Verwenden der blit-Methode aus Pygame, um das Bild an der angegebenen Position anzuzeigen,
                    ## mit den Positionskoordinaten (i*w, j*w) und den Koordinaten und Länge-Breite des Bilds in der Skin als (0,2*w,w,w)
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## Anzeigen als Raum(-) an dieser Position
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## Anzeigen als Spieler(@) an dieser Position
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## Anzeigen als Kiste($) an dieser Position
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## Anzeigen als Zielpunkt(.) an dieser Position
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## Anzeigen als Effekt des Spielers auf einem Zielpunkt
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## Anzeigen als Effekt der auf einem Zielpunkt platzierten Kiste
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))
✨ Lösung prüfen und üben

Implementierung der Bewegungsoperation

Die Bewegungsoperation verwendet die Pfeiltasten, um in vier Richtungen zu bewegen: nach links, nach rechts, nach oben und nach unten. Wir verwenden die vier Zeichen 'l' (links), 'r' (rechts), 'u' (oben) und 'd' (unten), um die Bewegungsrichtung anzugeben.

Da der Vorgang für die Wiederholungsoperation und die Bewegungsoperation ähnlich ist, definieren wir eine interne Funktion, _move(), um die Bewegung in der Sokoban-Klasse zu behandeln:

class Sokoban:

    ## Interne Bewegungsfunktion: verwendet, um die Positionänderungen der Elemente in der Karte nach der Bewegungsoperation zu aktualisieren, wobei d die Bewegungsrichtung darstellt
    def _move(self, d):
        ## Erhalten der Verschiebung in der Karte für die Bewegung
        h = get_offset(d, self.w)

        ## Wenn das Zielfeld der Bewegung ein leerer Raum oder ein Zielpunkt ist, muss nur der Spieler bewegt werden
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## Bewegen Sie den Spieler an die Zielfunktion
            move_man(self.level, self.man + h)
            ## Setzen Sie die ursprüngliche Position des Spielers nach der Bewegung
            move_floor(self.level, self.man)
            ## Die neue Position des Spielers
            self.man += h
            ## Fügen Sie die Bewegungsoperation zur Lösung hinzu
            self.solution += d

        ## Wenn das Zielfeld der Bewegung eine Kiste ist, müssen sowohl die Kiste als auch der Spieler bewegt werden
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## Die Verschiebung der Kiste und die Position des Spielers
            h2 = h * 2
            ## Die Kiste kann nur bewegt werden, wenn die nächste Position ein leerer Raum oder ein Zielpunkt ist
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## Bewegen Sie die Kiste zum Zielpunkt
                move_box(self.level, self.man + h2)
                ## Bewegen Sie den Spieler zum Zielpunkt
                move_man(self.level, self.man + h)
                ## Setzen Sie die aktuelle Position des Spielers zurück
                move_floor(self.level, self.man)
                ## Setzen Sie die neue Position des Spielers
                self.man += h
                ## Markieren Sie die Bewegungsoperation als einen Großbuchstaben, um anzuzeigen, dass in diesem Schritt eine Kiste geschoben wurde
                self.solution += d.upper()
                ## Inkrementieren Sie die Anzahl der Schritte zum Schieben der Kiste
                self.push += 1

In der _move-Funktion müssen wir die folgenden Funktionen verwenden:

  • get_offset(d, width): Erhalten der Verschiebung der Bewegung in der Karte. d stellt die Bewegungsrichtung dar, und width stellt die Breite des Spielfensters dar.
  • move_man(level, i): Bewegen der Position des Spielers in der Karte. level ist die Karteliste, und i ist die Position des Spielers.
  • move_floor(level, i): Zurücksetzen der Position nach der Bewegung. Nachdem der Spieler von einer Position bewegt wurde, muss es als leerer Raum oder ein Zielpunkt zurückgesetzt werden.
  • move_box(level, i): Bewegen der Position der Kiste in der Karte. level ist die Karteliste, und i ist die Position der Kiste.

Die Implementierung dieser Funktionen kann im vollständigen Code gesehen werden. Es ist wichtig, zu berücksichtigen, was das ursprüngliche Element an der Zielfunktion ist, wenn jedes Element bewegt wird, um zu bestimmen, was das Element nach der Bewegung gesetzt werden sollte.

Um die Bewegungsoperation durchzuführen, rufen Sie einfach _move auf und legen Sie todo[] auf leer (die Wiederholungsliste wird nur aktiviert, wenn die Rückgängigmachungsoperation durchgeführt wird).

✨ Lösung prüfen und üben

Implementierung der Rückgängigmachung

Die Rückgängigmachung ist die umgekehrte Operation einer Bewegung. Sie holt den vorherigen Schritt aus solution und führt die umgekehrte Operation durch. Siehe den detaillierten Code:

class Sokoban:

    ## Rückgängigmachungsoperation: macht die vorherige Bewegung rückgängig
    def undo(self):
        ## Überprüfen, ob es eine Bewegungserfassung gibt
        if self.solution.__len__()>0:
            ## Speichert die Bewegungserfassung in der todo-Liste für die Wiederholungsoperation
            self.todo.append(self.solution[-1])
            ## Löscht die Bewegungserfassung
            self.solution.pop()

            ## Erhalten der Verschiebung, die für die Rückgängigmachungsoperation ausgeführt werden soll: das Negative der Verschiebung der letzten Bewegung
            h = get_offset(self.todo[-1],self.w) * -1

            ## Überprüfen, ob diese Operation nur das Zeichen bewegt, ohne eine Kiste zu schieben
            if self.todo[-1].islower():
                ## Bewegen Sie das Zeichen zurück an seine ursprüngliche Position
                move_man(self.level, self.man + h)
                ## Setzen Sie die aktuelle Position des Zeichens
                move_floor(self.level, self.man)
                ## Setzen Sie die Position des Zeichens auf der Karte
                self.man += h
            else:
                ## Wenn in diesem Schritt eine Kiste geschoben wird, bewegen Sie das Zeichen, die Kiste und führen Sie in _move die entsprechenden Operationen durch
                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
✨ Lösung prüfen und üben

Wiederholungsoperation

Wenn der Befehl zur Rückgängigmachung ausgeführt wird, wird der Inhalt von solution[] in todo[] verschoben, und wir müssen nur die _move-Funktion extrahieren und aufrufen.

    ## Wiederholungsoperation: Wenn die Rückgängigmachungsoperation ausgeführt und aktiviert wird, bewegen Sie sich zurück an die Position vor der Rückgängigmachung
    def redo(self):
        ## Überprüfen, ob eine Rückgängigmachungsoperation aufgezeichnet ist
        if self.todo.__len__() > 0:
            ## Bewegen Sie die zurückgemachten Schritte zurück
            self._move(self.todo[-1].lower())
            ## Löschen Sie diese Aufzeichnung
            self.todo.pop()

Mit den obigen Schritten ist der Hauptinhalt des Spiels abgeschlossen. Bitte vervollständigen Sie den vollständigen Spielcode unabhängig, testen Sie die Screenshots und stellen Sie bei Unklarheiten in der Frage- und Antwort-Sektion des Experimenterraums Fragen. Das Experimenterraum-Team und die Lehrer werden Ihre Fragen unverzüglich beantworten.

✨ Lösung prüfen und üben

Zusätzliche Funktionen und Code-Refactoring

Wir haben jetzt ein grundlegendes Spiel, aber es ist noch nicht perfekt. Wir müssen einige zusätzliche Funktionen hinzufügen, um es spielbarer zu machen.

Wir müssen auch den Code umstrukturieren, um ihn lesbarer und wartbarer zu machen.

Klicken Sie, um den vollständigen Code anzuzeigen
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"
    )
✨ Lösung prüfen und üben

Ausführen und Testen

Um in der Konsole auszuführen:

cd ~/project
python sokoban.py

Wenn alles normal ist, werden Sie die folgende Spieloberfläche sehen:

Sokoban game interface preview
✨ Lösung prüfen und üben

Zusammenfassung

Dieses Projekt hat nur eine grundlegende Funktionalität eines Sokoban-Spiels implementiert. Basierend auf der Experimentation kann man überlegen, diesen Code zu erweitern, indem man:

  1. Erkennt, wie man die Kartendaten aus dem geschriebenen Code extrahiert und in eine Datei speichert.
  2. Maussteuerungen implementiert, um den Charakter schnell an eine bestimmte Position zu bewegen.
  3. Ein Algorithmus entwickelt, um automatisch zu bestimmen, ob eine Karte lösbar ist.