Introduction
Implémentez les déplacements autorisés et la poussée d’une seule caisse dans un jeu d’entrepôt C++ jouable au terminal.
Vous travaillerez sur un petit jeu de terminal dont le terrain fixe est séparé des objets mobiles. Vous implémenterez les fonctions C++ centrales, puis jouerez pour vérifier leur comportement.
Marcher sans traverser les murs
Dans cette étape, jouez avec un exemple complet, puis faites respecter le plateau aux déplacements de votre joueur. Commencez dans l’onglet Terminal. Si vous utilisez l’éditeur WebIDE, ouvrez Terminal > New Terminal ; toutes les commandes suivantes s’exécutent dans ce shell. cd change le répertoire de travail de ce shell ; ce changement ne se transmet pas aux nouvelles sessions de terminal.
cd /home/labex/project/sokoban
Le programme préparé sample est une démonstration complète, distincte de votre programme inachevé. ./ exécute un programme du répertoire courant.
./sample
Saisissez d, appuyez sur Enter, puis saisissez encore d et appuyez sur Enter. Ces deux déplacements approchent le joueur de la caisse, puis la poussent sur la cible. Solved! apparaît et $ devient *. Saisissez u et appuyez sur Enter : le joueur et la caisse retrouvent leurs cases précédentes, et le message devient Not solved. Saisissez q et appuyez sur Enter pour revenir au shell. Une commande contient une lettre par ligne ; une commande vide ou inconnue affiche de l’aide sans rien modifier. L’exemple montre le résultat visé ; son exécution ne termine pas votre propre moteur.
La classe préparée Game est déclarée dans engine.hpp. Point contient les coordonnées entières x et y : x augmente vers la droite et y vers le bas. State contient le joueur et un std::vector<Point> comprenant toutes les caisses. terrain[y][x] ne contient que des murs #, des espaces et des cibles .. const rend ce terrain accessible en lecture seule après sa construction : une caisse mobile ne peut donc pas effacer une cible. Le rendu combine ces deux couches : * représente une caisse sur une cible et +, le joueur sur une cible.
floor(point) indique si une coordonnée se trouve dans le plateau et n’est pas un mur. boxAt(point) renvoie l’indice d’une caisse dans le vecteur, ou -1 si aucune caisse n’occupe la case. Ces fonctions auxiliaires, le chargement, le rendu et la lecture des commandes sont préparés. Vous implémentez move, won, puis undo dans engine.cpp ; conservez engine.hpp, support.cpp, main.cpp, levels/ et sample. Vous pouvez ajouter des fonctions auxiliaires et utiliser des algorithmes équivalents. Les contrôles évaluent le comportement, pas une écriture ou un parcours particulier.
Lisez la déclaration préparée et votre code de départ :
cat engine.hpp
cat engine.cpp
Le programme de départ refuse tout déplacement. Ce contrôle volontaire affiche donc FAIL walk changes only the player et se termine avec un code non nul ; cela signale du travail restant, pas un environnement défectueux :
bash /opt/labex-sokoban/check.sh walk
Les cartes acceptées sont des grilles ASCII rectangulaires fermées d’au moins trois lignes et trois colonnes. Elles contiennent exactement un joueur, au moins une caisse et autant de cibles que de caisses, sans chevauchement d’objets. Les symboles sont # pour un mur, un espace pour le sol, . pour une cible, @ pour le joueur, $ pour une caisse, + pour le joueur sur une cible et * pour une caisse sur une cible. Une carte peut être insoluble ou déjà terminée au départ. Le chargeur préparé refuse les autres entrées ; vous n’avez pas à écrire d’analyseur.
move(dx, dy) n’accepte que (1,0), (-1,0), (0,1) ou (0,-1). Toute autre paire d’entiers doit renvoyer false sans changer l’état ni l’historique. Une action autorisée renvoie true ; une action interdite renvoie false et ne modifie rien. L’ordre des caisses dans le vecteur n’importe pas pour l’occupation des cases. Plus tard, l’annulation restaurera l’état complet réellement enregistré avant cette action, y compris l’ordre des caisses à cet instant.
Commencez uniquement par la marche. Point{...} construit une valeur de coordonnées. && exige les deux conditions ; || en exige au moins une. Énumérer explicitement les quatre directions avant d’ajouter les coordonnées permet aussi de refuser sans risque des entiers exceptionnellement grands. >= 0 indique une case occupée par une caisse. À ce stade, les caisses bloquent la marche ; la poussée vient ensuite. Les branches return false ne modifient aucun état.
Pour écrire le fichier entier depuis le terminal, cat > engine.cpp redirige le texte vers ce fichier et remplace son ancien contenu. Le marqueur de document intégré entre guillemets <<'CPP' demande au shell de prendre les lignes suivantes littéralement, jusqu’à une ligne contenant uniquement CPP. Copiez toute la commande, y compris son marqueur 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
Compilez les trois fichiers source. -std=c++17 sélectionne C++17, -Wall -Wextra active des avertissements utiles et -o sokoban nomme l’exécutable. Les erreurs de compilation indiquent un numéro de ligne ; corrigez cette ligne dans engine.cpp (ou répétez le remplacement complet), puis relancez la même compilation.
g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban
Saisissez a, puis Enter : le mur de gauche provoque Blocked et le plateau ne change pas. Saisissez d, puis Enter : le joueur marche à droite. Un deuxième d reste bloqué par la caisse, ce qui est attendu à cette étape. u affiche No earlier move : l’annulation n’est pas encore implémentée. Saisissez q pour quitter.
bash /opt/labex-sokoban/check.sh walk
All walk checks passed confirme la marche dans les quatre directions et les refus sûrs. Les contrôles compilent votre moteur réel dans un répertoire temporaire et exécutent également votre exécutable sokoban existant dans des parties isolées. Reconstruisez sokoban après modification : un exécutable absent ou périmé constitue une livraison incomplète. Les contrôles ne déplacent rien dans votre partie en cours et ne modifient pas votre source.
Pousser une caisse et conserver la cible
Dans cette étape, étendez votre moteur pour pousser une caisse et déduire la réussite de sa position réelle. Continuez dans Terminal, dans /home/labex/project/sokoban. Chaque exécution démarre une nouvelle partie en mémoire ; modifier ou reconstruire le programme ne sauvegarde pas une partie en cours.
Avant de pousser, vérifiez la case au-delà de la caisse. Elle doit être un sol ou une cible sans autre caisse. Effectuez d’abord les refus, puis modifiez ensemble la caisse et le joueur. Cela empêche de pousser à travers les murs ou de pousser une chaîne de deux caisses. L’indice de la caisse est un int, car -1 signifie qu’elle est absente ; ne l’utilisez pour accéder au vector qu’après avoir vérifié box >= 0.
won() const observe la partie sans la modifier. La boucle de plage for (Point box : state.boxes) parcourt par valeur les coordonnées de chaque caisse. Toutes les caisses doivent être sur un . du terrain fixe ; une seule caisse placée dans une carte à deux caisses ne suffit pas. Selon notre contrat, un vecteur de caisses vide n’est jamais une partie terminée. La victoire est recalculée à chaque fois : elle redevient fausse lorsqu’une caisse quitte sa cible. Dans ce laboratoire, undo refuse encore volontairement ; l’historique sera la nouvelle responsabilité du suivant.
Remplacez engine.cpp par l’implémentation complète du mouvement et de la réussite :
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
Saisissez d, d, chacun suivi d’Enter. Le plateau affiche * et Solved!. Saisissez encore d : la caisse quitte la cible et le joueur s’y tient sous la forme + ; la partie revient à Not solved. Un autre d est bloqué, car la caisse entrerait dans le mur. La cible fixe existe toujours sous le joueur. Saisissez q.
Essayez l’entrepôt préparé à deux caisses en passant son chemin de fichier comme argument facultatif du programme :
./sokoban levels/warehouse.txt
Un parcours possible est w, s, d, d, w, chaque commande étant suivie d’Enter. Vous pouvez choisir un autre parcours autorisé ; la solution la plus courte n’est pas exigée. Les deux caisses doivent afficher * avant l’apparition de Solved!. Saisissez q.
Un chemin de niveau mal saisi produit Cannot open level: ... et quitte sans modifier aucun fichier de niveau. Pour observer volontairement cette erreur ordinaire, exécutez :
./sokoban levels/missing.txt
Pour reprendre, répétez la commande précédente avec levels/warehouse.txt. Il suffit de corriger le nom du fichier, sans réinitialiser l’environnement. Relancer un niveau démarre une nouvelle partie en mémoire.
bash /opt/labex-sokoban/check.sh move
All move checks passed couvre les pas et poussées autorisés, les actions invalides, la conservation des cibles, une carte déjà terminée et différentes solutions à deux caisses. Conservez le moteur et les fichiers préparés : vous disposez désormais d’un système de mouvement jouable.
Résumé
Vous avez séparé le terrain fixe des objets mobiles, refusé les actions invalides sans effet secondaire et terminé deux petits entrepôts. Le laboratoire indépendant suivant ajoute l’historique et l’annulation.



