Робот и коробки
Самая сложная часть реализации игры в стиле Sokoban обычно состоит в том, чтобы перемещать робота так, чтобы он мог толкать коробки в нужном направлении. Игра должна уметь определять, может ли робот двигаться в указанном направлении, и обрабатывать любые ситуации, где коробка тоже должна двигаться. Теперь займемся этой задачей.
Обработка событий клавиш
Игрок управляет роботом четырьмя клавишами со стрелками, поэтому наш обработчик событий тоже должен уметь реагировать на соответствующие события клавиш:
def check_events(self):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
self.move(0, -1)
if event.key == pygame.K_RIGHT:
self.move(0, 1)
if event.key == pygame.K_UP:
self.move(-1, 0)
if event.key == pygame.K_DOWN:
self.move(1, 0)
if event.type == pygame.QUIT:
exit()Теперь каждый раз, когда игрок нажимает клавишу со стрелкой, вызывается метод move с подходящей парой аргументов. Первый аргумент содержит движение в вертикальном направлении, а второй — движение в горизонтальном направлении.
Поиск робота
Игра должна знать положение робота, чтобы правильно его перемещать. Добавим метод find_robot, который выясняет положение робота:
def find_robot(self ):
for y in range(self.height):
for x in range(self.width):
if self.map[y][x] in [4, 6]:
return (y, x)Метод проходит по всем клеткам игровой сетки и возвращает координаты клетки, которая содержит либо число 4 (робот сам по себе), либо число 6 (робот на целевой клетке).
Идея в том, что каждый раз, когда игрок нажимает клавишу со стрелкой, сначала положение робота определяется путем прохода по клеткам сетки. Это может показаться немного медленным и лишним, поскольку мы с тем же успехом могли бы хранить положение робота в одной или двух отдельных переменных. Преимущество этого подхода с поиском в том, что мы не храним положение робота в двух разных местах (в игровой сетке и в отдельных переменных), а вместо этого заботимся только об одном месте (игровой сетке). Это означает, что состояние игры в памяти компьютера проще обрабатывать.
Изменения игровой сетки
Выше мы уже вызвали метод move, но еще фактически его не определили. Сделаем это теперь.
Метод move принимает направление, в котором игрок хочет двигаться, в качестве аргументов. Затем он соответствующим образом обновляет игровую сетку или определяет, что ход запрещен, и оставляет сетку без изменений.
def move(self, move_y, move_x):
robot_old_y, robot_old_x = self.find_robot()
robot_new_y = robot_old_y + move_y
robot_new_x = robot_old_x + move_x
if self.map[robot_new_y][robot_new_x] == 1:
return
if self.map[robot_new_y][robot_new_x] in [3, 5]:
box_new_y = robot_new_y + move_y
box_new_x = robot_new_x + move_x
if self.map[box_new_y][box_new_x] in [1, 3, 5]:
return
self.map[robot_new_y][robot_new_x] -= 3
self.map[box_new_y][box_new_x] += 3
self.map[robot_old_y][robot_old_x] -= 4
self.map[robot_new_y][robot_new_x] += 4У метода довольно много разных этапов, поэтому рассмотрим каждый по очереди:
Старое и новое положение робота
robot_old_y, robot_old_x = self.find_robot()
robot_new_y = robot_old_y + move_y
robot_new_x = robot_old_x + move_xСначала метод вызывает find_robot, чтобы найти текущее положение робота до хода. Оно сохраняется в переменных robot_old_y и robot_old_x.
Затем новое положение робота после предполагаемого хода сохраняется в переменных robot_new_y и robot_new_x. Новые координаты легко вычислить, прибавив значения, переданные аргументами, к старому положению робота, поскольку оба они содержат вертикальное и горизонтальное значения.
Робот ударился о стену?
if self.map[robot_new_y][robot_new_x] == 1:
returnОператор if выше обрабатывает ситуацию, где робот в результате хода ударился бы о стену. Помните, 1 было позицией клетки стены в списке изображений. Это запрещено, поэтому метод просто возвращается без дальнейших действий.
Перемещение коробки
if self.map[robot_new_y][robot_new_x] in [3, 5]:
box_new_y = robot_new_y + move_y
box_new_x = robot_new_x + move_x
if self.map[box_new_y][box_new_x] in [1, 3, 5]:
return
self.map[robot_new_y][robot_new_x] -= 3
self.map[box_new_y][box_new_x] += 3Если новое предполагаемое положение робота содержит число 3 (коробка сама по себе) или число 5 (коробка на целевой клетке), робот пытается передвинуть коробку на следующую клетку дальше. Для этого нужны две новые переменные: box_new_y и box_new_x, которые содержат положение коробки после хода.
Как и робот, коробку нельзя передвинуть на клетку стены с идентификатором 1. Коробка также не может переместиться на другую коробку или на целевую клетку, где уже стоит коробка. Если это произошло бы в результате хода, метод снова просто возвращается, не внося никаких изменений в сетку.
Во всех остальных случаях коробка может двигаться. Значение в текущем месте коробки в сетке уменьшается на 3, а значение в ее новом месте увеличивается на 3. Благодаря удачному порядку элементов в списке images это работает правильно и когда участвующие клетки являются клетками пола, и когда они являются целевыми клетками.
Перемещение робота
self.map[robot_old_y][robot_old_x] -= 4
self.map[robot_new_y][robot_new_x] += 4Если выполнение метода дошло до этого места без возврата, пора переместить и робота. Процедура похожа на перемещение коробки, но значение, которое вычитается из соответствующих мест сетки и прибавляется к ним, на этот раз равно 4. Это снова обеспечивает, благодаря удачному порядку элементов в списке images, правильный итоговый результат на сетке и для клеток пола, и для целевых клеток, участвующих в ходе.
Рефакторинг?
Использовать только сетку для хранения состояния игры в любой момент очень удобно в том смысле, что во всем процессе постоянно участвует только одна переменная, и состояние сетки относительно легко обновлять простыми сложениями и вычитаниями.
Недостаток в том, что программный код игры может быть немного трудно понять. Если кто-то, не знакомый с используемой логикой, увидит следующую строку кода, он, скорее всего, немного озадачится:
if self.map[box_new_y][box_new_x] in [1, 3, 5]:Фрагмент кода выше использует магические числа для представления клеток сетки. Любой читающий код должен знать, что 1 означает стену, 3 означает коробку, а 5 означает коробку на целевой клетке.
Строк и с хитрыми вычитаниями и сложениями выглядели бы еще более загадочно:
self.map[robot_new_y][robot_new_x] -= 3Число 3 только что означало коробку, но теперь оно вычитается из значения клетки на сетке. Это работает в контексте нашей схемы нумерации, поскольку превращает коробку (3) в обычную клетку пола (0), а целевую клетку с коробкой (5) — в пустую целевую клетку (2), но для понимания этого требуется предварительное знакомство с используемой схемой нумерации.
Мы могли бы облегчить чтение кода, отрефакторив нашу реализацию. Это означает улучшение структуры и читаемости кода. Один способ добиться этого — использовать имена клеток вместо чисел от 0 до 6, хотя это все еще не объяснило бы, как и почему числа можно складывать и вычитать, сохраняя целостность сетки.
Чтобы сделать программный код действительно доступным, скорее всего, потребовался бы гораздо более фундаментальн ый рефакторинг. Например, мы могли бы хранить структуру карты игры в одном месте, а положения робота и коробок — в какой-нибудь отдельной структуре данных. Минус этого подхода в том, что он, вероятно, привел бы к гораздо большему объему кода, а внутренняя структура игры стала бы намного сложнее.
Рефакторинг и качество кода — тема некоторых последующих курсов, таких как Software Development Methods и Software Engineering.
Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.