From eecf4b1e666211a13afa56f93477c55e8fd01621 Mon Sep 17 00:00:00 2001 From: HampusM Date: Thu, 2 Jun 2022 19:51:54 +0200 Subject: feat: implement game of life --- CMakeLists.txt | 4 +- lib/CMakeLists.txt | 10 ++-- lib/doctest/CMakeLists.txt | 4 -- lib/gtest/CMakeLists.txt | 4 ++ src/CMakeLists.txt | 8 +++- src/bootstrap.cpp | 15 +++++- src/engine/data/vector2.cpp | 7 +++ src/engine/data/vector2.hpp | 2 + src/engine/graphics/scene.cpp | 2 +- src/engine/user/input.hpp | 6 +-- src/game/cell_helper.hpp | 34 +++++++++++++ src/game/cell_helper.tpp | 106 +++++++++++++++++++++++++++++++++++++++++ src/game/game.cpp | 103 +++++++++++++++++++++++++++++++++------ src/game/game.hpp | 19 ++++++-- src/game/status_manager.cpp | 2 + src/interfaces/cell_helper.hpp | 37 ++++++++++++++ src/interfaces/game.hpp | 2 +- src/interfaces/input.hpp | 4 +- src/interfaces/matrix.hpp | 11 +---- src/interfaces/statusline.hpp | 2 + src/util/algorithm.hpp | 31 ++++++++++++ src/util/algorithm.tpp | 57 ++++++++++++++++++++++ src/util/concepts.hpp | 78 ++++++++++++++++++++++++++++++ test/CMakeLists.txt | 41 ++++++++-------- test/cell_helper_test.cpp | 99 ++++++++++++++++++++++++++++++++++++++ test/function.test.cpp | 52 -------------------- test/main.cpp | 2 - test/mocks/matrix.hpp | 31 ++++++++++++ test/string_matrix.test.cpp | 53 --------------------- 29 files changed, 651 insertions(+), 175 deletions(-) delete mode 100644 lib/doctest/CMakeLists.txt create mode 100644 lib/gtest/CMakeLists.txt create mode 100644 src/game/cell_helper.hpp create mode 100644 src/game/cell_helper.tpp create mode 100644 src/interfaces/cell_helper.hpp create mode 100644 src/util/algorithm.hpp create mode 100644 src/util/algorithm.tpp create mode 100644 src/util/concepts.hpp create mode 100644 test/cell_helper_test.cpp delete mode 100644 test/function.test.cpp delete mode 100644 test/main.cpp create mode 100644 test/mocks/matrix.hpp delete mode 100644 test/string_matrix.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5afa219..b617393 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,13 +4,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS 1) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_EXTENSIONS off) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - project(game-of-life CXX) add_subdirectory(lib) add_subdirectory(src) -if("${TESTING}") +if(test) add_subdirectory(test) endif() diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 7fdf83c..df9b253 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -32,12 +32,12 @@ FetchContent_Declare( add_subdirectory(yacppdic) -if("${TESTING}") +if(test) FetchContent_Declare( - doctest - GIT_REPOSITORY "https://github.com/doctest/doctest" - GIT_TAG v2.4.8 + gtest + GIT_REPOSITORY "https://github.com/google/googletest" + GIT_TAG v1.11.0 ) - add_subdirectory(doctest) + add_subdirectory(gtest) endif() diff --git a/lib/doctest/CMakeLists.txt b/lib/doctest/CMakeLists.txt deleted file mode 100644 index 0ca5267..0000000 --- a/lib/doctest/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -message(STATUS "Fetching doctest...") - -FetchContent_MakeAvailable(doctest) - diff --git a/lib/gtest/CMakeLists.txt b/lib/gtest/CMakeLists.txt new file mode 100644 index 0000000..f28b693 --- /dev/null +++ b/lib/gtest/CMakeLists.txt @@ -0,0 +1,4 @@ +message(STATUS "Fetching gtest...") + +FetchContent_MakeAvailable(gtest) + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a566c12..0c576e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,12 @@ file(GLOB SOURCES add_executable(${PROJECT_NAME} ${SOURCES}) +set_target_properties( + ${PROJECT_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} +) + target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) target_compile_options( @@ -44,7 +50,7 @@ target_compile_options( -Wsign-conversion -Wsign-promo -Wstrict-overflow=5 -Wswitch-default -Wundef -Werror - -pedantic -fsanitize=address -fno-exceptions + -pedantic -fsanitize=address -fno-exceptions -fno-omit-frame-pointer ) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 02ec92d..adb53a2 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -2,6 +2,7 @@ // Interfaces #include "interfaces/argument_parser.hpp" +#include "interfaces/cell_helper.hpp" #include "interfaces/cursor.hpp" #include "interfaces/engine.hpp" #include "interfaces/game.hpp" @@ -23,6 +24,7 @@ #include "engine/graphics/statusline.hpp" #include "engine/user/cursor.hpp" #include "engine/user/input.hpp" +#include "game/cell_helper.hpp" #include "game/game.hpp" #include "game/generation_tracker.hpp" #include "game/status_manager.hpp" @@ -51,7 +53,7 @@ auto bootstrap() noexcept -> yacppdic::Container [&container]( const std::shared_ptr &scene, const std::shared_ptr &cursor_controller, - const std::shared_ptr user_input_observer) + const std::shared_ptr &user_input_observer) { std::shared_ptr statusline = container.get()(cursor_controller, scene); @@ -62,12 +64,15 @@ auto bootstrap() noexcept -> yacppdic::Container std::shared_ptr generation_tracker = container.get()(true); + const auto cell_helper_factory = container.get>(); + return std::make_unique( scene, cursor_controller, generation_tracker, status_manager, - user_input_observer); + user_input_observer, + cell_helper_factory(*(scene->get_matrix()))); }); container.bind().to_factory( @@ -109,5 +114,11 @@ auto bootstrap() noexcept -> yacppdic::Container return std::make_unique(is_paused); }); + container.bind>().to_factory( + [](const IMatrix &matrix) + { + return std::make_unique>(matrix); + }); + return container; } diff --git a/src/engine/data/vector2.cpp b/src/engine/data/vector2.cpp index 0f203d0..cb81947 100644 --- a/src/engine/data/vector2.cpp +++ b/src/engine/data/vector2.cpp @@ -2,6 +2,8 @@ #include "util/hash.hpp" +#include + auto Vector2::get_x() const noexcept -> Vector2::Value { return _x; @@ -79,6 +81,11 @@ auto Vector2::operator==(const Vector2 &rhs) const noexcept -> bool return _x == rhs._x && _y == rhs._y; } +auto Vector2::operator<(const Vector2 &rhs) const noexcept -> bool +{ + return std::tie(_x, _y) < std::tie(rhs._x, rhs._y); +} + auto Vector2Hasher::operator()(const Vector2 &vector2) const noexcept -> std::size_t { std::size_t result_hash = 0; diff --git a/src/engine/data/vector2.hpp b/src/engine/data/vector2.hpp index 620b41c..96ebf87 100644 --- a/src/engine/data/vector2.hpp +++ b/src/engine/data/vector2.hpp @@ -43,6 +43,8 @@ public: auto operator==(const Vector2 &rhs) const noexcept -> bool; + auto operator<(const Vector2 &rhs) const noexcept -> bool; + /** * Returns Vector2({.x = 0, .y = 1}) */ diff --git a/src/engine/graphics/scene.cpp b/src/engine/graphics/scene.cpp index 7a0e960..9f561f0 100644 --- a/src/engine/graphics/scene.cpp +++ b/src/engine/graphics/scene.cpp @@ -13,7 +13,7 @@ Scene::Scene( IMatrixFactory matrix_factory, std::shared_ptr cursor_controller) noexcept - : _matrix(matrix_factory(size() - Bounds({.width = 0U, .height = 1U}))), + : _matrix(matrix_factory(size())), _cursor_controller(std::move(cursor_controller)), _is_shown(false) { diff --git a/src/engine/user/input.hpp b/src/engine/user/input.hpp index 29534e8..9f565c1 100644 --- a/src/engine/user/input.hpp +++ b/src/engine/user/input.hpp @@ -16,13 +16,13 @@ public: void listen() noexcept override; - bool is_key_pressed(Key key) noexcept override; + auto is_key_pressed(Key key) noexcept -> bool override; - Key get_currently_pressed_key() const noexcept override; + [[nodiscard]] auto get_currently_pressed_key() const noexcept -> Key override; void clear_currently_pressed() noexcept override; private: - Key _currently_pressed; + Key _currently_pressed{}; std::mutex _currently_pressed_mutex; }; diff --git a/src/game/cell_helper.hpp b/src/game/cell_helper.hpp new file mode 100644 index 0000000..cf41c6f --- /dev/null +++ b/src/game/cell_helper.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "interfaces/cell_helper.hpp" +#include "interfaces/matrix.hpp" + +#include "engine/data/vector2.hpp" + +#include +#include + +template +class CellHelper : public ICellHelper +{ +public: + explicit CellHelper(const IMatrix &matrix) noexcept; + + [[nodiscard]] auto is_cell_dying(const Vector2 &cell_pos) const noexcept + -> bool override; + + [[nodiscard]] auto + get_birth_cell_positions(const std::list &cell_positions) const noexcept + -> std::list override; + + [[nodiscard]] auto find_neighbours(const Vector2 &cell_pos) const noexcept + -> std::list override; + +private: + const IMatrix &_matrix; + + static auto _get_position_neighbours(const Vector2 &position) noexcept + -> std::list; +}; + +#include "cell_helper.tpp" diff --git a/src/game/cell_helper.tpp b/src/game/cell_helper.tpp new file mode 100644 index 0000000..ebb35aa --- /dev/null +++ b/src/game/cell_helper.tpp @@ -0,0 +1,106 @@ +#pragma once + +#include "cell_helper.hpp" + +#include "util/algorithm.hpp" + +template +constexpr auto has_matrix_value( + const IMatrix &matrix, + const MatrixElement &value) noexcept +{ + return [&matrix, &value](const Vector2 &pos) + { + return matrix.get(pos) == value; + }; +} + +template +CellHelper::CellHelper(const IMatrix &matrix) noexcept + : _matrix(matrix) +{ +} + +template +auto CellHelper::is_cell_dying(const Vector2 &cell_pos) const noexcept + -> bool +{ + const auto neighbour_cell_positions = + container_filter(find_neighbours(cell_pos), has_matrix_value(_matrix, 'x')); + + const auto neighbour_cell_cnt = neighbour_cell_positions.size(); + + return neighbour_cell_cnt < 2 || neighbour_cell_cnt >= 4; +} + +template +auto CellHelper::get_birth_cell_positions( + const std::list &cell_positions) const noexcept -> std::list +{ + auto all_empty_neighbour_positions = std::list(); + + for (const auto &cell_pos : cell_positions) + { + const std::list empty_neighbour_positions = + container_filter(find_neighbours(cell_pos), has_matrix_value(_matrix, ' ')); + + all_empty_neighbour_positions.insert( + all_empty_neighbour_positions.end(), + empty_neighbour_positions.begin(), + empty_neighbour_positions.end()); + } + + // Remove duplicates + all_empty_neighbour_positions.sort(); + all_empty_neighbour_positions.unique(); + + auto birth_cell_positions = container_filter( + all_empty_neighbour_positions, + [this](const Vector2 &cell_pos) + { + const auto neighbour_cell_positions = container_filter( + find_neighbours(cell_pos), + has_matrix_value(_matrix, 'x')); + + return neighbour_cell_positions.size() == 3; + }); + + return birth_cell_positions; +} + +template +auto CellHelper::find_neighbours(const Vector2 &cell_pos) const noexcept + -> std::list +{ + std::list cell_positions = {}; + + const auto matrix_size = + Bounds({.width = _matrix.get_column_cnt(), .height = _matrix.get_row_cnt()}); + + const auto neighbours = _get_position_neighbours(cell_pos); + + for (const auto &neighbour_pos : neighbours) + { + if (matrix_size.validate_coords(neighbour_pos) == CoordsValidation::VALID) + { + cell_positions.push_back(neighbour_pos); + } + } + + return cell_positions; +} + +template +auto CellHelper::_get_position_neighbours( + const Vector2 &position) noexcept -> std::list +{ + return { + position + Vector2::up(), + position + Vector2::down(), + position + Vector2::left(), + position + Vector2::right(), + position + Vector2::up() + Vector2::left(), + position + Vector2::up() + Vector2::right(), + position + Vector2::down() + Vector2::left(), + position + Vector2::down() + Vector2::right()}; +} diff --git a/src/game/game.cpp b/src/game/game.cpp index 41bafdf..55877bb 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1,7 +1,10 @@ #include "game.hpp" +#include "util/algorithm.hpp" + #include +#include #include #include #include @@ -11,13 +14,14 @@ Game::Game( std::shared_ptr cursor_controller, std::shared_ptr generation_tracker, std::shared_ptr status_manager, - std::shared_ptr user_input_observer) noexcept + std::shared_ptr user_input_observer, + std::shared_ptr cell_helper) noexcept : _scene(std::move(scene)), _cursor_controller(std::move(cursor_controller)), _generation_tracker(std::move(generation_tracker)), _status_manager(std::move(status_manager)), _user_input_observer(std::move(user_input_observer)), - _gen_update_speed_millis(1000) + _cell_helper(std::move(cell_helper)) { } @@ -31,6 +35,8 @@ void Game::on_start() noexcept _status_manager->set_section_title(StatusLineSection::D, "Paused: "); _status_manager->set_section_title(StatusLineSection::E, "Generation: "); _status_manager->set_section_title(StatusLineSection::F, "Time since last frame: "); + _status_manager->set_section_title(StatusLineSection::G, "Living cells: "); + _status_manager->set_section_title(StatusLineSection::H, "Window size: "); const auto scene_size = _scene->size(); @@ -56,6 +62,15 @@ void Game::on_start() noexcept _status_manager->set_section_body(StatusLineSection::F, "0"); + _status_manager->set_section_body(StatusLineSection::G, "0"); + + _status_manager->set_section_body( + StatusLineSection::H, + fmt::format( + "Width {} Height {}", + scene_size.get_width(), + scene_size.get_height())); + _last_update_time = std::chrono::system_clock::now(); } @@ -64,6 +79,7 @@ void Game::on_update() noexcept const auto pressed_key = _user_input_observer->get_currently_pressed_key(); auto cursor_has_moved = false; + auto is_generation_stepping = false; switch (pressed_key) { @@ -91,8 +107,19 @@ void Game::on_update() noexcept std::exit(EXIT_SUCCESS); case 'i': - _insert_cell(_cursor_controller->where(), 'x'); + { + const auto position = _cursor_controller->where(); + const auto matrix = _scene->get_matrix(); + + if (matrix->get(position) == 'x') + { + break; + } + + _set_space(matrix, position, 'x'); + _living_cell_positions.push_back(position); break; + } case 'p': { @@ -104,6 +131,10 @@ void Game::on_update() noexcept break; } + case 's': + is_generation_stepping = true; + break; + default: break; } @@ -129,12 +160,21 @@ void Game::on_update() noexcept StatusLineSection::F, fmt::format("{} nanoseconds", time_since_last_update.count())); + _status_manager->set_section_body( + StatusLineSection::G, + fmt::format("{}", _living_cell_positions.size())); + + if (_generation_tracker->get_is_paused() && !is_generation_stepping) + { + _last_update_time = time_now; + return; + } + const auto time_since_last_gen_update = std::chrono::duration_cast( time_now - _last_gen_update_time); - if (!_generation_tracker->get_is_paused() && - time_since_last_gen_update.count() > _gen_update_speed_millis) + if (time_since_last_gen_update.count() > GENERATION_UPDATE_SPEED_MILLIS) { const auto new_current_gen = _generation_tracker->get_current_generation() + 1U; @@ -147,6 +187,36 @@ void Game::on_update() noexcept _last_gen_update_time = time_now; } + auto matrix = _scene->get_matrix(); + + const auto dying_cell_positions = container_filter( + _living_cell_positions, + [this](const Vector2 &cell_pos) + { + return _cell_helper->is_cell_dying(cell_pos); + }); + + auto birth_cell_positions = + _cell_helper->get_birth_cell_positions(_living_cell_positions); + + for (const auto &dying_cell_pos : dying_cell_positions) + { + _set_space(matrix, dying_cell_pos, ' '); + + const auto cell_found = container_find(_living_cell_positions, dying_cell_pos); + + if (cell_found != _living_cell_positions.end()) + { + _living_cell_positions.erase(cell_found); + } + } + + for (const auto &birth_cell_pos : birth_cell_positions) + { + _set_space(matrix, birth_cell_pos, 'x'); + _living_cell_positions.push_back(birth_cell_pos); + } + _last_update_time = time_now; } @@ -167,28 +237,33 @@ void Game::on_exit() const noexcept void Game::_move_cursor(const Vector2 &direction) noexcept { - const auto new_position = _cursor_controller->where().to_direction(direction, 1); + const auto current_position = _cursor_controller->where(); + + const auto dest_position = current_position + direction; const auto scene_size = _scene->size(); - if (scene_size.validate_coords(new_position) != CoordsValidation::VALID) + if (scene_size.validate_coords(dest_position) != CoordsValidation::VALID) { return; } - _cursor_controller->move_to(new_position); + _cursor_controller->move_to(dest_position); } -void Game::_insert_cell(const Vector2 &position, char cell) noexcept +void Game::_set_space( + const std::shared_ptr> &matrix, + const Vector2 &position, + char character) noexcept { - std::cout.put(cell); - std::cout.flush(); + const auto prev_position = _cursor_controller->where(); _cursor_controller->move_to(position); - auto matrix = _scene->get_matrix(); + std::cout.put(character); + std::cout.flush(); - const auto pos_offset = Vector2({.x = 0U, .y = 1U}); + matrix->set(position, character); - matrix->set(position - pos_offset, cell); + _cursor_controller->move_to(prev_position); } diff --git a/src/game/game.hpp b/src/game/game.hpp index 0ffa1d6..d29f4b6 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -1,15 +1,23 @@ #pragma once +#include "interfaces/cell_helper.hpp" #include "interfaces/cursor.hpp" #include "interfaces/game.hpp" #include "interfaces/generation_tracker.hpp" #include "interfaces/input.hpp" +#include "interfaces/matrix.hpp" #include "interfaces/scene.hpp" #include "interfaces/status_manager.hpp" +#include "engine/data/vector2.hpp" + #include +#include +#include #include +constexpr auto GENERATION_UPDATE_SPEED_MILLIS = 1000; + class Game : public IGame { public: @@ -18,7 +26,8 @@ public: std::shared_ptr cursor_controller, std::shared_ptr generation_tracker, std::shared_ptr status_manager, - std::shared_ptr user_input_observer) noexcept; + std::shared_ptr user_input_observer, + std::shared_ptr cell_helper) noexcept; void on_start() noexcept override; @@ -32,15 +41,19 @@ private: std::shared_ptr _generation_tracker; std::shared_ptr _status_manager; std::shared_ptr _user_input_observer; + std::shared_ptr _cell_helper; using TimePoint = std::chrono::system_clock::time_point; TimePoint _last_update_time; TimePoint _last_gen_update_time; - int _gen_update_speed_millis; + std::list _living_cell_positions; void _move_cursor(const Vector2 &direction) noexcept; - void _insert_cell(const Vector2 &position, char cell) noexcept; + void _set_space( + const std::shared_ptr> &matrix, + const Vector2 &position, + char character) noexcept; }; diff --git a/src/game/status_manager.cpp b/src/game/status_manager.cpp index 33174d1..4c189b9 100644 --- a/src/game/status_manager.cpp +++ b/src/game/status_manager.cpp @@ -18,6 +18,8 @@ void StatusManager::initialize() noexcept _statusline->set_section_length(StatusLineSection::D, 20U); _statusline->set_section_length(StatusLineSection::E, 25U); _statusline->set_section_length(StatusLineSection::F, 50U); + _statusline->set_section_length(StatusLineSection::G, 30U); + _statusline->set_section_length(StatusLineSection::H, 30U); _statusline->initialize_background(); } diff --git a/src/interfaces/cell_helper.hpp b/src/interfaces/cell_helper.hpp new file mode 100644 index 0000000..141d905 --- /dev/null +++ b/src/interfaces/cell_helper.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "interfaces/matrix.hpp" + +#include "engine/data/vector2.hpp" + +#include + +#include +#include + +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class ICellHelper +{ +public: + virtual ~ICellHelper() noexcept = default; + + [[nodiscard]] virtual auto is_cell_dying(const Vector2 &cell_pos) const noexcept + -> bool = 0; + + [[nodiscard]] virtual auto + get_birth_cell_positions(const std::list &cell_positions) const noexcept + -> std::list = 0; + + /* + [[nodiscard]] virtual auto + find_neighbour_cells(const Vector2 &cell_pos) const noexcept + -> std::list = 0; + */ + + [[nodiscard]] virtual auto find_neighbours(const Vector2 &cell_pos) const noexcept + -> std::list = 0; +}; + +template +using ICellHelperFactory = + yacppdic::Factory(const IMatrix &matrix)>; diff --git a/src/interfaces/game.hpp b/src/interfaces/game.hpp index e205fae..b6d7f36 100644 --- a/src/interfaces/game.hpp +++ b/src/interfaces/game.hpp @@ -24,4 +24,4 @@ public: using IGameFactory = yacppdic::Factory( const std::shared_ptr &scene, const std::shared_ptr &cursor_controller, - const std::shared_ptr user_input_observer)>; + const std::shared_ptr &user_input_observer)>; diff --git a/src/interfaces/input.hpp b/src/interfaces/input.hpp index c2ecefb..3558363 100644 --- a/src/interfaces/input.hpp +++ b/src/interfaces/input.hpp @@ -13,9 +13,9 @@ public: virtual void listen() noexcept = 0; - virtual bool is_key_pressed(Key key) noexcept = 0; + virtual auto is_key_pressed(Key key) noexcept -> bool = 0; - virtual Key get_currently_pressed_key() const noexcept = 0; + [[nodiscard]] virtual auto get_currently_pressed_key() const noexcept -> Key = 0; virtual void clear_currently_pressed() noexcept = 0; }; diff --git a/src/interfaces/matrix.hpp b/src/interfaces/matrix.hpp index 5a01943..010138f 100644 --- a/src/interfaces/matrix.hpp +++ b/src/interfaces/matrix.hpp @@ -9,15 +9,10 @@ #include template +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) class IMatrix { public: - IMatrix() noexcept = default; - - IMatrix(const IMatrix &matrix) noexcept = default; - - IMatrix(IMatrix &&matrix) noexcept = default; - virtual ~IMatrix() noexcept = default; virtual void fill(Element element) noexcept = 0; @@ -33,10 +28,6 @@ public: [[nodiscard]] virtual auto begin() const noexcept -> MatrixIterator = 0; [[nodiscard]] virtual auto end() const noexcept -> MatrixIterator = 0; - - auto operator=(const IMatrix &matrix) noexcept -> IMatrix & = default; - - auto operator=(IMatrix &&matrix) noexcept -> IMatrix & = default; }; template diff --git a/src/interfaces/statusline.hpp b/src/interfaces/statusline.hpp index 2fefa6d..a71699e 100644 --- a/src/interfaces/statusline.hpp +++ b/src/interfaces/statusline.hpp @@ -15,6 +15,8 @@ enum StatusLineSection D = 3, E = 4, F = 5, + G = 6, + H = 7 }; // NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) diff --git a/src/util/algorithm.hpp b/src/util/algorithm.hpp new file mode 100644 index 0000000..71a1724 --- /dev/null +++ b/src/util/algorithm.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "util/concepts.hpp" + +#include + +template +requires Container +constexpr auto container_find(const ContainerType &container, const Value &value) noexcept + -> typename ContainerType::const_iterator; + +template +requires Container +constexpr auto container_has(const ContainerType &container, const Value &value) noexcept + -> bool; + +template +requires Container && HasPushBack && + std::predicate +constexpr auto +container_filter(const ContainerType &container, Predicate predicate) noexcept + -> ContainerType; + +template +requires Container && + std::predicate +constexpr auto +container_filter(const ContainerType &container, Predicate predicate) noexcept + -> ContainerType; + +#include "algorithm.tpp" diff --git a/src/util/algorithm.tpp b/src/util/algorithm.tpp new file mode 100644 index 0000000..00269ed --- /dev/null +++ b/src/util/algorithm.tpp @@ -0,0 +1,57 @@ +#pragma once + +#include "algorithm.hpp" + +#include + +template +requires Container +constexpr auto container_find(const ContainerType &container, const Value &value) noexcept + -> typename ContainerType::const_iterator +{ + return std::find(container.begin(), container.end(), value); +} + +template +requires Container +constexpr auto container_has(const ContainerType &container, const Value &value) noexcept + -> bool +{ + return container_find(container, value) != container.end(); +} + +template +requires Container && HasPushBack && + std::predicate +constexpr auto +container_filter(const ContainerType &container, Predicate predicate) noexcept + -> ContainerType +{ + ContainerType filtered_container; + + std::copy_if( + std::begin(container), + std::end(container), + std::back_inserter(filtered_container), + predicate); + + return filtered_container; +} + +template +requires Container && + std::predicate +constexpr auto +container_filter(const ContainerType &container, Predicate predicate) noexcept + -> ContainerType +{ + ContainerType filtered_container; + + std::copy_if( + std::begin(container), + std::end(container), + std::inserter(filtered_container, filtered_container.begin()), + predicate); + + return filtered_container; +} diff --git a/src/util/concepts.hpp b/src/util/concepts.hpp new file mode 100644 index 0000000..928ee39 --- /dev/null +++ b/src/util/concepts.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +/** + * Concept for the Container named requirement. + * + * https://en.cppreference.com/w/cpp/named_req/Container + */ +template +concept Container = requires(ContainerType container_a, const ContainerType container_b) +{ + typename ContainerType::value_type; + typename ContainerType::reference; + typename ContainerType::const_reference; + typename ContainerType::iterator; + typename ContainerType::const_iterator; + typename ContainerType::difference_type; + typename ContainerType::size_type; + + requires std::regular; + requires std::swappable; + requires std::destructible; + requires std:: + same_as; + requires std::same_as< + typename ContainerType::const_reference, + const typename ContainerType::value_type &>; + requires std::forward_iterator; + requires std::forward_iterator; + requires std::signed_integral; + requires std::same_as< + typename ContainerType::difference_type, + typename std::iterator_traits::difference_type>; + requires std::same_as< + typename ContainerType::difference_type, + typename std::iterator_traits< + typename ContainerType::const_iterator>::difference_type>; + requires std::convertible_to< + typename ContainerType::iterator, + typename ContainerType::const_iterator>; + + { + container_a.begin() + } -> std::same_as; + { + container_a.end() + } -> std::same_as; + { + container_b.begin() + } -> std::same_as; + { + container_b.end() + } -> std::same_as; + { + container_a.cbegin() + } -> std::same_as; + { + container_a.cend() + } -> std::same_as; + { + container_a.size() + } -> std::same_as; + { + container_a.max_size() + } -> std::same_as; + { + container_a.empty() + } -> std::same_as; +}; + +template +concept HasPushBack = + requires(ContainerType container, typename ContainerType::value_type value) +{ + container.push_back(value); +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 243c1dd..0c74bf2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,18 +1,20 @@ +project(tests CXX) + +enable_testing() + file(GLOB SOURCES - main.cpp - string_matrix.test.cpp - "function.test.cpp" + cell_helper_test.cpp + ${CMAKE_SOURCE_DIR}/src/game/cell_helper.cpp ${CMAKE_SOURCE_DIR}/src/engine/data/vector2.cpp ${CMAKE_SOURCE_DIR}/src/engine/data/bounds.cpp - ${CMAKE_SOURCE_DIR}/src/engine/graphics/string_matrix.cpp ) -add_executable(tests ${SOURCES}) +add_executable(${PROJECT_NAME} ${SOURCES}) -target_compile_features(tests PUBLIC cxx_std_20) +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) target_compile_options( - tests + ${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Wshadow -Wold-style-cast -Wcast-align -Wno-unused @@ -26,21 +28,22 @@ target_compile_options( -pedantic -fsanitize=address -fno-exceptions ) -if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - target_compile_options( - tests - PRIVATE - -Wlogical-op -Wnoexcept -Wstrict-null-sentinel - ) -endif() - target_include_directories( - tests + ${PROJECT_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/src" - "${CMAKE_SOURCE_DIR}/test" ) -target_link_libraries(tests GSL doctest) +target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address) + +target_link_libraries( + ${PROJECT_NAME} + GSL + yacppdic + gtest_main + gmock +) + +include(GoogleTest) -target_link_options(tests PRIVATE -fsanitize=address) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/test/cell_helper_test.cpp b/test/cell_helper_test.cpp new file mode 100644 index 0000000..b570aba --- /dev/null +++ b/test/cell_helper_test.cpp @@ -0,0 +1,99 @@ +#include "engine/data/vector2.hpp" +#include "game/cell_helper.hpp" + +#include "mocks/matrix.hpp" + +#include + +#include +#include + +class CellHelperTest : public testing::Test +{ +protected: +}; + +TEST_F(CellHelperTest, IsCellDying) +{ + const auto mock_matrix = MockMatrix(); + + const auto rows = 8U; + const auto columns = 6U; + + const auto position = Vector2({.x = 3, .y = 5}); + + EXPECT_CALL(mock_matrix, get_row_cnt()).WillRepeatedly(testing::Return(rows)); + + EXPECT_CALL(mock_matrix, get_column_cnt()).WillRepeatedly(testing::Return(columns)); + + EXPECT_CALL(mock_matrix, get(position + Vector2::up())) + .WillOnce(testing::Return('x')) + .WillOnce(testing::Return(' ')) + .WillOnce(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::down())) + .WillRepeatedly(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::left())) + .WillOnce(testing::Return('x')) + .WillOnce(testing::Return('x')) + .WillOnce(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::right())) + .WillOnce(testing::Return('x')) + .WillOnce(testing::Return(' ')) + .WillOnce(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::up() + Vector2::left())) + .WillRepeatedly(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::up() + Vector2::right())) + .WillRepeatedly(testing::Return('x')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::down() + Vector2::left())) + .WillRepeatedly(testing::Return(' ')); + + EXPECT_CALL(mock_matrix, get(position + Vector2::down() + Vector2::right())) + .WillRepeatedly(testing::Return(' ')); + + auto cell_helper = CellHelper(mock_matrix); + + EXPECT_EQ(cell_helper.is_cell_dying(position), true); + + EXPECT_EQ(cell_helper.is_cell_dying(position), false); + + EXPECT_EQ(cell_helper.is_cell_dying(position), true); +} + +TEST_F(CellHelperTest, FindNeighbours) +{ + const auto mock_matrix = MockMatrix(); + + const auto rows = 8U; + const auto columns = 6U; + + const auto position = Vector2({.x = 3, .y = 5}); + + EXPECT_CALL(mock_matrix, get_row_cnt()).WillOnce(testing::Return(rows)); + + EXPECT_CALL(mock_matrix, get_column_cnt()).WillOnce(testing::Return(columns)); + + auto cell_helper = CellHelper(mock_matrix); + + const auto living_neighbour_cells = cell_helper.find_neighbours(position); + + EXPECT_EQ(living_neighbour_cells.size(), 8U); + + EXPECT_THAT( + living_neighbour_cells, + testing::ContainerEq(std::list( + {position + Vector2::up(), + position + Vector2::down(), + position + Vector2::left(), + position + Vector2::right(), + position + Vector2::up() + Vector2::left(), + position + Vector2::up() + Vector2::right(), + position + Vector2::down() + Vector2::left(), + position + Vector2::down() + Vector2::right()}))); +} + diff --git a/test/function.test.cpp b/test/function.test.cpp deleted file mode 100644 index 9aca0eb..0000000 --- a/test/function.test.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "util/function.hpp" - -#include -#include - -TEST_CASE("normalize_lamda") -{ - SUBCASE("Can return a function that returns a int") - { - const int number = 58; - - CHECK(normalize_lambda( - [number]() - { - return number; - })() == number); - } - - SUBCASE("Can preserve object state") - { - class Book - { - public: - Book() noexcept = default; - - void read_page() noexcept - { - _pages_read++; - } - - [[nodiscard]] uint32_t pages_read() const noexcept - { - return _pages_read; - } - - private: - uint32_t _pages_read{0U}; - }; - - auto book = Book(); - - book.read_page(); - book.read_page(); - book.read_page(); - - CHECK(normalize_lambda( - [book]() - { - return book.pages_read(); - })() == 3); - } -} diff --git a/test/main.cpp b/test/main.cpp deleted file mode 100644 index 0a3f254..0000000 --- a/test/main.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include diff --git a/test/mocks/matrix.hpp b/test/mocks/matrix.hpp new file mode 100644 index 0000000..1602ebf --- /dev/null +++ b/test/mocks/matrix.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "interfaces/matrix.hpp" + +#include + +template +class MockMatrix : public IMatrix +{ +public: + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(void, fill, (Element element), (noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(Element, get, (const Vector2 &pos), (const, noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(void, set, (const Vector2 &pos, Element element), (noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(uint32_t, get_row_cnt, (), (const, noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(uint32_t, get_column_cnt, (), (const, noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(MatrixIterator, begin, (), (const, noexcept, override)); + + // NOLINTNEXTLINE(modernize-use-trailing-return-type) + MOCK_METHOD(MatrixIterator, end, (), (const, noexcept)); +}; diff --git a/test/string_matrix.test.cpp b/test/string_matrix.test.cpp deleted file mode 100644 index 522a274..0000000 --- a/test/string_matrix.test.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "engine/graphics/string_matrix.hpp" -#include "engine/data/bounds.hpp" - -#include -#include - -constexpr uint32_t MATRIX_WIDTH = 76; -constexpr uint32_t MATRIX_HEIGHT = 31; - -TEST_CASE("String matrix") -{ - auto string_matrix = - StringMatrix(Bounds({.width = MATRIX_WIDTH, .height = MATRIX_HEIGHT})); - - SUBCASE("Can set & get elements") - { - // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) - const auto position = Vector2({.x = 56, .y = 20}); - - string_matrix.set(position, "#"); - - CHECK(string_matrix.get(position) == "#"); - } - - SUBCASE("Can iterate") - { - CHECK(std::is_same_v>); - - CHECK(std::is_same_v>); - - uint32_t row_iter_cnt = 0; - - for (auto row : string_matrix) - { - row_iter_cnt++; - - CHECK(std::is_same_v>); - - uint32_t col_iter_cnt = 0; - - for (auto &col : row) - { - col_iter_cnt++; - } - - CHECK(col_iter_cnt == MATRIX_WIDTH); - } - - CHECK(row_iter_cnt == MATRIX_HEIGHT); - } -} -- cgit v1.2.3-18-g5258