ColorRace/game/src/ludo/LudoGameMode.cpp
2026-08-05 18:42:01 +02:00

202 lines
8.4 KiB
C++

//
// Created by sebastian on 02.08.26.
//
#include "LudoGameMode.h"
#include <algorithm>
#include <cassert>
#include "gameMode/LudoRules.h"
#include "spdlog/spdlog.h"
void ludo::LudoGameMode::sendCommand(const std::variant<MovePieceCommand, RollDiceCommand> &command) {
std::visit([this](const auto& cmd) { handle(cmd);}, command);
}
std::vector<ludo::LudoEvent> ludo::LudoGameMode::pollEvents() {
auto events = std::move(m_pendingEvents);
m_pendingEvents.clear();
return events;
}
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
}
}
return std::nullopt;
}
ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) {
Piece& piece = m_gameState.m_pieces[pieceIndex];
int fromPosition = piece.position;
Destination destination = LudoRules::computeDestination(m_gameState, 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};
if (piece.state == PieceState::OnTrack) {
for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) {
if (i == pieceIndex) continue;
Piece& other = m_gameState.m_pieces[i];
if (other.color != piece.color && other.state == PieceState::OnTrack && other.position == piece.position) {
other.state = PieceState::AtHome;
other.position = -1;
result.captured = true;
result.capturedPieceIndex = i;
break; // dank Blockade-Regel kann max. eine gegnerische Figur auf einem Feld stehen
}
}
}
return result;
}
void ludo::LudoGameMode::autoReleasePiece(int pieceIndex) {
auto result = movePieceInternal(pieceIndex, m_gameState.getDiceValue());
publishEvent(PieceMovedEvent::create(pieceIndex, result.fromPosition, result.toPosition, result.captured, result.startState, result.targetState));
if (result.captured) {
//Todo: Determine real toPosition Index dependent on home belegung
std::vector<int> freeHomePositions = m_gameState.getFreeHomePositions(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex));
const int smallestFreeHomePosition = *std::ranges::min_element(freeHomePositions);
publishEvent(PieceMovedEvent::create(result.capturedPieceIndex, result.fromPosition, smallestFreeHomePosition, result.captured, result.startState, result.targetState));
}
}
void ludo::LudoGameMode::handle(const RollDiceCommand &command) {
if (!isCurrentPlayer(command.player) || m_gameState.m_phase != TurnPhase::AwaitingRoll) return;
m_gameState.m_diceValue = rollDice();
publishEvent(DiceRolledEvent(command.player, m_gameState.m_diceValue));
if (m_gameState.m_diceValue == 6) {
if (auto homeIndex = findLowestHomeSlotPiece(getColorOf(command.player))) {
if (!LudoRules::isBlockedByOwnPiece(m_gameState, 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;
}
}
ludo::PlayerColor ludo::LudoGameMode::getColorOf(engine::PlayerID player) const {
const auto& order = getPlayerOrder();
auto it = std::find(order.begin(), order.end(), player);
assert(it != order.end() && "getColorOf mit unregistrierter PlayerID aufgerufen");
return static_cast<PlayerColor>(std::distance(order.begin(), it));
}
void ludo::LudoGameMode::handle(const MovePieceCommand &command) {
if (!isCurrentPlayer(command.player)) {
spdlog::warn("MovePieceCommand abgewiesen: {} ist nicht am Zug (aktuell: {})", command.player, getCurrentPlayer());
return;
}
if (m_gameState.m_phase != TurnPhase::AwaitingPieceSelection) {
spdlog::warn("MovePieceCommand abgewiesen: falsche Phase ({})", static_cast<int>(m_gameState.m_phase));
return;
}
auto movable = getMoveablePieceIndices(getColorOf(command.player), m_gameState.m_diceValue);
if (std::find(movable.begin(), movable.end(), command.pieceIndex) == movable.end()) {
spdlog::warn("MovePieceCommand abgewiesen: pieceIndex={} nicht in movable-Liste", command.pieceIndex);
return;
}
// ungültiger Zug, abweisen
spdlog::info("MovePieceCommand akzeptiert: pieceIndex={}", command.pieceIndex);
auto result = movePieceInternal(command.pieceIndex, m_gameState.m_diceValue);
publishEvent(PieceMovedEvent::create(command.pieceIndex, result.fromPosition, result.toPosition, result.captured, result.startState, result.targetState));
if (result.captured) {
spdlog::info("MovePieceCommand: gegnerische Figur auf {} geworfen", result.toPosition);
auto freeHomePositions = m_gameState.getFreeHomePositions(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex));
if (!freeHomePositions.empty()) {
Piece capturedPiece = m_gameState.m_pieces[result.capturedPieceIndex];
PieceMovedEvent event = PieceMovedEvent::create(result.capturedPieceIndex, capturedPiece.position, freeHomePositions.front(), true, capturedPiece.state, PieceState::AtHome);
publishEvent(event);
} else {
throw std::runtime_error(
std::format("No free home positions for captured piece {}",std::string(ludo::toString(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex))))
);
}
}
finishTurn();
}
void ludo::LudoGameMode::finishTurn() {
bool extraTurn = (m_gameState.m_diceValue == 6 && (++m_gameState.m_consecutiveSixes < 2));
if (m_gameState.m_diceValue != 6) {
m_gameState.m_consecutiveSixes = 0;
}
endTurn(extraTurn);
m_gameState.m_phase = TurnPhase::AwaitingRoll;
publishEvent(TurnChangedEvent{getCurrentPlayer()});
}
std::vector<int> ludo::LudoGameMode::getMoveablePieceIndices(PlayerColor color, int diceValue) const {
std::vector<int> moveablePieceIndices;
for (int i = 0; i < m_gameState.getPieces().size(); ++i) {
const Piece& piece = m_gameState.getPieces()[i];
if (piece.color != color) continue;
Destination dest = LudoRules::computeDestination(m_gameState, piece, diceValue).value();
// Ü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 (!LudoRules::isBlockedByOwnPiece(m_gameState, color, dest.state, dest.position)) {
moveablePieceIndices.push_back(i);
}
}
return moveablePieceIndices;
}
std::optional<int> ludo::LudoGameMode::wouldCapture(int pieceIndex, int diceValue) const {
const auto& piece = m_gameState.m_pieces[pieceIndex];
auto dest = LudoRules::computeDestination(m_gameState, 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() {
return m_diceSource->roll();
}
int ludo::LudoGameMode::getEntryOffset(PlayerColor color) {
return static_cast<int>(color) * 10;
}
void ludo::LudoGameMode::publishEvent(LudoEvent event) {
m_pendingEvents.push_back(event);
m_eventBus.publish(event);
}