Fazer cada movimento respeitar o tabuleiro

C++Beginner
Pratique Agora

Introdução

Implemente passos válidos e o movimento de uma única caixa em um jogo de armazém em C++ para o terminal.

Você trabalhará em um pequeno jogo de terminal com terreno fixo e objetos móveis separados. Implementará as funções centrais em C++ e depois jogará para conferir seu comportamento.

Caminhar sem atravessar paredes

Nesta etapa, jogue um exemplo completo e depois faça os movimentos do seu jogador respeitarem o tabuleiro. Comece na aba Terminal. Se usar o editor WebIDE, abra Terminal > New Terminal; todos os comandos abaixo são executados nesse shell. cd altera o diretório de trabalho desse shell; a mudança não é transferida para novas sessões de terminal.

cd /home/labex/project/sokoban

O programa preparado sample é uma demonstração completa, separada do seu programa incompleto. ./ executa um programa do diretório atual.

./sample

Digite d, pressione Enter, digite d novamente e pressione Enter. Esses dois movimentos aproximam o jogador da caixa e a empurram para o alvo. Surge Solved!, e $ vira *. Digite u e pressione Enter: o jogador e a caixa voltam às casas anteriores, e a mensagem passa a Not solved. Digite q e pressione Enter para voltar ao shell. Cada linha aceita uma letra; comandos vazios ou desconhecidos exibem ajuda sem alterar nada. O exemplo mostra o resultado desejado; executá-lo não conclui seu próprio motor.

A classe preparada Game é declarada em engine.hpp. Point guarda as coordenadas inteiras x e y: x cresce para a direita e y para baixo. State guarda o jogador e um std::vector<Point> contendo todas as caixas. terrain[y][x] contém apenas paredes #, espaços e alvos .. const torna esse terreno somente leitura após a construção, impedindo que uma caixa em movimento apague um alvo. A exibição combina as duas camadas: * indica uma caixa sobre um alvo, e +, o jogador sobre um alvo.

floor(point) informa se uma coordenada está dentro do tabuleiro e não é uma parede. boxAt(point) retorna o índice de uma caixa no vetor, ou -1 quando nenhuma caixa ocupa a casa. Esses auxiliares, o carregamento, a exibição e a leitura de comandos já estão preparados. Você implementa move, won e, mais tarde, undo em engine.cpp; preserve engine.hpp, support.cpp, main.cpp, levels/ e sample. Pode adicionar auxiliares e usar algoritmos equivalentes. A verificação avalia o comportamento, não uma escrita ou rota específica.

Leia a declaração preparada e seu código inicial:

cat engine.hpp
cat engine.cpp

O programa inicial recusa qualquer movimento. Por isso esta autoverificação intencional imprime FAIL walk changes only the player e termina com estado diferente de zero; isso indica trabalho pendente, não um ambiente quebrado:

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

Os mapas aceitos são grades ASCII retangulares fechadas, com pelo menos três linhas e três colunas. Contêm exatamente um jogador, ao menos uma caixa e tantos alvos quanto caixas, sem objetos sobrepostos. Os símbolos são # parede, espaço chão, . alvo, @ jogador, $ caixa, + jogador sobre alvo e * caixa sobre alvo. Um mapa pode não ter solução ou já começar concluído. O carregador preparado recusa outras entradas; você não precisa escrever um analisador.

move(dx, dy) aceita apenas (1,0), (-1,0), (0,1) ou (0,-1). Qualquer outro par de inteiros deve retornar false sem mudar estado ou histórico. Uma ação válida retorna true; uma inválida retorna false e não muda nada. A ordem do vetor de caixas não importa para a ocupação das casas. Mais tarde, desfazer restaura o estado completo real salvo antes daquela ação, incluindo a ordem das caixas naquele momento.

Comece apenas pela caminhada. Point{...} constrói um valor de coordenadas. && exige as duas condições; || exige pelo menos uma. Enumeramos explicitamente as quatro direções antes de somar coordenadas, o que também recusa com segurança inteiros excepcionalmente grandes. >= 0 identifica uma casa ocupada por uma caixa. Nesta fase, caixas bloqueiam a caminhada; empurrar vem depois. Os ramos return false deixam todo o estado intacto.

Para escrever o arquivo completo pelo terminal, cat > engine.cpp redireciona o texto para esse arquivo, substituindo seu conteúdo anterior. O marcador de here-document entre aspas <<'CPP' faz o shell ler literalmente as linhas seguintes até uma linha contendo apenas CPP. Copie o comando inteiro, incluindo o marcador final:

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

Compile os três arquivos-fonte. -std=c++17 seleciona C++17, -Wall -Wextra ativa avisos úteis e -o sokoban dá nome ao executável. Erros de compilação incluem um número de linha; corrija essa linha em engine.cpp ou repita a substituição completa e execute a mesma compilação novamente.

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

Digite a e pressione Enter: a parede esquerda produz Blocked, e o tabuleiro não muda. Digite d e pressione Enter: o jogador caminha para a direita. Um segundo d continua bloqueado pela caixa, como esperado nesta etapa. u informa No earlier move: desfazer ainda não foi implementado. Digite q para sair.

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

All walk checks passed confirma a caminhada em quatro direções e recusas seguras. As verificações compilam seu motor real em um diretório temporário e também executam seu executável sokoban existente em partidas isoladas. Recompile sokoban após editar; um executável ausente ou desatualizado é uma entrega incompleta. As verificações não fazem movimentos na partida em andamento nem mudam seu código-fonte.

Empurrar uma caixa e preservar o alvo

Nesta etapa, amplie o motor para empurrar uma caixa e deduzir a conclusão a partir da posição real dela. Continue em Terminal, no diretório /home/labex/project/sokoban. Cada execução inicia uma nova partida na memória; editar ou recompilar o programa não salva uma partida em andamento.

Antes de empurrar, verifique a casa além da caixa. Ela deve ser chão ou alvo sem outra caixa. Primeiro recuse os casos inválidos; depois altere caixa e jogador juntos. Isso impede empurrar através de paredes ou mover duas caixas adjacentes juntas em uma única ação. O índice da caixa é um int porque -1 significa ausência; use-o para acessar o vetor apenas após verificar box >= 0.

won() const observa a partida sem alterá-la. O laço de intervalo for (Point box : state.boxes) percorre por valor as coordenadas de todas as caixas. Cada caixa deve estar sobre um . do terreno fixo; apenas uma caixa posicionada em um mapa de duas caixas não basta. Pelo nosso contrato, um vetor de caixas vazio nunca é uma partida concluída. A vitória é calculada a cada vez, então volta a ser falsa quando uma caixa sai do alvo. Neste laboratório, undo ainda recusa deliberadamente; o histórico será a nova responsabilidade do próximo.

Substitua engine.cpp pela implementação completa de movimento e conclusão:

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

Digite d, d, pressionando Enter após cada um. O tabuleiro mostra * e Solved!. Digite outro d: a caixa sai do alvo, e o jogador fica sobre ele como +; a partida volta a Not solved. Um próximo d é bloqueado porque a caixa entraria na parede. O alvo fixo ainda existe sob o jogador. Digite q.

Experimente o armazém preparado de duas caixas passando o caminho do arquivo como argumento opcional do programa:

./sokoban levels/warehouse.txt

Uma rota possível é w, s, d, d, w, com Enter após cada comando. Pode escolher outra rota válida; não se exige a solução mais curta. As duas caixas devem mostrar * antes de aparecer Solved!. Digite q.

Um caminho de nível digitado incorretamente produz Cannot open level: ... e encerra o programa sem alterar nenhum arquivo de nível. Para observar intencionalmente esse erro comum, execute:

./sokoban levels/missing.txt

Recupere-se repetindo o comando anterior com levels/warehouse.txt. Basta corrigir o nome do arquivo; não é preciso redefinir o ambiente. Abrir o nível de novo inicia uma nova partida na memória.

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

All move checks passed abrange passos e empurrões válidos, ações inválidas, preservação dos alvos, um mapa já concluído e diferentes soluções de duas caixas. Guarde o motor e os arquivos preparados; seu sistema de movimento já permite jogar.

Resumo

Você separou o terreno fixo dos objetos móveis, recusou ações inválidas sem efeitos colaterais e concluiu dois pequenos armazéns. O próximo laboratório independente acrescenta histórico e desfazer.