Научить каждый ход соблюдать правила поля

C++Beginner
Практиковаться сейчас

Введение

Реализуйте допустимые шаги и толкание одного ящика в игре про склад на C++ для терминала.

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

Ходить, не проходя сквозь стены

На этом шаге поиграйте в готовый пример, а затем заставьте своего игрока соблюдать границы поля. Начните во вкладке Terminal. При работе в редакторе WebIDE откройте Terminal > New Terminal; все следующие команды выполняются в этой оболочке. cd меняет рабочий каталог именно этой оболочки; изменение не переносится в новые сеансы терминала.

cd /home/labex/project/sokoban

Подготовленная программа sample — полная демонстрация, отдельная от вашей незавершённой программы. ./ запускает программу из текущего каталога.

./sample

Введите d и нажмите Enter, затем ещё раз введите d и нажмите Enter. За эти два хода игрок подходит к ящику и толкает его на цель. Появится Solved!, а $ превратится в *. Введите u и нажмите Enter: игрок и ящик вернутся на прежние клетки, а сообщение станет Not solved. Введите q и нажмите Enter, чтобы вернуться в оболочку. В каждой строке допускается одна буква; пустая или неизвестная команда показывает справку и ничего не меняет. Пример демонстрирует результат; его запуск не означает завершения вашего движка.

Подготовленный класс Game объявлен в engine.hpp. Point хранит целочисленные координаты x и y: x растёт вправо, y — вниз. State хранит игрока и std::vector<Point> со всеми ящиками. terrain[y][x] содержит только стены #, пробелы и цели .. Благодаря const после создания поле доступно только для чтения, поэтому движущийся ящик не может стереть цель. При отображении объединяются два слоя: * означает ящик на цели, а + — игрока на цели.

floor(point) сообщает, находится ли координата внутри поля и не является ли стеной. boxAt(point) возвращает индекс ящика в векторе или -1, если на клетке нет ящика. Эти вспомогательные функции, загрузка, отображение и чтение команд уже подготовлены. Вы реализуете move, won, а позднее undo в engine.cpp; сохраните engine.hpp, support.cpp, main.cpp, levels/ и sample. Можно добавлять вспомогательные функции и использовать равнозначные алгоритмы. Проверяется поведение, а не конкретное написание кода или маршрут.

Прочитайте подготовленное объявление и начальный код:

cat engine.hpp
cat engine.cpp

Начальная программа отклоняет все ходы. Поэтому эта намеренная самопроверка печатает FAIL walk changes only the player и завершается с ненулевым статусом: это означает, что реализация ещё не готова, а не что окружение сломано:

bash /opt/labex-sokoban/check.sh walk

Поддерживаются замкнутые прямоугольные ASCII-карты размером не менее трёх строк и трёх столбцов. На карте ровно один игрок, хотя бы один ящик и столько же целей, сколько ящиков, без наложения объектов. Обозначения: # — стена, пробел — пол, . — цель, @ — игрок, $ — ящик, + — игрок на цели, * — ящик на цели. Карта может быть неразрешимой или уже завершённой с самого начала. Подготовленный загрузчик отклоняет остальные данные; писать парсер не требуется.

move(dx, dy) принимает только (1,0), (-1,0), (0,1) или (0,-1). Любая другая пара целых должна возвращать false, не меняя состояние и историю. Допустимое действие возвращает true; недопустимое возвращает false и ничего не меняет. Порядок ящиков в векторе не влияет на занятость клеток. Позднее отмена восстановит фактическое полное состояние, сохранённое до действия, включая тогдашний порядок ящиков.

Сначала реализуйте только ходьбу. Point{...} создаёт значение координат. && требует выполнения обоих условий, а || — хотя бы одного. Мы явно перечисляем четыре направления до сложения координат: это также безопасно отклоняет необычно большие целые. >= 0 обозначает клетку, занятую ящиком. Пока ящики мешают ходьбе; толкание добавим дальше. Ветви return false оставляют всё состояние неизменным.

Чтобы записать полный файл из терминала, cat > engine.cpp перенаправляет текст в этот файл, заменяя прежнее содержимое. Маркер here-document в кавычках <<'CPP' заставляет оболочку читать следующие строки буквально до строки, содержащей только CPP. Скопируйте команду целиком, включая закрывающий маркер:

cat > engine.cpp <<'CPP'
#include "engine.hpp"

bool Game::move(int dx, int dy) {
    if (!((dx == -1 && dy == 0) || (dx == 1 && dy == 0) ||
          (dx == 0 && dy == -1) || (dx == 0 && dy == 1))) return false;
    Point next{state.player.x + dx, state.player.y + dy};
    if (!floor(next) || boxAt(next) >= 0) return false;
    state.player = next;
    return true;
}
bool Game::undo() { return false; }
bool Game::won() const { return false; }
CPP

Скомпилируйте три исходных файла. -std=c++17 выбирает C++17, -Wall -Wextra включает полезные предупреждения, а -o sokoban задаёт имя исполняемого файла. Ошибки компиляции содержат номер строки; исправьте эту строку в engine.cpp либо повторите полную замену и снова выполните ту же сборку.

g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban

Введите a, затем Enter: стена слева вызывает Blocked, и поле не меняется. Введите d, затем Enter: игрок идёт вправо. Второй d всё ещё блокируется ящиком, что ожидаемо на этом шаге. u сообщает No earlier move: отмена ещё не реализована. Введите q для выхода.

bash /opt/labex-sokoban/check.sh walk

All walk checks passed подтверждает ходьбу в четырёх направлениях и безопасные отказы. Проверки компилируют ваш настоящий движок во временном каталоге, а также запускают уже имеющийся исполняемый файл sokoban в изолированных играх. После изменений пересобирайте sokoban: отсутствующий или устаревший файл означает незавершённую работу. Проверки не делают ходов в текущей игре и не меняют исходники.

Толкать один ящик и сохранять цель

На этом шаге расширьте движок, чтобы толкать один ящик и определять завершение по его реальному положению. Продолжайте в Terminal, в /home/labex/project/sokoban. Каждый запуск начинает новую игру в памяти; редактирование или пересборка программы не сохраняет текущую партию.

Перед толканием проверьте клетку за ящиком. Она должна быть полом или целью без другого ящика. Сначала отклоните недопустимый вариант, затем вместе измените ящик и игрока. Это предотвращает толкание сквозь стены и цепочки из двух ящиков. Индекс ящика имеет тип int, потому что -1 означает отсутствие; используйте его для доступа к вектору только после проверки box >= 0.

won() const наблюдает за игрой, не меняя её. Цикл по диапазону for (Point box : state.boxes) перебирает координаты каждого ящика по значению. Каждый ящик должен находиться на . неподвижного поля; одного установленного ящика на карте с двумя недостаточно. По нашему контракту пустой вектор ящиков никогда не считается завершённой игрой. Победа вычисляется заново каждый раз, поэтому снова становится ложной, когда ящик покидает цель. В этой работе undo всё ещё намеренно отклоняет действие; история будет новой задачей следующей работы.

Замените engine.cpp полной реализацией движения и проверки завершения:

cat > engine.cpp <<'CPP'
#include "engine.hpp"

bool Game::move(int dx, int dy) {
    if (!((dx == -1 && dy == 0) || (dx == 1 && dy == 0) ||
          (dx == 0 && dy == -1) || (dx == 0 && dy == 1))) return false;
    Point next{state.player.x + dx, state.player.y + dy};
    if (!floor(next)) return false;
    int box = boxAt(next);
    Point beyond{next.x + dx, next.y + dy};
    if (box >= 0 && (!floor(beyond) || boxAt(beyond) >= 0)) return false;
    if (box >= 0) state.boxes[box] = beyond;
    state.player = next;
    return true;
}
bool Game::undo() {
    return false;
}
bool Game::won() const {
    if (state.boxes.empty()) return false;
    for (Point box : state.boxes) {
        if (terrain[box.y][box.x] != '.') return false;
    }
    return true;
}
CPP
g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban

Введите d, d, после каждой команды нажимая Enter. На поле появятся * и Solved!. Введите ещё d: ящик уйдёт с цели, игрок окажется на ней как +, а игра вернётся к Not solved. Следующий d блокируется, поскольку ящик вошёл бы в стену. Неподвижная цель по-прежнему находится под игроком. Введите q.

Попробуйте подготовленный склад с двумя ящиками, передав путь к его файлу как необязательный аргумент программы:

./sokoban levels/warehouse.txt

Один из возможных маршрутов: w, s, d, d, w, с Enter после каждой команды. Можно выбрать другой допустимый маршрут; кратчайшее решение не требуется. Оба ящика должны отображаться как *, прежде чем появится Solved!. Введите q.

Неверно написанный путь к уровню выводит Cannot open level: ... и завершает программу без изменения файлов уровней. Чтобы намеренно увидеть эту обычную ошибку, выполните:

./sokoban levels/missing.txt

Для восстановления повторите предыдущую команду с levels/warehouse.txt. Нужно исправить только имя файла, а не сбрасывать окружение. Повторный запуск уровня начинает новую игру в памяти.

bash /opt/labex-sokoban/check.sh move

All move checks passed охватывает допустимые шаги и толкания, недопустимые действия, сохранение целей, уже завершённую карту и разные решения с двумя ящиками. Сохраните движок и подготовленные файлы: у вас уже есть игровая система движения.

Резюме

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