diff --git a/CMakeLists.txt b/CMakeLists.txt index daca027..7926419 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,6 +212,8 @@ add_executable(ColorRace game/src/ludo/BoardLayout.h game/src/ludo/PiecePresenter.cpp game/src/ludo/PiecePresenter.h + game/src/ludo/LudoAiController.cpp + game/src/ludo/LudoAiController.h ) diff --git a/engine/src/game/TurnBasedGameMode.h b/engine/src/game/TurnBasedGameMode.h index 3720f6b..fbd371a 100644 --- a/engine/src/game/TurnBasedGameMode.h +++ b/engine/src/game/TurnBasedGameMode.h @@ -18,11 +18,12 @@ namespace engine { explicit TurnBasedGameMode(std::vector playerOrder) : m_playerOrder(std::move(playerOrder)) {} [[nodiscard]] PlayerID getCurrentPlayer() const { return m_playerOrder[m_currentPlayerIndex]; } [[nodiscard]] bool isCurrentPlayer(PlayerID playerID) const { return getCurrentPlayer() == playerID; } + [[nodiscard]] std::vector getPlayerOrder() const { return m_playerOrder; } protected: void endTurn(bool extraTurn = false) { if (!extraTurn) m_currentPlayerIndex = (m_currentPlayerIndex + 1) % m_playerOrder.size(); } - std::vector getPlayerOrder() const { return m_playerOrder; } + private: std::vector m_playerOrder; size_t m_currentPlayerIndex = 0; diff --git a/engine/src/renderer/MeshRenderer.cpp b/engine/src/renderer/MeshRenderer.cpp index 64eb332..c0b9e71 100644 --- a/engine/src/renderer/MeshRenderer.cpp +++ b/engine/src/renderer/MeshRenderer.cpp @@ -13,7 +13,7 @@ void engine::MeshRenderer::render(RenderQueue &queue, const Camera &camera, cons m_shader->loadCameraPosition(camera.getPosition()); m_shader->loadLight(light.position, light.color); - for (const auto&[mesh, transformationMatrix] : queue.getRenderCommands()) { + for (const auto&[mesh, transformationMatrix] : queue.getRenderCommands()) { m_shader->loadTransformationMatrix(transformationMatrix); mesh->bind(); diff --git a/game/src/GameLayer.cpp b/game/src/GameLayer.cpp index 0637f33..4b810ac 100644 --- a/game/src/GameLayer.cpp +++ b/game/src/GameLayer.cpp @@ -56,6 +56,9 @@ void GameLayer::renderHud() { void GameLayer::onUpdate(float deltaTime) { Layer::onUpdate(deltaTime); m_cameraController->update(getKeyboard(), *m_camera, deltaTime); + for (auto& ai : aiControllers) { + ai.update(deltaTime); + } for (const auto& event : m_gameMode->pollEvents()) { std::visit([this](const T& e) { diff --git a/game/src/GameLayer.h b/game/src/GameLayer.h index e5ef097..a248a11 100644 --- a/game/src/GameLayer.h +++ b/game/src/GameLayer.h @@ -11,6 +11,7 @@ #include "ecs/systems/PickingSystem.h" #include "layer/Layer.h" #include "ludo/BoardLayout.h" +#include "ludo/LudoAiController.h" #include "ludo/PiecePresenter.h" #include "renderer/MeshRenderer.h" #include "renderer/RenderQueue.h" @@ -23,7 +24,13 @@ public: engine::Keyboard& keyboard, engine::Mouse& mouse, std::shared_ptr boardModel, std::unordered_map> pieceModdels) : Layer(keyboard, mouse), m_gameMode(std::move(gameMode)), m_entityRegistry(entityRegistry), m_camera(std::move(cam)), m_renderer(std::make_unique()), m_pointLight(std::move(light)), m_boardLayout(std::move(boardModel)), m_piecePresenter(m_entityRegistry, std::move(pieceModdels), m_boardLayout), - m_pickingSystem(*m_camera){} + m_pickingSystem(*m_camera) { + for (auto playerID : m_gameMode->getPlayerOrder()) { + if (playerID != m_localPlayer) { + aiControllers.emplace_back(m_gameMode, playerID); + } + } + } void onUpdate(float deltaTime) override; void onRender() override; @@ -46,6 +53,8 @@ private: engine::PickingSystem m_pickingSystem; std::string m_lastEventMessage; + std::vector aiControllers; + void renderHud(); }; diff --git a/game/src/ludo/LudoAiController.cpp b/game/src/ludo/LudoAiController.cpp new file mode 100644 index 0000000..88ae9c9 --- /dev/null +++ b/game/src/ludo/LudoAiController.cpp @@ -0,0 +1,59 @@ +// +// Created by sebastian on 02.08.26. +// + +#include "LudoAiController.h" + +#include +#include +#include + +ludo::LudoAiController::LudoAiController(std::shared_ptr gameMode, engine::PlayerID player) : m_gameMode(std::move(gameMode)), m_player(player){ +} + +void ludo::LudoAiController::update(float deltaTime) { + if (m_gameMode->getCurrentPlayer() != m_player) { + m_thinkTimer = 0.0f; + return; + } + + m_thinkTimer += deltaTime; + std::cout << "Thinking..." << std::endl; + + if (m_thinkTimer < kThinkDelay) return; + std::cout << "Thinking done" << std::endl; + m_thinkTimer = 0.0f; + + switch (m_gameMode->getState().getPhase()) { + case TurnPhase::AwaitingRoll: + m_gameMode->sendCommand(RollDiceCommand{m_player}); + break; + case TurnPhase::AwaitingPieceSelection: + if (auto pieceIndex = chooseMove()) { + m_gameMode->sendCommand(MovePieceCommand{m_player, *pieceIndex}); + } + break; + } + + +} + +std::optional ludo::LudoAiController::chooseMove() const { + auto color = m_gameMode->getColorOf(m_player); + auto moveable = m_gameMode->getMoveablePieceIndices(color, m_gameMode->getState().getDiceValue()); + if (moveable.empty()) return std::nullopt; + + int diceValue = m_gameMode->getState().getDiceValue(); + + //Priority 1: Schmeißen + for (int idx : moveable) { + if (m_gameMode->wouldCapture(idx, diceValue).has_value()) { + return idx; + } + } + for (int idx : moveable) { + if (m_gameMode->getState().getPieces()[idx].state == PieceState::AtHome) return idx; + } + // Fallback: erste bewegbare Figur + return moveable.front(); +} diff --git a/game/src/ludo/LudoAiController.h b/game/src/ludo/LudoAiController.h new file mode 100644 index 0000000..5b442ce --- /dev/null +++ b/game/src/ludo/LudoAiController.h @@ -0,0 +1,28 @@ +// +// Created by sebastian on 02.08.26. +// + +#ifndef COLORRACE_LUDOAICONTROLLER_H +#define COLORRACE_LUDOAICONTROLLER_H +#include + +#include "LudoGameMode.h" + + +namespace ludo { + class LudoAiController { + public: + LudoAiController(std::shared_ptr gameMode, engine::PlayerID player); + void update(float deltaTime); + private: + static constexpr float kThinkDelay = 0.8f; + + [[nodiscard]] std::optional chooseMove() const; + std::shared_ptr m_gameMode; + engine::PlayerID m_player; + float m_thinkTimer = 0.0f; + }; + +} + +#endif //COLORRACE_LUDOAICONTROLLER_H diff --git a/game/src/ludo/LudoGameMode.cpp b/game/src/ludo/LudoGameMode.cpp index 1e35406..9a3bda0 100644 --- a/game/src/ludo/LudoGameMode.cpp +++ b/game/src/ludo/LudoGameMode.cpp @@ -17,37 +17,50 @@ std::vector ludo::LudoGameMode::pollEvents() { return events; } -ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) { - Piece& piece = m_gameState.m_pieces[pieceIndex]; - int fromPosition = piece.position; - +[[nodiscard]] std::optional ludo::LudoGameMode::computeDestination(const Piece &piece, int diceValue) const { switch (piece.state) { - case PieceState::AtHome: { - piece.state = PieceState::OnTrack; - piece.position = getEntryOffset(piece.color); - break; - } + case PieceState::AtHome: + return Destination{PieceState::OnTrack, getEntryOffset(piece.color)}; + case PieceState::OnTrack: { int distanceTraveled = (piece.position - getEntryOffset(piece.color) + kTrackLength) % kTrackLength; int newDistance = distanceTraveled + diceValue; if (newDistance <= kTrackLength - 1) { - piece.position = (piece.position + diceValue) % kTrackLength; - } else { - int homeIndex = newDistance - kTrackLength; - piece.state = PieceState::InHomeStretch; - piece.position = homeIndex; + return Destination{PieceState::OnTrack, (piece.position + diceValue) % kTrackLength}; } - break; + int homeIndex = newDistance - kTrackLength; + if (homeIndex > kHomeStretchLength - 1) return std::nullopt; // überzählig + return Destination{homeIndex == kHomeStretchLength - 1 ? PieceState::Finished : PieceState::InHomeStretch, homeIndex}; } + case PieceState::InHomeStretch: { - piece.position += diceValue; - if (piece.position == kHomeStretchLength - 1) { - piece.state = PieceState::Finished; - } + int newIndex = piece.position + diceValue; + if (newIndex > kHomeStretchLength - 1) return std::nullopt; // überzählig + return Destination{newIndex == kHomeStretchLength - 1 ? PieceState::Finished : PieceState::InHomeStretch, newIndex}; } + case PieceState::Finished: - break; + return std::nullopt; // nie bewegbar } + return std::nullopt; +} + +std::optional ludo::LudoGameMode::findLowestHomeSlotPiece(PlayerColor color) const { + for (int i = 0; i < static_cast(m_gameState.m_pieces.size()); ++i) { + const auto& piece = m_gameState.m_pieces[i]; + if (piece.color == color && piece.state == PieceState::AtHome) { + return i; // erste Übereinstimmung = kleinster Home-Slot, dank Erzeugungsreihenfolge + } + } +} + +ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) { + Piece& piece = m_gameState.m_pieces[pieceIndex]; + int fromPosition = piece.position; + + Destination destination = computeDestination(piece, diceValue).value(); + piece.state = destination.state; + piece.position = destination.position; MoveResult result{fromPosition, piece.position, false, -1}; @@ -68,16 +81,34 @@ ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIn return result; } +void ludo::LudoGameMode::autoReleasePiece(int pieceIndex) { + auto result = movePieceInternal(pieceIndex, m_gameState.getDiceValue()); + m_pendingEvents.emplace_back(PieceMovedEvent{pieceIndex, result.fromPosition, result.toPosition, result.captured}); + if (result.captured) { + m_pendingEvents.emplace_back(PieceMovedEvent{result.capturedPieceIndex, result.toPosition, -1, false}); + } +} + void ludo::LudoGameMode::handle(const RollDiceCommand &command) { if (!isCurrentPlayer(command.player) || m_gameState.m_phase != TurnPhase::AwaitingRoll) return; m_gameState.m_diceValue = rollDice(); m_pendingEvents.emplace_back(DiceRolledEvent(command.player, m_gameState.m_diceValue)); + if (m_gameState.m_diceValue == 6) { + if (auto homeIndex = findLowestHomeSlotPiece(getColorOf(command.player))) { + if (!isBlockedByOwnPiece(getColorOf(command.player), PieceState::OnTrack, getEntryOffset(getColorOf(command.player)))) { + autoReleasePiece(*homeIndex); + finishTurn(); + return; + } + } + } + if (getMoveablePieceIndices(getColorOf(command.player), m_gameState.m_diceValue).empty()) { finishTurn(); // kein gültiger zug, sofort weitergeben } else { - m_gameState.m_phase = TurnPhase::AwaitingPieceSelection; + m_gameState.m_phase = TurnPhase::AwaitingPieceSelection; } } @@ -120,46 +151,38 @@ std::vector ludo::LudoGameMode::getMoveablePieceIndices(PlayerColor color, const Piece& piece = m_gameState.getPieces()[i]; if (piece.color != color) continue; - switch (piece.state) { - case PieceState::AtHome: { - if (diceValue == 6 && !isBlockedByOwnPiece(color, PieceState::OnTrack, getEntryOffset(color))) { - moveablePieceIndices.push_back(i); - } - break; - } - case PieceState::OnTrack: { - int distanceTraveled = (piece.position - getEntryOffset(color)) + kTrackLength; - int newDistance = distanceTraveled + diceValue; + Destination dest = computeDestination(piece, diceValue).value(); - if (newDistance <= kTrackLength - 1) { - int newPos = (piece.position + diceValue) % kTrackLength; - if (!isBlockedByOwnPiece(color, PieceState::OnTrack, newPos)) { - moveablePieceIndices.push_back(i); - } - } else if (newDistance <= kTrackLength - 1 + kHomeStretchLength){ - int homeIndex = newDistance - kTrackLength; - if (!isBlockedByOwnPiece(color, PieceState::InHomeStretch, homeIndex)) { - moveablePieceIndices.push_back(i); - } - } - // sonst: Wurf zu hoch, Figur kann sich nicht "totlaufen" -> nicht bewegbar - break; - } - case PieceState::InHomeStretch: { - int newIndex = piece.position + diceValue; - if (newIndex <= kHomeStretchLength - 1 && !isBlockedByOwnPiece(color, PieceState::Finished, newIndex)) { - moveablePieceIndices.push_back(i); - } - break; - } - case PieceState::Finished: - break; + // Überschuss in der Zielgeraden abfangen: computeDestination liefert bei Überschuss + // aktuell denselben Zustand zurück, wie unten in computeDestination definiert + if (piece.state == PieceState::InHomeStretch && dest.position > kHomeStretchLength - 1) continue; + if (piece.state == PieceState::AtHome && diceValue != 6) { + continue; + } + + if (!isBlockedByOwnPiece(color, dest.state, dest.position)) { + moveablePieceIndices.push_back(i); } } return moveablePieceIndices; } +std::optional ludo::LudoGameMode::wouldCapture(int pieceIndex, int diceValue) const { + const auto& piece = m_gameState.m_pieces[pieceIndex]; + auto dest = computeDestination(piece, diceValue); + if (dest.value().state != PieceState::OnTrack) return std::nullopt; + + for (int i = 0; i < static_cast(m_gameState.m_pieces.size()); ++i) { + if (i == pieceIndex) continue; + const auto& other = m_gameState.m_pieces[i]; + if (other.color != piece.color && other.state == PieceState::OnTrack && other.position == dest.value().position) { + return i; + } + } + return std::nullopt; +} + int ludo::LudoGameMode::rollDice() { std::uniform_int_distribution distribution(1, 6); diff --git a/game/src/ludo/LudoGameMode.h b/game/src/ludo/LudoGameMode.h index 7c4f598..09b7749 100644 --- a/game/src/ludo/LudoGameMode.h +++ b/game/src/ludo/LudoGameMode.h @@ -22,6 +22,9 @@ namespace ludo { [[nodiscard]] const LudoGameState& getState() const override { return m_gameState; } [[nodiscard]] PlayerColor getColorOf(engine::PlayerID playerID) const; + + std::vector getMoveablePieceIndices(PlayerColor color, int diceValue) const; + std::optional wouldCapture(int pieceIndex, int diceValue) const; private: struct MoveResult { int fromPosition; @@ -30,7 +33,16 @@ namespace ludo { int capturedPieceIndex = -1; }; + struct Destination { + PieceState state; + int position; + }; + + std::optional computeDestination(const Piece &piece, int diceValue) const; + [[nodiscard]] std::optional findLowestHomeSlotPiece(PlayerColor color) const; + MoveResult movePieceInternal(int pieceIndex, int diceValue); + void autoReleasePiece(int pieceIndex); void handle(const RollDiceCommand &command); @@ -39,7 +51,6 @@ namespace ludo { void handle(const MovePieceCommand &command); void finishTurn(); - std::vector getMoveablePieceIndices(PlayerColor color, int diceValue) const; LudoGameState m_gameState; std::vector m_pendingEvents; diff --git a/game/src/ludo/PiecePresenter.cpp b/game/src/ludo/PiecePresenter.cpp index 0d6e6b4..ccd847e 100644 --- a/game/src/ludo/PiecePresenter.cpp +++ b/game/src/ludo/PiecePresenter.cpp @@ -18,6 +18,7 @@ void ludo::PiecePresenter::spawnPieces(const ludo::LudoGameState &state) { m_registry.addComponent(entity, transform); m_registry.addComponent(entity, engine::MeshComponent(m_pieceModels[std::string(ludo::toString(piece.color))])); m_registry.addComponent(entity, engine::PickableComponent(kPieceRadius)); + m_pieceEntities.push_back(entity); } }