diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c45d12..6551ad6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,18 @@ CPMAddPackage( GIT_TAG v1.15.3 ) +# --- GoogleTest --- +CPMAddPackage( + NAME googletest + GITHUB_REPOSITORY google/googletest + GIT_TAG v1.15.2 + OPTIONS + "INSTALL_GTEST OFF" + "gtest_force_shared_crt ON" +) + +enable_testing() + add_library(imgui STATIC ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp @@ -203,15 +215,6 @@ add_library(Engine STATIC engine/src/core/animation/components/TransformAnimationComponent.h engine/src/core/animation/AnimationSystem.cpp engine/src/core/animation/AnimationSystem.h -) -target_include_directories(Engine PUBLIC engine/src ${dr_libs_SOURCE_DIR}) -target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui spdlog::spdlog tinyobjloader stb_image OpenAL::OpenAL) - -# --- Executable --- -add_executable(ColorRace - game/src/main.cpp - game/src/ColorRaceApp.cpp - game/src/ColorRaceApp.h engine/src/renderer/shader/StaticShader.cpp engine/src/renderer/shader/StaticShader.h engine/src/renderer/entities/Camera.cpp @@ -236,14 +239,22 @@ add_executable(ColorRace engine/src/renderer/MeshRenderer.h engine/src/layer/Layer.cpp engine/src/layer/Layer.h - game/src/GameLayer.cpp - game/src/GameLayer.h - engine/src/layer/SceneManager.cpp - engine/src/layer/SceneManager.h - game/src/GameScene.cpp - game/src/GameScene.h engine/src/core/inputsOutputs/context/InputContext.cpp engine/src/core/inputsOutputs/context/InputContext.h + engine/src/layer/SceneManager.cpp + engine/src/layer/SceneManager.h +) +target_include_directories(Engine PUBLIC engine/src ${dr_libs_SOURCE_DIR}) +target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui spdlog::spdlog tinyobjloader stb_image OpenAL::OpenAL) + +# --- GameLib: Spiel-Logik, unabhängig von main.cpp testbar --- +add_library(GameLib STATIC + game/src/ColorRaceApp.cpp + game/src/ColorRaceApp.h + game/src/GameLayer.cpp + game/src/GameLayer.h + game/src/GameScene.cpp + game/src/GameScene.h game/src/ludo/LudoGameState.cpp game/src/ludo/LudoGameState.h game/src/ludo/LudoTypes.h @@ -259,9 +270,34 @@ add_executable(ColorRace game/src/ludo/ecs/highlight/HighlightSystem.h game/src/AudioLayer.cpp game/src/AudioLayer.h + game/src/ludo/DiceSource.cpp + game/src/ludo/DiceSource.h +) +target_include_directories(GameLib PUBLIC game/src) +target_link_libraries(GameLib PUBLIC Engine) +# --- Executable --- +add_executable(ColorRace + game/src/main.cpp ) target_link_libraries(ColorRace PRIVATE Engine -) \ No newline at end of file + GameLib +) + +# --- Tests --- +add_executable(ColorRaceTests + tests/LoduGameMode.cpp + tests/helpers/SequenceDiceSource.cpp + tests/helpers/SequenceDiceSource.h +) + +target_link_libraries(ColorRaceTests PRIVATE + GameLib + GTest::gtest + GTest::gtest_main +) + +include(GoogleTest) +gtest_discover_tests(ColorRaceTests) \ No newline at end of file diff --git a/engine/src/core/animation/AnimationSystem.cpp b/engine/src/core/animation/AnimationSystem.cpp index 2469783..57f6e12 100644 --- a/engine/src/core/animation/AnimationSystem.cpp +++ b/engine/src/core/animation/AnimationSystem.cpp @@ -30,24 +30,4 @@ namespace engine::animation { } } } - - glm::vec3 AnimationSystem::evaluate(const AnimationTrack &track, float time) { - Keyframe a; - Keyframe b; - findSurroundingKeys(track, time, a, b); - - float localT = (time - a.time) / (b.time - a.time); - float easedT = a.easingFunction(localT); - return glm::mix(a.value, b.value, easedT); - } - - void AnimationSystem::findSurroundingKeys(const AnimationTrack &track, float time, Keyframe &a, Keyframe &b) { - for (int i = 1; i < track.keys.size(); i++) { - if (track.keys[i-1].time < 0.f && track.keys[i].time > 0.f) { - a = track.keys[i-1]; - b = track.keys[i]; - return; - } - } - } } // engine \ No newline at end of file diff --git a/engine/src/core/animation/AnimationSystem.h b/engine/src/core/animation/AnimationSystem.h index fa2f5b0..488a343 100644 --- a/engine/src/core/animation/AnimationSystem.h +++ b/engine/src/core/animation/AnimationSystem.h @@ -14,8 +14,43 @@ namespace engine::animation { void update(float deltaTime); private: EntityRegistry& m_registry; - glm::vec3 evaluate(const AnimationTrack& track, float time); - void findSurroundingKeys(const AnimationTrack& track, float time, Keyframe& a, Keyframe& b); + + template + glm::vec3 evaluate(const AnimationTrack& track, float time) { + Keyframe a; + Keyframe b; + findSurroundingKeys(track, time, a, b); + + float localT = (time - a.time) / (b.time - a.time); + float easedT = a.easingFunction(localT); + return glm::mix(a.value, b.value, easedT); + } + + template + void findSurroundingKeys(const AnimationTrack& track, float time, Keyframe& a, Keyframe& b) { + if (track.keys.size() == 1) { + a = b = track.keys.front(); + return; + } + + if (time <= track.keys.front().time) { + a = b = track.keys.front(); + return; + } + + if (time >= track.keys.back().time) { + a = b = track.keys.back(); + return; + } + + for (size_t i = 1; i < track.keys.size(); ++i) { + if (track.keys[i - 1].time <= time && track.keys[i].time >= time) { + a = track.keys[i - 1]; + b = track.keys[i]; + return; + } + } + } }; } // engine diff --git a/engine/src/core/animation/components/TransformAnimationComponent.h b/engine/src/core/animation/components/TransformAnimationComponent.h index 17b4384..b2600c6 100644 --- a/engine/src/core/animation/components/TransformAnimationComponent.h +++ b/engine/src/core/animation/components/TransformAnimationComponent.h @@ -11,26 +11,29 @@ namespace engine::animation { using EasingFunction = std::function; + template struct Keyframe { float time; - glm::vec3 value; + T value; EasingFunction easingFunction = [](float t) { return t; }; }; + + template struct AnimationTrack { - std::vector keys; + std::vector> keys; }; struct TransformAnimationComponent { float time = 0.0f; float duration = 0.0f; - AnimationTrack position; - AnimationTrack rotation; - AnimationTrack scale; + AnimationTrack position; + AnimationTrack rotation; + AnimationTrack scale; std::function onFinished; bool loop = false; diff --git a/game/src/ludo/BoardLayout.cpp b/game/src/ludo/BoardLayout.cpp index aac6a66..81e07ff 100644 --- a/game/src/ludo/BoardLayout.cpp +++ b/game/src/ludo/BoardLayout.cpp @@ -5,7 +5,6 @@ #include "BoardLayout.h" #include - #include "spdlog/spdlog.h" glm::vec3 ludo::BoardLayout::getWorldPosition(ludo::PlayerColor color, ludo::PieceState state, int position) const { @@ -27,4 +26,4 @@ glm::vec3 ludo::BoardLayout::getWorldPosition(ludo::PlayerColor color, ludo::Pie return glm::vec3(0.0f); } return it->second; -} +} \ No newline at end of file diff --git a/game/src/ludo/DiceSource.cpp b/game/src/ludo/DiceSource.cpp new file mode 100644 index 0000000..383b243 --- /dev/null +++ b/game/src/ludo/DiceSource.cpp @@ -0,0 +1,10 @@ +// +// Created by sebastian on 05.08.26. +// + +#include "DiceSource.h" + +int ludo::RandomDiceSource::roll() { + std::uniform_int_distribution distribution(1, 6); + return distribution(m_generator); +} diff --git a/game/src/ludo/DiceSource.h b/game/src/ludo/DiceSource.h new file mode 100644 index 0000000..faf3219 --- /dev/null +++ b/game/src/ludo/DiceSource.h @@ -0,0 +1,26 @@ +// +// Created by sebastian on 05.08.26. +// + +#ifndef COLORRACE_DICESOURCE_H +#define COLORRACE_DICESOURCE_H +#include + + +namespace ludo { + class IDiceSource { + public: + virtual ~IDiceSource() = default; + virtual int roll() = 0; + }; + + class RandomDiceSource : public IDiceSource { + public: + int roll() override; + private: + std::mt19937 m_generator{std::random_device{}()}; + }; +} + + +#endif //COLORRACE_DICESOURCE_H diff --git a/game/src/ludo/LudoGameMode.cpp b/game/src/ludo/LudoGameMode.cpp index 9a3bda0..ff9747d 100644 --- a/game/src/ludo/LudoGameMode.cpp +++ b/game/src/ludo/LudoGameMode.cpp @@ -52,6 +52,7 @@ std::optional ludo::LudoGameMode::findLowestHomeSlotPiece(PlayerColor color return i; // erste Übereinstimmung = kleinster Home-Slot, dank Erzeugungsreihenfolge } } + return std::nullopt; } ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) { @@ -59,10 +60,13 @@ ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIn int fromPosition = piece.position; Destination destination = computeDestination(piece, diceValue).value(); + + PieceState startState = piece.state; + PieceState targetState = destination.state; piece.state = destination.state; piece.position = destination.position; + MoveResult result{fromPosition, piece.position, false, -1, targetState, startState}; - MoveResult result{fromPosition, piece.position, false, -1}; if (piece.state == PieceState::OnTrack) { for (int i = 0; i < static_cast(m_gameState.m_pieces.size()); ++i) { @@ -185,11 +189,10 @@ std::optional ludo::LudoGameMode::wouldCapture(int pieceIndex, int diceValu int ludo::LudoGameMode::rollDice() { - std::uniform_int_distribution distribution(1, 6); - return distribution(m_rng); + return m_diceSource->roll(); } -int ludo::LudoGameMode::getEntryOffset(PlayerColor color) const { +int ludo::LudoGameMode::getEntryOffset(PlayerColor color) { return static_cast(color) * 10; } @@ -201,3 +204,42 @@ bool ludo::LudoGameMode::isBlockedByOwnPiece(PlayerColor owner, PieceState state } return false; } + +std::vector ludo::LudoGameMode::computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition) { + std::vector path; + + if (startPosition == targetPosition && startingState == targetState) return path; + + PieceState state = startingState; + int position = startPosition; + + path.push_back({state, position}); + + while (state != targetState || position != targetPosition) { + switch (state) { + case PieceState::AtHome: + state = PieceState::OnTrack; + position = getEntryOffset(color); + break; + case PieceState::OnTrack: + if (position == getEntryOffset(color) && (targetState == PieceState::InHomeStretch || targetState == PieceState::Finished)) { + state = PieceState::InHomeStretch; + position = 0; + } else { + position = (position + 1) % kTrackLength; + } + break; + case PieceState::InHomeStretch: + if (targetState == PieceState::Finished && position == kHomeStretchLength - 1) { + state = PieceState::Finished; + } else { + ++position; + } + break; + case PieceState::Finished: + break; + } + path.push_back({state, position}); + } + return path; +} diff --git a/game/src/ludo/LudoGameMode.h b/game/src/ludo/LudoGameMode.h index 09b7749..9d75658 100644 --- a/game/src/ludo/LudoGameMode.h +++ b/game/src/ludo/LudoGameMode.h @@ -4,18 +4,25 @@ #ifndef COLORRACE_LUDOGAMEMODE_H #define COLORRACE_LUDOGAMEMODE_H +#include #include #include +#include "DiceSource.h" #include "LudoGameState.h" #include "LudoTypes.h" #include "game/TurnBasedGameMode.h" namespace ludo { + struct PathNode { + PieceState state; + int position; + }; + class LudoGameMode: public engine::TurnBasedGameMode { public: explicit LudoGameMode(std::vector players, std::optional seed = std::nullopt) : TurnBasedGameMode(std::move(players)), - m_rng(seed.value_or(std::random_device{}())), m_gameState(getPlayerOrder()) {} + m_diceSource(std::make_unique()), m_gameState(getPlayerOrder()) {} void sendCommand(const std::variant &command) override; std::vector pollEvents() override; @@ -25,12 +32,18 @@ namespace ludo { std::vector getMoveablePieceIndices(PlayerColor color, int diceValue) const; std::optional wouldCapture(int pieceIndex, int diceValue) const; + [[nodiscard]] static int getEntryOffset(PlayerColor color); + static std::vector computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition); + + void setDiceSource(std::unique_ptr diceSource) { m_diceSource = std::move(diceSource); } private: struct MoveResult { - int fromPosition; - int toPosition; + int fromPosition = 0; + int toPosition = 0; bool captured = false; int capturedPieceIndex = -1; + PieceState targetState = PieceState::OnTrack; + PieceState startState = PieceState::OnTrack; }; struct Destination { @@ -46,8 +59,6 @@ namespace ludo { void handle(const RollDiceCommand &command); - - void handle(const MovePieceCommand &command); void finishTurn(); @@ -56,9 +67,9 @@ namespace ludo { std::vector m_pendingEvents; int rollDice(); - std::mt19937 m_rng; + std::unique_ptr m_diceSource; + - [[nodiscard]] int getEntryOffset(PlayerColor color) const; [[nodiscard]] bool isBlockedByOwnPiece(PlayerColor owner, PieceState state, int position) const; }; diff --git a/game/src/ludo/LudoTypes.h b/game/src/ludo/LudoTypes.h index 8353d55..df79e3a 100644 --- a/game/src/ludo/LudoTypes.h +++ b/game/src/ludo/LudoTypes.h @@ -66,7 +66,7 @@ namespace ludo { struct RollDiceCommand { engine::PlayerID player; }; using LudoCommand = std::variant; - struct PieceMovedEvent { int pieceIndex; int fromPosition; int toPosition; bool captured; }; + struct PieceMovedEvent { int pieceIndex; int fromPosition; int toPosition; bool captured; PieceState targetState; PieceState startState; }; struct DiceRolledEvent { engine::PlayerID player; int diceValue; }; struct TurnChangedEvent {engine::PlayerID newPlayer; }; using LudoEvent = std::variant; diff --git a/game/src/ludo/PiecePresenter.cpp b/game/src/ludo/PiecePresenter.cpp index ec53a66..5da2349 100644 --- a/game/src/ludo/PiecePresenter.cpp +++ b/game/src/ludo/PiecePresenter.cpp @@ -5,6 +5,8 @@ #include "PiecePresenter.h" +#include "LudoGameMode.h" +#include "core/animation/components/TransformAnimationComponent.h" #include "ecs/standardComponents/MeshComponent.h" #include "ecs/standardComponents/PickableComponent.h" #include "ecs/standardComponents/TransformComponent.h" @@ -37,3 +39,5 @@ std::optional ludo::PiecePresenter::getPieceIndex(std::optional(std::distance(m_pieceEntities.begin(), it)); } + + diff --git a/tests/LoduGameMode.cpp b/tests/LoduGameMode.cpp new file mode 100644 index 0000000..5f8028b --- /dev/null +++ b/tests/LoduGameMode.cpp @@ -0,0 +1,19 @@ +#include "gtest/gtest.h" +// +// Created by sebastian on 05.08.26. +// +#include "helpers/SequenceDiceSource.h" +#include "ludo/LudoGameMode.h" + +TEST(LudoGameMode, SecondSixMovesOutOfBlockedEntry) { + ludo::LudoGameMode mode({0}); // ein Spieler reicht zum Isolieren + mode.setDiceSource(std::make_unique(std::vector{6,6})); + + mode.sendCommand(ludo::RollDiceCommand{0}); + ASSERT_EQ(mode.getCurrentPlayer(), 0u) << "Spieler sollte nach Extra-Turn gleich bleiben"; + ASSERT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingRoll); + + mode.sendCommand(ludo::RollDiceCommand{0}); + EXPECT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingPieceSelection) + << "Erwartet: Figur-Auswahl, da Entry blockiert"; +} \ No newline at end of file diff --git a/tests/helpers/SequenceDiceSource.cpp b/tests/helpers/SequenceDiceSource.cpp new file mode 100644 index 0000000..9c7de14 --- /dev/null +++ b/tests/helpers/SequenceDiceSource.cpp @@ -0,0 +1,11 @@ +// +// Created by sebastian on 05.08.26. +// + +#include "SequenceDiceSource.h" + +int SequenceDiceSource::roll() { + int v = m_diceValues.at(m_index); + m_index = std::min(m_index +1, m_diceValues.size() -1); + return v; +} diff --git a/tests/helpers/SequenceDiceSource.h b/tests/helpers/SequenceDiceSource.h new file mode 100644 index 0000000..91c4596 --- /dev/null +++ b/tests/helpers/SequenceDiceSource.h @@ -0,0 +1,21 @@ +// +// Created by sebastian on 05.08.26. +// + +#ifndef COLORRACE_SEQUENCEDICESOURCE_H +#define COLORRACE_SEQUENCEDICESOURCE_H +#include "ludo/DiceSource.h" + + +class SequenceDiceSource : public ludo::IDiceSource{ +public: + explicit SequenceDiceSource(const std::vector &diceValues) : m_diceValues(diceValues) {} + int roll() override; + +private: + std::vector m_diceValues; + size_t m_index = 0; +}; + + +#endif //COLORRACE_SEQUENCEDICESOURCE_H