From d8ae57eb3e40e375e996db39bbae6c78deb1b527 Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 1 Feb 2025 18:12:42 +0100 Subject: [PATCH] big refactoring & many details --- TEST.txt | 7 ++ screens/completed_single_level.txt | 8 ++ screens/death.txt | 13 +-- screens/help.txt | 2 +- src/blockSettings.hpp | 1 + src/color.hpp | 1 + src/fileutils.hpp | 85 +++++++++++++++---- src/main.cpp | 128 ++++++++--------------------- src/movementHandler.hpp | 82 +++++++++++++++++- src/output.hpp | 83 +++++++++++++++++++ src/player.hpp | 27 ++++-- 11 files changed, 311 insertions(+), 126 deletions(-) create mode 100644 TEST.txt create mode 100644 screens/completed_single_level.txt create mode 100644 src/output.hpp diff --git a/TEST.txt b/TEST.txt new file mode 100644 index 0000000..a2f38e7 --- /dev/null +++ b/TEST.txt @@ -0,0 +1,7 @@ +Führe das Programm mit dem Argument --test aus + +asdddddddddddddddwwwwwwddddddwwwwdddd +aaaaaaassssssddddssssdddddddddddwwwwwddddddwwwwdddddddddddddddddddd +ddddddddddddddddddddddddddddddddddwwwwwdddddddddd +aaddddddddddwwwwwwddddddddddddddddddddddd +ddddwwwwwwddddddddddddddddddddddddddddddddddddaaaaaaaaaaaaaaaaaaaaassssdddddddddddddddddddddddddddddddddddddddddddddddddwwwwwwddddddddddddddd \ No newline at end of file diff --git a/screens/completed_single_level.txt b/screens/completed_single_level.txt new file mode 100644 index 0000000..87dae4b --- /dev/null +++ b/screens/completed_single_level.txt @@ -0,0 +1,8 @@ +┌────────────────────────────┐ +│ Du hast ▚▒░▞ │ +│ G E W O N N E N ! ▙▟ │ +│ Yay :) ▟▙ │ +└────────────────────────────┘ + +Du hast das Level erfolgreich abgeschlossen. +Wenn du mehr sehen möchtest, starte das Spiel ohne Kommandozeilenargumente. \ No newline at end of file diff --git a/screens/death.txt b/screens/death.txt index 97d00ca..a27ba64 100644 --- a/screens/death.txt +++ b/screens/death.txt @@ -1,5 +1,8 @@ -┌────────────────────────────┐ -│ Du bist ▞▀▀▀▚ │ -│ G E S T O R B E N ▙▝ ▘▟ │ -│ R.I.P. ▚▞▞ │ -└────────────────────────────┘ +┌────────────────────────────┐ +│ Du bist 💀 │ +│ G E S T O R B E N /|\ │ +│ R.I.P / \ │ +└────────────────────────────┘ + +Oh nein! +Paul ist tot und kann die Beachparty nicht genießen :( \ No newline at end of file diff --git a/screens/help.txt b/screens/help.txt index 67ecada..72a17b4 100644 --- a/screens/help.txt +++ b/screens/help.txt @@ -1,5 +1,5 @@ Welcome to Adventura v1.0 by Martin Prokoph! No Arguments: Play through all levels inside the world folder (in alphabetical order) ---level, -l: Load (only) the specified level +--level, -l : Load (only) the specified level --help, -h: Show this screen \ No newline at end of file diff --git a/src/blockSettings.hpp b/src/blockSettings.hpp index 38371b2..1d38947 100644 --- a/src/blockSettings.hpp +++ b/src/blockSettings.hpp @@ -1,3 +1,4 @@ +#pragma once class BlockSettings { public: BlockSettings() {} diff --git a/src/color.hpp b/src/color.hpp index 40b0220..a38330c 100644 --- a/src/color.hpp +++ b/src/color.hpp @@ -1,3 +1,4 @@ +#pragma once #include enum class Color { diff --git a/src/fileutils.hpp b/src/fileutils.hpp index 8d84c0c..136e587 100644 --- a/src/fileutils.hpp +++ b/src/fileutils.hpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include + +#include "color.hpp" using std::cin; using std::cout; @@ -13,7 +17,6 @@ using std::string; using std::vector; string readInput(string feedback); -string readFile(const string& fileLocation); /** * Reads a string from the user. The string is expected to be the first @@ -28,24 +31,70 @@ string readInput(string feedback) { return name; } -string readFile(const string& fileLocation) { - std::ifstream ifs(fileLocation); - return string((std::istreambuf_iterator ( ifs )), - (std::istreambuf_iterator())); +/** + * Get a list of all files in the specified directory, sorted alphabetically. + * + * This is used to progress through the worlds in the correct order. + * + * @param dir The directory to get the file names from + * @return A list of all file names in the specified directory, sorted alphabetically. + */ +vector getOrderedFileNames(string dir) { + vector worlds; + // Iterate over all files in the worlds directory + for (auto & entry : std::filesystem::directory_iterator(dir)) { + worlds.push_back(entry.path()); + } + // We use this to sort the worlds alphabetically, so that the game progresses in the correct order. + std::sort( worlds.begin(), worlds.end(), [](string a, string b) { + return a < b; + }); + return worlds; } + +/** + * Reads the given file and returns its content as a vector of strings. + * Each string represents a line in the file. + * + * @param fileLocation The location of the file to read. + * @return The content of the file as a vector of strings. + */ vector readFileAsVector(const string& fileLocation) { - string rawFile = readFile(fileLocation); - vector file; - string currentLine = ""; - for (char c : rawFile) { - if (c == '\n' || c == '\r') { - file.push_back(currentLine); - currentLine = ""; - } - else { - currentLine += c; - } + + vector lines; + // string currentLine = ""; + // for (char c : readFile(fileLocation)) { + // if (c == '\n' || c == '\r') { + // file.push_back(currentLine); + // currentLine = ""; + // } + // else { + // currentLine += c; + // } + // } + // file.push_back(currentLine); + + std::ifstream file(fileLocation); + // Read the file line by line into a string + string line; + while (std::getline(file, line)) { + lines.push_back(line); } - file.push_back(currentLine); - return file; + file.close(); + return lines; +} + +/** + * Prints the content of a file line by line into the console (in the specified color). + * We use this to print our death and victory screens. + * + * @param fileLocation Path to the file to be printed. + * @param color Color to be used for the output. + */ +void printFile(string fileLocation, Color color) { + cout << color; + vector file = readFileAsVector(fileLocation); + for (unsigned int y = 0; y < file.size(); y++) { + cout << file.at(y) << endl; + } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index bf402e7..8d4746a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,6 @@ #include #include -#include -#include + #include #include @@ -9,17 +8,17 @@ #include "player.hpp" #include "blockRegistry.hpp" #include "movementHandler.hpp" +#include "output.hpp" using std::string; using std::cout; using std::endl; -namespace fs = std::filesystem; -void render(World &world, Player &player); -void redraw(World &world, Player &player); -void jumpBackOneLine(); -void printFile(string fileLocation, Color color); bool startWorld(string worldFile); +vector getOrderedFileNames(string dir); + +bool testMode = false; +unsigned int worldIndex = 2; /** * Entry point of the program. @@ -32,30 +31,34 @@ int main(int argc, char *argv[]) { if (argc > 1) { for (int i = 1; i < argc; i++) { string arg = string(argv[i]); - if (arg == "-h" || arg == "--help") { - printFile("./screens/help.txt", Color::BRIGHT_BLUE); + if (arg == "-h" || arg == "--help") + break; + else if (arg == "-t" || arg == "--test") + testMode = true; + + else if ((arg == "-l" || arg == "--level") && argc > i + 1) { + if (!startWorld("./worlds/" + string(argv[i+1]))) + return 0; // Load only the specified world + else + printFile("./screens/completed_single_level.txt", Color::BRIGHT_GREEN); return 0; } - if ((arg == "-l" || arg == "--level") && argc > i + 1 && !startWorld("./worlds/" + string(argv[i+1]))) return 0; } - + if (!testMode) { + printFile("./screens/help.txt", Color::BRIGHT_BLUE); // Print help screen + return 0; + } } - else { + if (!testMode) { printFile("./screens/start.txt", Color::BRIGHT_YELLOW); waitForInput(); - vector worlds; - // Iterate over all files in the worlds directory - for (auto & entry : fs::directory_iterator("./worlds")) { - worlds.push_back(entry.path()); - } - // We use this to sort the worlds alphabetically, so that the game progresses in the correct order. - std::sort( worlds.begin(), worlds.end(), [](string a, string b) { - return a < b; - }); - // Load every world in order - for (const auto & world : worlds) - if (!startWorld(world)) return 0; + printGuide(); + waitForInput(); } + + // Load every world in order + for (const auto & world : getOrderedFileNames("./worlds")) + if (!startWorld(world)) return 0; // Print the victory screen once all levels have been completed printFile("./screens/victory.txt", Color::BRIGHT_GREEN); @@ -74,78 +77,11 @@ bool startWorld(string worldFile) { world.loadFromFile(worldFile); Player player = Player(world.getStartPos(), world); - render(world, player); - while (player.isAlive() && !player.hasReachedGoal()) { - char lastChar; - cin >> lastChar; - if (onInput(lastChar, world, player)) redraw(world, player); - else jumpBackOneLine(); - } + render(world, player.mapToWorldspace()); + + inputLoop(player, world, testMode, worldIndex); + + worldIndex++; if (!player.isAlive()) printFile("./screens/death.txt", Color::BRIGHT_RED); return player.hasReachedGoal(); -} - -/** - * Move the console cursor up by one line. - * Used to overwrite the previous line. - */ -void jumpBackOneLine() { - std::cout << "\033[1A"; -} - -/** - * Redraws the game world and player state on the console. - * This function first moves the console cursor up by the number of lines - * equivalent to the world's height, effectively clearing previous output. - * It then calls the render function to display the current state of the world - * and the player. - * - * @param world Reference to the World object representing the game's world. - * @param player Reference to the Player object representing the player's state. - */ -void redraw(World &world, Player &player) { - for (unsigned int y = 0; y <= world.getMaxY()+1; y++) { - jumpBackOneLine(); - } - render(world, player); -} - -/** - * Renders the current state of the game world and player onto the console. - * It prints the world's blocks with their respective colors and encodings (characters). - * On positions that overlap with the player texture, the relevant character of the player's texture is printed instead. - */ -void render(World &world, Player &player) { - vector> canvas = world.getFieldState(); - vector> playerTexture = player.mapToWorldspace(); - - for (unsigned int y = 0; y <= world.getMaxY(); y++) { - for (unsigned int x = 0; x <= world.getMaxX(); x++) { - if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable() - && playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') { - cout << Color::BRIGHT_YELLOW << playerTexture.at(y).at(x); - } - else if (canvas.size() > y && canvas.at(y).size() > x) { - cout << canvas.at(y).at(x).getColor() << canvas.at(y).at(x).getEncoding(); - } - else cout << ' '; - } - cout << endl; - } -} - -/** - * Prints the content of a file line by line onto the console, - * in the specified color. - * We use this to print our death and victory screens. - * - * @param fileLocation Path to the file to be printed. - * @param color Color to be used for the output. - */ -void printFile(string fileLocation, Color color) { - cout << color; - vector file = readFileAsVector(fileLocation); - for (unsigned int y = 0; y < file.size(); y++) { - cout << file.at(y) << endl; - } } \ No newline at end of file diff --git a/src/movementHandler.hpp b/src/movementHandler.hpp index f115052..9dc7215 100644 --- a/src/movementHandler.hpp +++ b/src/movementHandler.hpp @@ -1,6 +1,10 @@ +#pragma once +#include + #include "player.hpp" #include "world.hpp" #include "blockRegistry.hpp" +#include "output.hpp" bool tryWalk(World& world, Player& player, bool left); bool tryGoDown(World& world, Player& player); @@ -8,11 +12,33 @@ bool tryGoUp(World& world, Player& player); void tryPushBlock(BlockPos& blockPos, World& world, bool left); void tryBlockGravity(BlockPos& blockPos, World& world); +/** + * Checks if a given value is in a parameter pack of values. + * + * This is a C++17 implementation of a function that checks if a given value + * is in a parameter pack of values. This is useful for checking if a value is + * in a list of values without having to write a bunch of repetitive code. + * Source: https://stackoverflow.com/a/15181949 + * + * @param first The value to search for. + * @param t The parameter pack of values to search in. + * @return true if the value is found in the parameter pack, false otherwise. + */ +template +bool is_in(First &&first, T && ... t) { + return ((first == t) || ...); +} + +/** + * Waits until the user enters a key. + * Used to prompt the user to press any key to continue. + */ void waitForInput() { char lastChar = ' '; - while (lastChar == ' ') cin >> lastChar; + while (!is_in(lastChar, 'w', 'a', 's', 'd')) cin >> lastChar; } + /** * Processes the player's input and attempts to move the player in the game world * based on the input character. Supports moving up, left, down, or right @@ -46,6 +72,7 @@ bool onInput(char lastChar, World& world, Player& player) { default: return false; } } + /** * Attempts to move the player one block to the left or right. * @@ -75,6 +102,7 @@ bool tryWalk(World& world, Player& player, bool left) { } return false; } + /** * Attempts to move the player one block downwards. * @@ -93,6 +121,7 @@ bool tryGoDown(World& world, Player& player) { } return false; } + /** * Attempts to move the player one block upwards. * @@ -111,6 +140,19 @@ bool tryGoUp(World& world, Player& player) { } return false; } + +/** + * Attempts to push the block at the given position to the left or right. + * + * Checks if the block at the given position is pushable and if so, tries to push it + * to the left or right by swapping it with the block to its left/right. + * If the block to the left/right is also pushable, this function will be called + * recursively to handle the furthest block first. + * + * @param blockPos The position of the block to try to push. + * @param world Reference to the World object representing the current world. + * @param left Whether to push the block to the left (true) or right (false). + */ void tryPushBlock(BlockPos& blockPos, World& world, bool left) { BlockPos neighbourBlockPos = blockPos+(left ? BlockPos(-1, 0) : BlockPos(1, 0)); if (world.getBlockAt(blockPos).getSettings().isPushable()) { @@ -123,9 +165,47 @@ void tryPushBlock(BlockPos& blockPos, World& world, bool left) { } } } + +/** + * Checks if the block below the player's feet has gravity and if so, + * moves it down one block if possible. + * + * @param playerPos The position of the player. + * @param world Reference to the World object representing the current world. + */ void tryBlockGravity(BlockPos& playerPos, World& world) { if (world.getBlockAt(playerPos.add(0, 2)).getSettings().hasGravity() && world.getBlockAt(playerPos.add(0, 3)) == world.getBlockRegistry().AIR) { world.setBlockAt(playerPos.add(0, 3), world.getBlockAt(playerPos.add(0, 2))); world.setBlockAt(playerPos.add(0, 2), world.getBlockRegistry().AIR); } +} + +/** + * Listens for the player's input and updates the game state accordingly. + * If test mode is enabled, reads input from the file TEST.txt instead of the console. + * In this case, the game state is updated every 100 milliseconds (to simulate the player's input). + * If the player dies or reaches the goal, exit the loop. + */ +void inputLoop(Player& player, World& world, bool testMode, unsigned int worldIndex) { + vector testFile = readFileAsVector("TEST.txt"); + unsigned int inputIndex = 0; + while (player.isAlive() && !player.hasReachedGoal()) { + string currentInput; + if (testMode) { + currentInput = testFile[worldIndex][inputIndex]; + inputIndex++; + if (inputIndex > testFile[worldIndex].length()) break; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + else cin >> currentInput; + if (!testMode) { + jumpBackOneLine(); + } + + for (char lastChar : currentInput) { + if (onInput(lastChar, world, player)) + redraw(world, player.mapToWorldspace()); + } + } + inputIndex = 0; } \ No newline at end of file diff --git a/src/output.hpp b/src/output.hpp new file mode 100644 index 0000000..0a2c2cf --- /dev/null +++ b/src/output.hpp @@ -0,0 +1,83 @@ +#pragma once +#include +#include + +#include "world.hpp" + +using std::string; +using std::cout; +using std::endl; + +/** + * Move the console cursor up by one line. + * Used to overwrite the previous line. + */ +void jumpBackOneLine() { + std::cout << "\033[1A"; +} + +/** + * Renders the current state of the game world and player onto the console. + * It prints the world's blocks with their respective colors and encodings (characters). + * On positions that overlap with the player texture, the relevant character of the player's texture is printed instead. + * + * @param world Reference to the World object representing the current world. + * @param player Reference to the Player object representing the player's state. + */ +void render(World &world, vector> playerTexture) { + vector> canvas = world.getFieldState(); + + + for (unsigned int y = 0; y <= world.getMaxY(); y++) { + for (unsigned int x = 0; x <= world.getMaxX(); x++) { + if (!world.getBlockAt(BlockPos(x, y)).getSettings().isPushable() + && playerTexture.size() > y && playerTexture.at(y).size() > x && playerTexture.at(y).at(x) != ' ') { + cout << Color::BRIGHT_YELLOW << playerTexture.at(y).at(x); + } + else if (canvas.size() > y && canvas.at(y).size() > x) { + cout << canvas.at(y).at(x).getColor() << canvas.at(y).at(x).getEncoding(); + } + else cout << ' '; + } + cout << endl; + } +} + +/** + * Redraws the game world and player state on the console. + * This function first moves the console cursor up by the number of lines + * equivalent to the world's height, effectively clearing previous output. + * It then calls the render function to display the current state of the world + * and the player. + * + * @param world Reference to the World object representing the current world. + * @param player Reference to the Player object representing the player's state. + */ +void redraw(World &world, vector> playerTexture) { + for (unsigned int y = 0; y <= world.getMaxY(); y++) { + jumpBackOneLine(); + } + render(world, playerTexture); +} + +/** + * Prints a guide for the player, explaining what each block in the game + * represents. + */ +void printGuide() { + // We use a vector here instead of a map, because we want to keep this order + std::vector> guide = { + {"- Plattform", Color::RESET}, + {"H Leiter", Color::BRIGHT_MAGENTA}, + {"S Start", Color::RESET}, + {"O Ziel", Color::BRIGHT_GREEN}, + {"0 Wand", Color::RESET}, + {"^ Stacheln", Color::BRIGHT_RED}, + {"~ Wasser", Color::BRIGHT_BLUE}, + {"x Kiste", Color::BRIGHT_CYAN}, + {"* Sand", Color::BRIGHT_YELLOW} + }; + for (std::pair p : guide) { + cout << p.second << p.first << endl; + } +} \ No newline at end of file diff --git a/src/player.hpp b/src/player.hpp index 401420a..ba3e62b 100644 --- a/src/player.hpp +++ b/src/player.hpp @@ -1,17 +1,17 @@ #pragma once #include +#include +#include + #include "blockPos.hpp" +#include "output.hpp" class Player { public: Player(BlockPos pos, World& world) : world(world) { this->pos = pos; this->world = world; - playerTexture = {{ - {' ', 'o', ' '}, - {'/', '|', '\\'}, - {'/', ' ', '\\'} - }}; // Player pos is at the center '|' char + playerTexture = REGULAR_PLAYER_TEXTURE; } BlockPos getPos() { @@ -37,11 +37,15 @@ public: isFreeFalling = !world.getBlockAt(pos.add(0, 2)).getSettings().isSolid(); if (isFreeFalling) { fallLength += 1; + if (fallLength > 2) playerTexture = FALLING_PLAYER_TEXTURE; + redraw(world, this->mapToWorldspace()); + std::this_thread::sleep_for(std::chrono::milliseconds(100 / fallLength + 50)); move(0, 1); } else { if (fallLength > 5) alive = false; fallLength = 0; + playerTexture = REGULAR_PLAYER_TEXTURE; } if (world.getBlockAt(pos.add(0, 2)).getSettings().isLethal()) alive = false; @@ -82,4 +86,17 @@ private: bool isFreeFalling = false; bool reachedGoal = false; int fallLength = 0; + + const std::array, 3> REGULAR_PLAYER_TEXTURE {{ + {' ', 'o', ' '}, + {'/', '|', '\\'}, + {'/', ' ', '\\'} + } // Player pos is at the center '|' char + }; + const std::array, 3> FALLING_PLAYER_TEXTURE {{ + {'\\', 'o', '/'}, + {' ', '|', ' '}, + {'/', ' ', '\\'} + } // Player pos is at the center '|' char + }; }; \ No newline at end of file