diff --git a/.gitignore b/.gitignore index 46f42f8..378eac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1 @@ -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..775c834 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.0) +project(CppConsoleGame VERSION 1.0.0) + +add_subdirectory(conio) +add_subdirectory(random-utils) +add_subdirectory(cpp-console-game) \ No newline at end of file diff --git a/conio/CMakeLists.txt b/conio/CMakeLists.txt new file mode 100644 index 0000000..ad03c0c --- /dev/null +++ b/conio/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library( + conio + conio.hpp + conio.cpp + ) + +target_include_directories( + conio + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ) \ No newline at end of file diff --git a/conio/conio.cpp b/conio/conio.cpp new file mode 100644 index 0000000..f1007f1 --- /dev/null +++ b/conio/conio.cpp @@ -0,0 +1,30 @@ +#include "conio.hpp" + +#include +#include +#include + +/** + * Get a character from the console without needing to press enter + */ +char conio::getch() +{ + char c; + struct termios oldattr, newattr; + + // Get the terminal settings + tcgetattr(STDIN_FILENO, &oldattr); + newattr = oldattr; + + // Disable canonical mode and echo + newattr.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newattr); + + // Get the character, will not wait for enter because of the new settings + std::cin.get(c); + + // Reset the terminal settings back to normal behaviour + tcsetattr(STDIN_FILENO, TCSANOW, &oldattr); + + return c; +} \ No newline at end of file diff --git a/conio/conio.hpp b/conio/conio.hpp new file mode 100644 index 0000000..adc93f0 --- /dev/null +++ b/conio/conio.hpp @@ -0,0 +1,9 @@ +#ifndef CONIO_HPP +#define CONIO_HPP + +namespace conio +{ + char getch(); +} + +#endif // CONIO_HPP \ No newline at end of file diff --git a/cpp-console-game/Board.cpp b/cpp-console-game/Board.cpp new file mode 100644 index 0000000..f190224 --- /dev/null +++ b/cpp-console-game/Board.cpp @@ -0,0 +1,55 @@ +#include "Board.hpp" + +#include + +namespace ConsoleGame +{ + Board::Board(int width, int height, std::string fill) : fill(fill) + { + cells.resize(height); + for (int i = 0; i < height; i++) + { + cells[i].resize(width, fill); + } + } + + void Board::draw() + { + std::cout << "\033[H"; // Move cursor to the top left corner + std::cout << "\033[2J"; // Clear the screen + + for (auto row : cells) + { + for (auto cell : row) + { + std::cout << cell; + } + std::cout << std::endl; + } + } + + void Board::setCell(int x, int y, std::string value) + { + cells[y][x] = value; + } + + std::string Board::getCell(int x, int y) + { + return cells[y][x]; + } + + int Board::getWidth() + { + return cells[0].size(); + } + + int Board::getHeight() + { + return cells.size(); + } + + std::string Board::getFill() + { + return fill; + } +} \ No newline at end of file diff --git a/cpp-console-game/Board.hpp b/cpp-console-game/Board.hpp new file mode 100644 index 0000000..2156ffa --- /dev/null +++ b/cpp-console-game/Board.hpp @@ -0,0 +1,32 @@ +#ifndef BOARD_HPP +#define BOARD_HPP + +#include +#include + +namespace ConsoleGame +{ + class Board + { + public: + /** + * Construct a new Board object + * @param width The width of the board + * @param height The height of the board + * @param fill The string to fill the board with + */ + Board(int width, int height, std::string fill = "#"); + void draw(); + void setCell(int x, int y, std::string value); + std::string getCell(int x, int y); + int getWidth(); + int getHeight(); + std::string getFill(); + + private: + std::vector> cells; + std::string fill; + }; +} + +#endif // BOARD_HPP \ No newline at end of file diff --git a/cpp-console-game/CMakeLists.txt b/cpp-console-game/CMakeLists.txt new file mode 100644 index 0000000..d336365 --- /dev/null +++ b/cpp-console-game/CMakeLists.txt @@ -0,0 +1,40 @@ +add_library( + Board + Board.hpp + Board.cpp + ) + +add_library( + GameState + GameState.hpp + GameState.cpp + ) + +add_library( + Player + Player.hpp + Player.cpp + ) + +add_executable( + cpp-console-game + main.cpp + ) + +target_link_libraries( + cpp-console-game PRIVATE + GameState + conio + ) + +target_link_libraries( + GameState PRIVATE + Board + Player + RandomUtils + ) + +target_link_libraries( + Player PRIVATE + RandomUtils + ) \ No newline at end of file diff --git a/cpp-console-game/GameState.cpp b/cpp-console-game/GameState.cpp new file mode 100644 index 0000000..186c6d7 --- /dev/null +++ b/cpp-console-game/GameState.cpp @@ -0,0 +1,91 @@ +#include "GameState.hpp" + +#include + +int idCounter = 0; + +namespace ConsoleGame +{ + GameState::GameState(int width, int height, std::string fill) + : board(width, height, fill), + running(true), + players() {} + + int GameState::addPlayer() + { + std::lock_guard lock(mutex); + + // Add a player to the game at a random position + int x, y; + do + { + x = RandomUtils::rand(0, board.getWidth() - 1); + y = RandomUtils::rand(0, board.getHeight() - 1); + } while (board.getCell(x, y) != board.getFill()); + + // Increment the id counter and add the player to the map + int id = idCounter++; + players[id] = Player({x, y}); + board.setCell(x, y, players[id].getFill()); + + return id; + } + + void GameState::removePlayer(int id) + { + std::lock_guard lock(mutex); + + // Replace the player's position with a regular board cell + auto pos = players[id].getPosition(); + board.setCell(pos.first, pos.second, board.getFill()); + + players.erase(id); + } + + void GameState::movePlayer(int id, std::pair newPos) + { + std::lock_guard lock(mutex); + + // Check if the new position is out of bounds + if (newPos.first < 0 || newPos.first >= board.getWidth() || newPos.second < 0 || newPos.second >= board.getHeight()) + { + return; + } + + // Replace the player's old position with a regular board cell + auto oldPos = players[id].getPosition(); + board.setCell(oldPos.first, oldPos.second, board.getFill()); + + // Update the player's position and set the new position on the board + players[id].setPosition(newPos); + board.setCell(newPos.first, newPos.second, players[id].getFill()); + } + + std::pair GameState::getPlayerPosition(int id) + { + std::lock_guard lock(mutex); + + return players[id].getPosition(); + } + + void GameState::draw() + { + std::lock_guard lock(mutex); + + board.draw(); + } + + bool GameState::isRunning() + { + std::lock_guard lock(mutex); + + return running; + } + + void GameState::stop() + { + std::lock_guard lock(mutex); + + running = false; + } +} diff --git a/cpp-console-game/GameState.hpp b/cpp-console-game/GameState.hpp new file mode 100644 index 0000000..930c4fe --- /dev/null +++ b/cpp-console-game/GameState.hpp @@ -0,0 +1,68 @@ +#ifndef GAME_STATE_HPP +#define GAME_STATE_HPP + +#include "Board.hpp" +#include "Player.hpp" + +#include +#include +#include + +namespace ConsoleGame +{ + class GameState + { + public: + GameState(int width, int heigh, std::string fill = "#"); + + /** + * Add a player to the game + * @return New player id + */ + int addPlayer(); + + /** + * Remove a player from the game + * @param id Player id + */ + void removePlayer(int id); + + /** + * Move a player to a new position + * @param id Player id + * @param newPos New position of the player + */ + void movePlayer(int id, std::pair newPos); + + /** + * Get the position of a player + * @param id Player id + * @return Player position + */ + std::pair getPlayerPosition(int id); + + /** + * Draw the game board + */ + void draw(); + + /** + * Check if the game is running + * @return True if the game is running, false otherwise + */ + bool isRunning(); + + /** + * Stop the game + */ + void stop(); + + private: + Board board; + std::unordered_map players; + bool running = true; + std::mutex mutex; + }; +} + +#endif // GAME_STATE_HPP \ No newline at end of file diff --git a/cpp-console-game/Player.cpp b/cpp-console-game/Player.cpp new file mode 100644 index 0000000..84d520b --- /dev/null +++ b/cpp-console-game/Player.cpp @@ -0,0 +1,41 @@ +#include "Player.hpp" + +#include +#include + +namespace ConsoleGame +{ + int colors[] = { + 31, // Red + 32, // Green + 33, // Yellow + 36, // Cyan + }; + + Player::Player() : pos({0, 0}) + { + fill = "\033[32m@\033[0m"; + } + + Player::Player(std::pair pos) : pos(pos) + { + // Set the fill color to a random color + int color = colors[RandomUtils::rand(0, sizeof(colors) / sizeof(colors[0]) - 1)]; + fill = "\033[" + std::to_string(color) + "m@\033[0m"; + } + + std::pair Player::getPosition() + { + return pos; + } + + std::string Player::getFill() + { + return fill; + } + + void Player::setPosition(std::pair newPos) + { + pos = newPos; + } +} \ No newline at end of file diff --git a/cpp-console-game/Player.hpp b/cpp-console-game/Player.hpp new file mode 100644 index 0000000..1ff49d4 --- /dev/null +++ b/cpp-console-game/Player.hpp @@ -0,0 +1,25 @@ +#ifndef PLAYER_HPP +#define PLAYER_HPP + +#include + +namespace ConsoleGame +{ + class Player + { + public: + Player(); // TODO - Get rid of this constructor while keeping the compiler happy + Player(std::pair pos); + + std::pair getPosition(); + std::string getFill(); + + void setPosition(std::pair newPos); + + private: + std::pair pos; + std::string fill; + }; +} + +#endif // PLAYER_HPP \ No newline at end of file diff --git a/cpp-console-game/main.cpp b/cpp-console-game/main.cpp new file mode 100644 index 0000000..812260d --- /dev/null +++ b/cpp-console-game/main.cpp @@ -0,0 +1,60 @@ +#include "GameState.hpp" + +#include +#include +#include + +ConsoleGame::GameState gameState(20, 10); + +void inputLoop(int playerId) +{ + while (gameState.isRunning()) + { + char c = conio::getch(); + auto position = gameState.getPlayerPosition(playerId); + + switch (c) + { + case 'w': + gameState.movePlayer(playerId, {position.first, position.second - 1}); + break; + case 'a': + gameState.movePlayer(playerId, {position.first - 1, position.second}); + break; + case 's': + gameState.movePlayer(playerId, {position.first, position.second + 1}); + break; + case 'd': + gameState.movePlayer(playerId, {position.first + 1, position.second}); + break; + case 'q': + gameState.stop(); + break; + case 'p': + gameState.addPlayer(); + break; + } + } +} + +void renderLoop() +{ + while (gameState.isRunning()) + { + gameState.draw(); + usleep(100000); // Sleep for 100ms + } +} + +int main() +{ + int playerId = gameState.addPlayer(); + + std::thread inputThread(inputLoop, playerId); + std::thread renderThread(renderLoop); + + inputThread.join(); + renderThread.join(); + + return 0; +} \ No newline at end of file diff --git a/random-utils/CMakeLists.txt b/random-utils/CMakeLists.txt new file mode 100644 index 0000000..00e604c --- /dev/null +++ b/random-utils/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library( + RandomUtils + RandomUtils.hpp + RandomUtils.cpp + ) + +target_include_directories( + RandomUtils + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ) \ No newline at end of file diff --git a/random-utils/RandomUtils.cpp b/random-utils/RandomUtils.cpp new file mode 100644 index 0000000..e6dfd69 --- /dev/null +++ b/random-utils/RandomUtils.cpp @@ -0,0 +1,26 @@ +#include "RandomUtils.hpp" + +#include + +namespace RandomUtils +{ + bool seeded = false; + + void seed() + { + if (seeded) + { + return; + } + + srand(time(NULL)); + seeded = true; + } + + int rand(int min, int max) + { + seed(); + + return min + std::rand() % (max - min + 1); + } +} \ No newline at end of file diff --git a/random-utils/RandomUtils.hpp b/random-utils/RandomUtils.hpp new file mode 100644 index 0000000..5b1295d --- /dev/null +++ b/random-utils/RandomUtils.hpp @@ -0,0 +1,14 @@ +#ifndef RANDOM_HPP +#define RANDOM_HPP + +#include + +namespace RandomUtils +{ + /** + * Generates a random number between min and max (inclusive). + */ + int rand(int min = 0, int max = RAND_MAX); +} + +#endif // RANDOM_HPP \ No newline at end of file