Игра "Подвиньте коробку" с использованием Pygame

PythonBeginner
Практиковаться сейчас

Введение

Этот проект представляет собой разработку классической игры Сокобан на языке Python с использованием Pygame.

В этом проекте затрагиваются следующие знания:

  • Базовый синтаксис Python
  • Основы разработки игр с использованием Pygame

Этот курс имеет средний уровень сложности и подходит для пользователей, имеющих базовое понимание Python и желающих дальнейшего углубления своих знаний.

Исходный код sokoban.py.zip распространяется под лицензией GNU GPL v3, а скин был создан Borgar.

👀 Предпросмотр

Анимация предварительного просмотра игры Сокобан

🎯 Задачи

В этом проекте вы научитесь:

  • Как инициализировать игру с использованием Pygame
  • Как обрабатывать игровые события и операции с клавиатурой
  • Как реализовать карту для игры
  • Как реализовать операции перемещения игрока и ящиков
  • Как реализовать операции отмены и повторения
  • Как протестировать игровой интерфейс

🏆 Достижения

После завершения этого проекта вы сможете:

  • Инициализировать Pygame и настроить игровое окно
  • Обрабатывать игровые события и ввод с клавиатуры в Pygame
  • Реализовать игровую карту и отобразить ее с использованием Pygame
  • Реализовать операции перемещения игрока и ящиков
  • Реализовать операции отмены и повторения в игре
  • Протестировать и запустить игровой интерфейс

Описание игры

В игре Сокобан имеется заграждающая стена, которая образует неравнобедренный многоугольный участок. Игрок и ящики могут перемещаться только внутри этого участка. Внутри участка есть человек, несколько ящиков и точки назначения. Цель игры - использовать стрелки на клавиатуре для управления движением человека и толкнуть ящики на точки назначения. Можно двигать только один ящик за раз, и если ящик застрянет в углу, игра не может продолжаться.

Персонажи

Из вышеописанного можно выделить следующих персонажей в игре:

  1. Стены: Закрытые участки, блокирующие пути движения.
  2. Площади: Участки, где человек может ходить и толкать ящики.
  3. Человек: Персонаж, контролируемый игроком.
  4. Ящики
  5. Точки назначения

Человек, ящики и точки назначения должны быть инициализированы внутри области пространства, а другие персонажи не должны появляться внутри области стен.

Управление

В игре Сокобан единственным персонажем, которого мы можем контролировать, является человек. Мы используем стрелки на клавиатуре для управления движением человека, как для перемещения человека, так и для толкания ящиков. Существует два типа движений для человека, и мы должны обработать каждый случай отдельно:

  1. Перемещение человека без ящика
  2. Перемещение человека с толканием ящика

Кроме того, игра поддерживает следующие две операции:

  1. Отмена: Отменить предыдущее движение, контролируется клавишей Backspace.
  2. Повтор: Повторить ранее отмененное движение, контролируется пробелом.

В целом, нам нужно поддерживать события клавиатуры для четырёх стрелок, клавишу Backspace для отмены и пробел для повторения. В следующем разделе по реализации pygame нам придётся обработать эти шесть событий клавиатуры.

Подготовка к разработке

Для того чтобы в среде можно было использовать pygame, откройте терминал в экспериментальной среде и введите следующую команду для установки pygame:

sudo pip install pygame

В pygame есть много модулей, в том числе мышь, устройства отображения, графика, события, шрифты, изображения, клавиатуры, звук, видео, аудио и т.д. В игре Сокобан мы будем использовать следующие модули:

  • pygame.display: Доступ к устройствам отображения для отображения изображений.
  • pygame.image: Загрузка и хранение изображений, используется для обработки спрайт-листов.
  • pygame.key: Чтение ввода с клавиатуры.
  • pygame.event: Управление событиями, обработка событий клавиатуры в игре.
  • pygame.time: Управление временем и отображение информации о кадрах.

В приведенном выше описании упоминались спрайт-листы. Спрайт-лист - это распространенный метод объединения изображений в игре, при котором мелкие иконки и изображения фона объединяются в одно изображение, а затем с использованием позиционирования изображений в pygame отображается нужная часть изображения.

В игре Сокобан мы используем готовый спрайт-лист. Здесь я не буду подробно останавливаться на том, как обрезать изображения и объединить спрайт-листы, так как онлайн доступно бесчисленное количество методов.

Элементы изображений в спрайт-листе для игры Сокобан, используемом в этом проекте, взяты от borgar, и файл можно найти по пути ~/project/borgar.png.

Элементы игровых изображений включают в себя:

  • Цвет фона игрового интерфейса
  • Игрока
  • Обычный ящик
  • Точку назначения
  • Эффект наложения игрока и точки назначения
  • Эффект наложения ящика, достигшего точки назначения
  • Стену

Два изображения ящиков в спрайт-листе не понадобятся в нашей реализации. В последующей части реализации мы подробно объясним, как использовать метод blit в pygame для загрузки и отображения содержимого спрайт-листа.

Разработка игры

Сначала создайте файл sokoban.py в директории ~/project, затем введите в файл следующее содержимое:

  1. Инициализация pygame
import pygame, sys, os
from pygame.locals import *

from collections import deque


pygame.init()
  1. Настройка объекта отображения
## Установите размер окна отображения pygame в 400 пикселей в ширину и 300 пикселей в высоту
screen = pygame.display.set_mode((400,300))
  1. Загрузка элементов изображений
## Загрузите элементы изображений из одного файла
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()

## Установите цвет фона окна элементом по координатам (0,0) в файле skin
screen.fill(skin.get_at((0,0)))
  1. Настройка часов и времени повторения событий клавиатуры. Используйте key.set_repeat, чтобы установить интервал времени для повторяющихся событий с параметрами (задержка, интервал).
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. Запуск главного цикла
## Главный цикл игры
while True:
    clock.tick(60)
    pass
  1. Обработка игровых событий и операций с клавиатурой. В главном цикле мы должны обрабатывать события клавиатуры, как упоминалось ранее, нам нужно поддерживать шесть клавиш: вверх, вниз, влево, вправо, Backspace и пробел.
## Получите игровые события
for event in pygame.event.get():
    ## Событие выхода из игры
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## Операция с клавиатурой
    elif event.type == KEYDOWN:
        ## Переместиться влево
        if event.key == K_LEFT:
            pass
        ## Переместиться вверх
        elif event.key == K_UP:
            pass
        ## Переместиться вправо
        elif event.key == K_RIGHT:
            pass
        ## Переместиться вниз
        elif event.key == K_DOWN:
            pass
        ## Операция отмены
        elif event.key == K_BACKSPACE:
            pass
        ## Операция повторения
        elif event.key == K_SPACE:
            pass

Теперь мы завершили игровой фреймворк на основе pygame. Давайте приступим к реализации игровой логики.

Реализация карты

Сначала нам нужно определить объект Сокобан. Мы используем класс для включения всей игровой логики.

class Sokoban:

    ## Инициализация игры Сокобан
    def __init__(self):
        pass

В игре Сокобан требуется рабочая область, которая представляет собой игровую карту. Мы используем список символов для представления карты, где разные символы представляют разные элементы в игре:

  1. Стена: ## символ
  2. Пространство: - символ
  3. Игрок: @ символ
  4. Ящик: $ символ
  5. Точка назначения: . символ
  6. Игрок на точке назначения: + символ
  7. Ящик на точке назначения: * символ

При запуске игры нам нужно установить стандартный список символов для карты. В то же время нам нужно знать ширину и высоту карты, чтобы сгенерировать из этого одномерного списка двумерную карту.

Представление карты похоже на следующий код. Можете ли вы представить, как это будет выглядеть после запуска на основе этого кода?

class Sokoban:

    ## Инициализация игры Сокобан
    def __init__(self):
        ## Установка карты
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## Установка ширины и высоты карты и позиции игрока в карте (индексное значение в списке карты)
        ## Всего 19 столбцов
        self.w = 19

        ## Всего 11 строк
        self.h = 11

        ## Начальная позиция игрока находится в self.level[163]
        self.man = 163

Карта отображается путём сканирования списка символов и отображения различных элементов в соответствующих позициях в зависимости от символов.

Поскольку отображение двумерное, ширину и высоту используют для определения позиции каждого символа в двумерной области отображения. Мы должны передать screen и skin, упомянутые в pygame, в качестве параметров в функцию рисования draw.

Важно отметить, что функция рисования, которую мы реализовали, использует blit из pygame, которая извлекает изображение из спрайт-листа и отображает его в указанной позиции:

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

Полная реализация функции draw выглядит следующим образом. Сначала выполняется сканирование, а затем на основе спрайт-листа отображается соответствующее изображение для каждого символа:

class Sokoban:

    ## Отрисовка карты на окне pygame на основе уровня карты
    def draw(self, screen, skin):

        ## Получение ширины каждого элемента изображения
        w = skin.get_width() / 4

        ## Перебор каждого символового элемента в уровне карты
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## Получение символа в j-й строке и i-м столбце карты
                item = self.level[j*self.w + i]

                ## Отображение как стена(#) в этой позиции
                if item == '#':
                    ## Использование метода blit из pygame для отображения изображения в указанной позиции,
                    ## с координатами позиции (i*w, j*w), а координатами и длиной-шириной изображения в skin как (0,2*w,w,w)
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## Отображение как пространство(-) в этой позиции
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## Отображение как игрок(@) в этой позиции
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## Отображение как ящик($) в этой позиции
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## Отображение как точка назначения(.) в этой позиции
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## Отображение как эффект игрока на точке назначения
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## Отображение как эффект ящика, помещённого на точку назначения
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))

Реализация операции перемещения

Операция перемещения использует стрелки на клавиатуре для управления движением в четырех направлениях: влево, вправо, вверх и вниз. Мы используем четыре символа 'l' (влево), 'r' (вправо), 'u' (вверх) и 'd' (вниз) для указания направления движения.

Поскольку процесс для операции повторения и операции перемещения похож, мы определяем внутреннюю функцию, _move(), для обработки движения в классе Sokoban:

class Sokoban:

    ## Внутренняя функция перемещения: используется для обновления изменений позиции элементов в карте после операции перемещения, где d представляет направление движения
    def _move(self, d):
        ## Получить смещение в карте для движения
        h = get_offset(d, self.w)

        ## Если целевая область движения - это пустое пространство или точка назначения, нужно только переместить игрока
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## Переместить игрока в целевую позицию
            move_man(self.level, self.man + h)
            ## Установить исходную позицию игрока после перемещения
            move_floor(self.level, self.man)
            ## Новая позиция игрока
            self.man += h
            ## Добавить операцию перемещения в решение
            self.solution += d

        ## Если целевая область движения - это ящик, нужно переместить и ящик, и игрока
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## Смещение ящика и позиция игрока
            h2 = h * 2
            ## Ящик можно переместить только если следующая позиция - это пустое пространство или точка назначения
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## Переместить ящик в точку назначения
                move_box(self.level, self.man + h2)
                ## Переместить игрока в точку назначения
                move_man(self.level, self.man + h)
                ## Сбросить текущую позицию игрока
                move_floor(self.level, self.man)
                ## Установить новую позицию игрока
                self.man += h
                ## Отметить операцию перемещения как заглавный символ, чтобы показать, что в этом шаге был толкнут ящик
                self.solution += d.upper()
                ## Увеличить количество шагов для толкания ящика
                self.push += 1

В функции _move нам нужно использовать следующие функции:

  • get_offset(d, width): Получить смещение движения в карте. d представляет направление движения, а width представляет ширину игрового окна.
  • move_man(level, i): Переместить позицию игрока в карте. level - это список карты, а i - позиция игрока.
  • move_floor(level, i): Сбросить позицию после перемещения. После того, как игрок переместился из позиции, она должна быть сброшена в пустое пространство или точку назначения.
  • move_box(level, i): Переместить позицию ящика в карте. level - это список карты, а i - позиция ящика.

Реализация этих функций можно увидеть в полном коде. Важно учитывать, какой был исходный элемент в целевой позиции при перемещении каждого элемента, чтобы определить, какой элемент должен быть установлен после перемещения.

Для выполнения операции перемещения достаточно вызвать _move и установить todo[] в пустое (список повторений активируется только при выполнении операций отмены).

Реализация отмены действия

Операция отмены - это обратная операция перемещения. Она извлекает предыдущий шаг из solution и выполняет обратную операцию. См. подробный код:

class Sokoban:

    ## Операция отмены: отменить предыдущее движение
    def undo(self):
        ## Проверить, есть ли запись о движении
        if self.solution.__len__()>0:
            ## Сохранить запись о движении в списке todo для операции повторения
            self.todo.append(self.solution[-1])
            ## Удалить запись о движении
            self.solution.pop()

            ## Получить смещение для отмены операции перемещения: отрицание смещения последнего движения
            h = get_offset(self.todo[-1],self.w) * -1

            ## Проверить, выполняет ли эта операция только перемещение символа без толкания ящика
            if self.todo[-1].islower():
                ## Переместить символ обратно в исходную позицию
                move_man(self.level, self.man + h)
                ## Установить текущую позицию символа
                move_floor(self.level, self.man)
                ## Установить позицию символа на карте
                self.man += h
            else:
                ## Если в этом шаге толкнули ящик, переместить символ, ящик и выполнить соответствующие операции в _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

Действие повторения

Когда выполняется команда отмены, содержимое перемещается из solution[] в todo[], и нам просто нужно извлечь и вызвать функцию _move.

    ## Операция повторения: Когда выполняется и активируется операция отмены, вернуться в позицию до отмены
    def redo(self):
        ## Проверить, есть ли запись об операции отмены
        if self.todo.__len__() > 0:
            ## Вернуть отменённые шаги
            self._move(self.todo[-1].lower())
            ## Удалить эту запись
            self.todo.pop()

С помощью вышеописанных шагов основное содержание игры было завершено. Пожалуйста, продолжайте самостоятельно завершать полный код игры, тестировать скриншоты и задавать любые вопросы в разделе Вопрос-ответ в Экспериментальной комнате, если у вас есть непонятные моменты. Команда Экспериментальной комнаты и учителя оперативно ответят на любые вопросы, которые вы можете задать.

Дополнительные функции и рефакторинг кода

Теперь у нас есть базовая игра, но она не идеальна. Мы должны добавить некоторые дополнительные функции, чтобы сделать ее более играбельной.

Мы также должны отрефакторить код, чтобы сделать его более читаемым и поддерживаемым.

Нажмите, чтобы увидеть полный код
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"
    )

Запуск и тестирование

Для запуска в терминале:

cd ~/project
python sokoban.py

Если все в порядке, вы увидите следующий игровой интерфейс:

Предпросмотр игрового интерфейса Sokoban

Резюме

В этом проекте реализована только базовая функциональность игры "Сокобан". Основываясь на эксперименте, можно рассмотреть расширение этого кода следующим образом:

  1. Выяснить, как извлечь данные карты из написанного кода и сохранить их в файл.
  2. Реализовать управление мышью для быстрого перемещения персонажа в определенную позицию.
  3. Разработать алгоритм для автоматического определения, является ли карта разрешаемой.
✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться✨ Проверить решение и практиковаться