ADD: Basic AI

This commit is contained in:
sebastian 2026-08-02 21:52:16 +02:00
parent c6d1fd8910
commit bb29a1d3f1
10 changed files with 195 additions and 58 deletions

View File

@ -212,6 +212,8 @@ add_executable(ColorRace
game/src/ludo/BoardLayout.h game/src/ludo/BoardLayout.h
game/src/ludo/PiecePresenter.cpp game/src/ludo/PiecePresenter.cpp
game/src/ludo/PiecePresenter.h game/src/ludo/PiecePresenter.h
game/src/ludo/LudoAiController.cpp
game/src/ludo/LudoAiController.h
) )

View File

@ -18,11 +18,12 @@ namespace engine {
explicit TurnBasedGameMode(std::vector<PlayerID> playerOrder) : m_playerOrder(std::move(playerOrder)) {} explicit TurnBasedGameMode(std::vector<PlayerID> playerOrder) : m_playerOrder(std::move(playerOrder)) {}
[[nodiscard]] PlayerID getCurrentPlayer() const { return m_playerOrder[m_currentPlayerIndex]; } [[nodiscard]] PlayerID getCurrentPlayer() const { return m_playerOrder[m_currentPlayerIndex]; }
[[nodiscard]] bool isCurrentPlayer(PlayerID playerID) const { return getCurrentPlayer() == playerID; } [[nodiscard]] bool isCurrentPlayer(PlayerID playerID) const { return getCurrentPlayer() == playerID; }
[[nodiscard]] std::vector<PlayerID> getPlayerOrder() const { return m_playerOrder; }
protected: protected:
void endTurn(bool extraTurn = false) { void endTurn(bool extraTurn = false) {
if (!extraTurn) m_currentPlayerIndex = (m_currentPlayerIndex + 1) % m_playerOrder.size(); if (!extraTurn) m_currentPlayerIndex = (m_currentPlayerIndex + 1) % m_playerOrder.size();
} }
std::vector<PlayerID> getPlayerOrder() const { return m_playerOrder; }
private: private:
std::vector<PlayerID> m_playerOrder; std::vector<PlayerID> m_playerOrder;
size_t m_currentPlayerIndex = 0; size_t m_currentPlayerIndex = 0;

View File

@ -13,7 +13,7 @@ void engine::MeshRenderer::render(RenderQueue &queue, const Camera &camera, cons
m_shader->loadCameraPosition(camera.getPosition()); m_shader->loadCameraPosition(camera.getPosition());
m_shader->loadLight(light.position, light.color); 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); m_shader->loadTransformationMatrix(transformationMatrix);
mesh->bind(); mesh->bind();

View File

@ -56,6 +56,9 @@ void GameLayer::renderHud() {
void GameLayer::onUpdate(float deltaTime) { void GameLayer::onUpdate(float deltaTime) {
Layer::onUpdate(deltaTime); Layer::onUpdate(deltaTime);
m_cameraController->update(getKeyboard(), *m_camera, deltaTime); m_cameraController->update(getKeyboard(), *m_camera, deltaTime);
for (auto& ai : aiControllers) {
ai.update(deltaTime);
}
for (const auto& event : m_gameMode->pollEvents()) { for (const auto& event : m_gameMode->pollEvents()) {
std::visit([this]<typename T>(const T& e) { std::visit([this]<typename T>(const T& e) {

View File

@ -11,6 +11,7 @@
#include "ecs/systems/PickingSystem.h" #include "ecs/systems/PickingSystem.h"
#include "layer/Layer.h" #include "layer/Layer.h"
#include "ludo/BoardLayout.h" #include "ludo/BoardLayout.h"
#include "ludo/LudoAiController.h"
#include "ludo/PiecePresenter.h" #include "ludo/PiecePresenter.h"
#include "renderer/MeshRenderer.h" #include "renderer/MeshRenderer.h"
#include "renderer/RenderQueue.h" #include "renderer/RenderQueue.h"
@ -23,7 +24,13 @@ public:
engine::Keyboard& keyboard, engine::Mouse& mouse, std::shared_ptr<engine::Model> boardModel, std::unordered_map<std::string, std::shared_ptr<engine::Model>> pieceModdels) engine::Keyboard& keyboard, engine::Mouse& mouse, std::shared_ptr<engine::Model> boardModel, std::unordered_map<std::string, std::shared_ptr<engine::Model>> pieceModdels)
: Layer(keyboard, mouse), m_gameMode(std::move(gameMode)), m_entityRegistry(entityRegistry), m_camera(std::move(cam)), m_renderer(std::make_unique<engine::MeshRenderer>()), : Layer(keyboard, mouse), m_gameMode(std::move(gameMode)), m_entityRegistry(entityRegistry), m_camera(std::move(cam)), m_renderer(std::make_unique<engine::MeshRenderer>()),
m_pointLight(std::move(light)), m_boardLayout(std::move(boardModel)), m_piecePresenter(m_entityRegistry, std::move(pieceModdels), m_boardLayout), 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 onUpdate(float deltaTime) override;
void onRender() override; void onRender() override;
@ -46,6 +53,8 @@ private:
engine::PickingSystem m_pickingSystem; engine::PickingSystem m_pickingSystem;
std::string m_lastEventMessage; std::string m_lastEventMessage;
std::vector<ludo::LudoAiController> aiControllers;
void renderHud(); void renderHud();
}; };

View File

@ -0,0 +1,59 @@
//
// Created by sebastian on 02.08.26.
//
#include "LudoAiController.h"
#include <iostream>
#include <ostream>
#include <utility>
ludo::LudoAiController::LudoAiController(std::shared_ptr<LudoGameMode> 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<int> 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();
}

View File

@ -0,0 +1,28 @@
//
// Created by sebastian on 02.08.26.
//
#ifndef COLORRACE_LUDOAICONTROLLER_H
#define COLORRACE_LUDOAICONTROLLER_H
#include <memory>
#include "LudoGameMode.h"
namespace ludo {
class LudoAiController {
public:
LudoAiController(std::shared_ptr<LudoGameMode> gameMode, engine::PlayerID player);
void update(float deltaTime);
private:
static constexpr float kThinkDelay = 0.8f;
[[nodiscard]] std::optional<int> chooseMove() const;
std::shared_ptr<LudoGameMode> m_gameMode;
engine::PlayerID m_player;
float m_thinkTimer = 0.0f;
};
}
#endif //COLORRACE_LUDOAICONTROLLER_H

View File

@ -17,37 +17,50 @@ std::vector<ludo::LudoEvent> ludo::LudoGameMode::pollEvents() {
return events; return events;
} }
ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) { [[nodiscard]] std::optional<ludo::LudoGameMode::Destination> ludo::LudoGameMode::computeDestination(const Piece &piece, int diceValue) const {
Piece& piece = m_gameState.m_pieces[pieceIndex];
int fromPosition = piece.position;
switch (piece.state) { switch (piece.state) {
case PieceState::AtHome: { case PieceState::AtHome:
piece.state = PieceState::OnTrack; return Destination{PieceState::OnTrack, getEntryOffset(piece.color)};
piece.position = getEntryOffset(piece.color);
break;
}
case PieceState::OnTrack: { case PieceState::OnTrack: {
int distanceTraveled = (piece.position - getEntryOffset(piece.color) + kTrackLength) % kTrackLength; int distanceTraveled = (piece.position - getEntryOffset(piece.color) + kTrackLength) % kTrackLength;
int newDistance = distanceTraveled + diceValue; int newDistance = distanceTraveled + diceValue;
if (newDistance <= kTrackLength - 1) { if (newDistance <= kTrackLength - 1) {
piece.position = (piece.position + diceValue) % kTrackLength; return Destination{PieceState::OnTrack, (piece.position + diceValue) % kTrackLength};
} else {
int homeIndex = newDistance - kTrackLength;
piece.state = PieceState::InHomeStretch;
piece.position = homeIndex;
} }
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: { case PieceState::InHomeStretch: {
piece.position += diceValue; int newIndex = piece.position + diceValue;
if (piece.position == kHomeStretchLength - 1) { if (newIndex > kHomeStretchLength - 1) return std::nullopt; // überzählig
piece.state = PieceState::Finished; return Destination{newIndex == kHomeStretchLength - 1 ? PieceState::Finished : PieceState::InHomeStretch, newIndex};
}
} }
case PieceState::Finished: case PieceState::Finished:
break; return std::nullopt; // nie bewegbar
} }
return std::nullopt;
}
std::optional<int> ludo::LudoGameMode::findLowestHomeSlotPiece(PlayerColor color) const {
for (int i = 0; i < static_cast<int>(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}; MoveResult result{fromPosition, piece.position, false, -1};
@ -68,16 +81,34 @@ ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIn
return result; 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) { void ludo::LudoGameMode::handle(const RollDiceCommand &command) {
if (!isCurrentPlayer(command.player) || m_gameState.m_phase != TurnPhase::AwaitingRoll) return; if (!isCurrentPlayer(command.player) || m_gameState.m_phase != TurnPhase::AwaitingRoll) return;
m_gameState.m_diceValue = rollDice(); m_gameState.m_diceValue = rollDice();
m_pendingEvents.emplace_back(DiceRolledEvent(command.player, m_gameState.m_diceValue)); 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()) { if (getMoveablePieceIndices(getColorOf(command.player), m_gameState.m_diceValue).empty()) {
finishTurn(); // kein gültiger zug, sofort weitergeben finishTurn(); // kein gültiger zug, sofort weitergeben
} else { } else {
m_gameState.m_phase = TurnPhase::AwaitingPieceSelection; m_gameState.m_phase = TurnPhase::AwaitingPieceSelection;
} }
} }
@ -120,46 +151,38 @@ std::vector<int> ludo::LudoGameMode::getMoveablePieceIndices(PlayerColor color,
const Piece& piece = m_gameState.getPieces()[i]; const Piece& piece = m_gameState.getPieces()[i];
if (piece.color != color) continue; if (piece.color != color) continue;
switch (piece.state) { Destination dest = computeDestination(piece, diceValue).value();
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;
if (newDistance <= kTrackLength - 1) { // Überschuss in der Zielgeraden abfangen: computeDestination liefert bei Überschuss
int newPos = (piece.position + diceValue) % kTrackLength; // aktuell denselben Zustand zurück, wie unten in computeDestination definiert
if (!isBlockedByOwnPiece(color, PieceState::OnTrack, newPos)) { if (piece.state == PieceState::InHomeStretch && dest.position > kHomeStretchLength - 1) continue;
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;
if (piece.state == PieceState::AtHome && diceValue != 6) {
continue;
}
if (!isBlockedByOwnPiece(color, dest.state, dest.position)) {
moveablePieceIndices.push_back(i);
} }
} }
return moveablePieceIndices; return moveablePieceIndices;
} }
std::optional<int> 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<int>(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() { int ludo::LudoGameMode::rollDice() {
std::uniform_int_distribution<int> distribution(1, 6); std::uniform_int_distribution<int> distribution(1, 6);

View File

@ -22,6 +22,9 @@ namespace ludo {
[[nodiscard]] const LudoGameState& getState() const override { return m_gameState; } [[nodiscard]] const LudoGameState& getState() const override { return m_gameState; }
[[nodiscard]] PlayerColor getColorOf(engine::PlayerID playerID) const; [[nodiscard]] PlayerColor getColorOf(engine::PlayerID playerID) const;
std::vector<int> getMoveablePieceIndices(PlayerColor color, int diceValue) const;
std::optional<int> wouldCapture(int pieceIndex, int diceValue) const;
private: private:
struct MoveResult { struct MoveResult {
int fromPosition; int fromPosition;
@ -30,7 +33,16 @@ namespace ludo {
int capturedPieceIndex = -1; int capturedPieceIndex = -1;
}; };
struct Destination {
PieceState state;
int position;
};
std::optional<Destination> computeDestination(const Piece &piece, int diceValue) const;
[[nodiscard]] std::optional<int> findLowestHomeSlotPiece(PlayerColor color) const;
MoveResult movePieceInternal(int pieceIndex, int diceValue); MoveResult movePieceInternal(int pieceIndex, int diceValue);
void autoReleasePiece(int pieceIndex);
void handle(const RollDiceCommand &command); void handle(const RollDiceCommand &command);
@ -39,7 +51,6 @@ namespace ludo {
void handle(const MovePieceCommand &command); void handle(const MovePieceCommand &command);
void finishTurn(); void finishTurn();
std::vector<int> getMoveablePieceIndices(PlayerColor color, int diceValue) const;
LudoGameState m_gameState; LudoGameState m_gameState;
std::vector<LudoEvent> m_pendingEvents; std::vector<LudoEvent> m_pendingEvents;

View File

@ -18,6 +18,7 @@ void ludo::PiecePresenter::spawnPieces(const ludo::LudoGameState &state) {
m_registry.addComponent<engine::TransformComponent>(entity, transform); m_registry.addComponent<engine::TransformComponent>(entity, transform);
m_registry.addComponent<engine::MeshComponent>(entity, engine::MeshComponent(m_pieceModels[std::string(ludo::toString(piece.color))])); m_registry.addComponent<engine::MeshComponent>(entity, engine::MeshComponent(m_pieceModels[std::string(ludo::toString(piece.color))]));
m_registry.addComponent<engine::PickableComponent>(entity, engine::PickableComponent(kPieceRadius)); m_registry.addComponent<engine::PickableComponent>(entity, engine::PickableComponent(kPieceRadius));
m_pieceEntities.push_back(entity);
} }
} }