//
// Shardok-specific game engine adapter implementation
//

#include "ShardokGameEngine.hpp"

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>

#include "ShardokAction.hpp"
#include "ShardokGameState.hpp"
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSTypes.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AICommandFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIHeuristicWeighting.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/score/AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"

namespace shardok::mcts {

namespace {

auto IsDefenderPlayer(const GameStateW& gameState, const PlayerId playerId) -> bool {
    for (const auto* pi : *gameState->player_infos()) {
        if (pi->player_id() == playerId) { return pi->is_defender(); }
    }
    return false;
}

auto MixCacheKey(uint64_t seed, const uint64_t value) -> uint64_t {
    constexpr uint64_t kHashCombineConstant = 0x9e3779b97f4a7c15ULL;
    seed ^= value + kHashCombineConstant + (seed << 6) + (seed >> 2);
    return seed;
}

auto PointerIdentityHash(const void* ptr) -> uint64_t {
    return static_cast<uint64_t>(reinterpret_cast<std::uintptr_t>(ptr));
}

auto LegalActionsCacheKey(
        const uint64_t stateHash,
        const bool rootIsDefender,
        const GameSettings* gameSettings,
        const APDCache* apdCache) -> uint64_t {
    constexpr uint64_t kDefenderRoleHash = 0x9e3779b97f4a7c15ULL;
    uint64_t key = rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;

    // The cached action set is the output of AICommandFilter, not just the raw command list.
    // Include the context objects that can affect filtering so a process-wide cache entry from
    // one search cannot be reused by a different settings/APD context with the same state hash.
    key = MixCacheKey(key, PointerIdentityHash(gameSettings));
    key = MixCacheKey(key, PointerIdentityHash(apdCache != nullptr ? apdCache->get() : nullptr));
    return key;
}

auto SortActionsByWeight(
        std::vector<std::unique_ptr<MCTSAction>> actions,
        const std::vector<double>& weights) -> std::vector<std::unique_ptr<MCTSAction>> {
    std::vector<size_t> sortedIndices(actions.size());
    std::iota(sortedIndices.begin(), sortedIndices.end(), 0);

    std::ranges::sort(sortedIndices, [&weights](size_t a, size_t b) {
        return weights[a] > weights[b];
    });

    std::vector<std::unique_ptr<MCTSAction>> sortedActions;
    sortedActions.reserve(actions.size());
    for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }

    return sortedActions;
}

}  // namespace

// Shared cache for legal actions (uses lock-free parallel hash map for thread safety)
// Using 8 submaps to reduce contention with 16 MCTS threads
gtl::parallel_flat_hash_map<
        uint64_t,
        ShardokGameEngine::LegalActionsCache,
        std::hash<uint64_t>,
        std::equal_to<uint64_t>,
        std::allocator<std::pair<const uint64_t, ShardokGameEngine::LegalActionsCache>>,
        8,
        std::mutex>
        ShardokGameEngine::legalActionsCache_;
std::atomic<uint64_t> ShardokGameEngine::cacheHits_{0};
std::atomic<uint64_t> ShardokGameEngine::cacheMisses_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInHashComputation_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInLegalActionsComputation_{0};

namespace {
std::once_flag legalActionsCacheReserveOnce;
}  // namespace

ShardokGameEngine::ShardokGameEngine(
        [[maybe_unused]] const ShardokEngine* engine,
        const AIScoreCalculator* scoreCalculator,
        const GameSettingsSPtr& gameSettings,
        const APDCache* apdCache,
        const ALCache* alCache,
        bool isDefender,
        const AIStrategy& strategy,
        const CoordsSet& castleCoords,
        const CoordsSet& criticalTileCoords)
    : scoreCalculator_(scoreCalculator),
      gameSettings_(gameSettings),
      apdCache_(apdCache),
      alCache_(alCache),
      isDefender_(isDefender),
      strategy_(strategy),
      castleCoords_(castleCoords),
      criticalTileCoords_(criticalTileCoords) {
    // Thread-local cache is automatically initialized per thread
    // Reserve space to reduce rehashing (based on profiling: ~30-50K unique states per search)
    std::call_once(legalActionsCacheReserveOnce, [] {
        ShardokGameEngine::legalActionsCache_.reserve(100000);
    });
}

std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
        const MCTSGameState& state,
        const MCTSAction& action,
        double deterministicRoll) const {
    const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
    const auto* shardokAction = dynamic_cast<const ShardokAction*>(&action);

    if (!shardokState || !shardokAction) { return nullptr; }

    const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());

    auto engine = std::make_shared<ShardokEngine>(
            gameSettings_,
            shardokState->getShardokState(),
            criticalTileCoords_,
            0,
            false);
    // Populate the AI command list before posting so the MCTS action index maps to the
    // no-follow-up command ordering used by getLegalActions().
    [[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);

    // Create deterministic random generator if a specific roll is requested
    // deterministicRoll of -1.0 (default) means use random generator
    // Any other value (including negative) creates a deterministic generator
    // For open-ended percentile commands, we compute a sequence of values that will
    // produce the desired final result through the normal open-ended mechanics
    std::shared_ptr<::RandomGenerator> randomGen = nullptr;
    constexpr double kNoRollSentinel = -1.0;
    if (deterministicRoll != kNoRollSentinel) {
        std::vector<double> sequence;

        if (deterministicRoll >= 5.0 && deterministicRoll <= 95.0) {
            // Normal range: single value works directly
            sequence = {deterministicRoll / 100.0};
        } else if (deterministicRoll < 5.0) {
            // Need open-ended LOW result (e.g., -100 for guaranteed success)
            // OpenEndedPercentile: if initial < 5, returns initial - OpenEndedHighImpl(0, 4)
            // We want: initial - accumulated = deterministicRoll
            // Use initial = 2 (clearly < 5), so accumulated = 2 - deterministicRoll
            constexpr double kInitialLow = 2.0;
            sequence = {kInitialLow / 100.0};
            // OpenEndedHighImpl accumulates rolls until one < 95
            // Split accumulated into rolls: 96 (continues) + remaining (stops)
            double remaining = kInitialLow - deterministicRoll;
            while (remaining > 95.0) {
                sequence.push_back(0.96);  // 96 > 95, continues accumulation
                remaining -= 96.0;
            }
            sequence.push_back(remaining / 100.0);  // Final roll < 95, stops
        } else {
            // Need open-ended HIGH result (e.g., 150 for guaranteed failure)
            // OpenEndedPercentile: if initial > 95, returns OpenEndedHighImpl(initial, 4)
            // OpenEndedHighImpl accumulates rolls until one < 95
            constexpr double kInitialHigh = 96.0;
            sequence = {kInitialHigh / 100.0};
            double remaining = deterministicRoll - kInitialHigh;
            while (remaining > 95.0) {
                sequence.push_back(0.96);
                remaining -= 96.0;
            }
            sequence.push_back(remaining / 100.0);
        }

        randomGen = std::make_shared<::SequenceRandomGenerator>(sequence);
    }

    engine->PostCommand(currentPlayer, shardokAction->getIndex(), randomGen);

    // Create and return the new state
    auto newState = std::make_unique<ShardokGameState>(
            engine->GetCurrentGameState(),
            scoreCalculator_,
            gameSettings_.get(),
            isDefender_,
            strategy_,
            castleCoords_,
            *apdCache_,
            *alCache_,
            criticalTileCoords_);

    // Don't pre-compute hash - let it be computed lazily on first use
    // Many states (especially in simulation) never need their hash computed
    return newState;
}

void ShardokGameEngine::applyActionMutable(
        std::unique_ptr<MCTSGameState>& state,
        const MCTSAction& action) const {
    auto* shardokState = dynamic_cast<ShardokGameState*>(state.get());
    const auto* shardokAction = dynamic_cast<const ShardokAction*>(&action);

    if (!shardokState || !shardokAction) {
        // Fallback to default implementation
        state = applyAction(*state, action);
        return;
    }

    const auto currentPlayer = static_cast<PlayerId>(state->currentPlayerId());

    auto engine = std::make_shared<ShardokEngine>(
            gameSettings_,
            shardokState->getShardokState(),
            criticalTileCoords_,
            0,
            false);
    // Populate the AI command list before posting so the MCTS action index maps to the
    // no-follow-up command ordering used by getLegalActions().
    [[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);

    engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
    shardokState->getMutableShardokState() = engine->GetCurrentGameState();
    shardokState->invalidateHashCache();
}

std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
        const MCTSGameState& state,
        MCTSPlayerId /*rootPlayerId*/,
        int currentPlayerFlips,
        int maxPlayerFlips) const {
    const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
    if (!shardokState) { return {}; }

    const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());

    // Check if we've exceeded the maximum allowed player flips
    // currentPlayerFlips is the number of times the player has changed since root
    // maxPlayerFlips is the maximum number of changes we allow
    // If maxPlayerFlips is 0, only explore root player's moves (stop when player first changes)
    // If maxPlayerFlips is 1, explore through opponent's response (stop after opponent's moves)
    if (currentPlayerFlips > maxPlayerFlips) {
        return {};  // Stop exploration - we've exceeded the flip limit
    }

    // Time hash computation
    const auto hashStart = std::chrono::high_resolution_clock::now();
    const uint64_t stateHash = shardokState->hash();
    const uint64_t cacheKey =
            LegalActionsCacheKey(stateHash, isDefender_, gameSettings_.get(), apdCache_);
    const auto hashEnd = std::chrono::high_resolution_clock::now();
    timeInHashComputation_.fetch_add(
            std::chrono::duration_cast<std::chrono::microseconds>(hashEnd - hashStart).count(),
            std::memory_order_relaxed);

    // Check transposition table for cached legal actions
    if (auto it = legalActionsCache_.find(cacheKey); it != legalActionsCache_.end()) {
        const auto engine = std::make_shared<ShardokEngine>(
                gameSettings_,
                shardokState->getShardokState(),
                criticalTileCoords_);

        const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);

        bool cacheEntryMatches = commands != nullptr && commands->size() == it->second.commandCount;
        for (const auto& cachedAction : it->second.actions) {
            const size_t commandIndex = cachedAction.getIndex();
            if (!cacheEntryMatches || commandIndex >= commands->size()) {
                cacheEntryMatches = false;
                break;
            }

            const auto& command = commands->at(commandIndex);
            const auto [targetRow, targetColumn] = cachedAction.getTarget();
            cacheEntryMatches = command->GetCommandType() ==
                                        static_cast<net::eagle0::shardok::common::CommandType>(
                                                cachedAction.getType()) &&
                                command->GetPlayerId() == cachedAction.getPlayer() &&
                                command->GetActorUnitId() == cachedAction.getActorId() &&
                                command->GetTargetRow() == targetRow &&
                                command->GetTargetColumn() == targetColumn &&
                                command->HasOdds() == cachedAction.hasOdds();
            if (!cacheEntryMatches) { break; }
        }

        if (cacheEntryMatches) {
            cacheHits_.fetch_add(1, std::memory_order_relaxed);

            // Convert immutable cached action snapshots to MCTS action instances.
            std::vector<std::unique_ptr<MCTSAction>> actions;
            actions.reserve(it->second.actions.size());

            for (const auto& cachedAction : it->second.actions) {
                actions.push_back(cachedAction.clone());
            }

            // Sort actions by weight (descending) to ensure MCTS explores high-value actions first
            const std::vector<double> weights = getActionWeights(actions, state);
            return SortActionsByWeight(std::move(actions), weights);
        }
    }

    cacheMisses_.fetch_add(1, std::memory_order_relaxed);

    // Time legal actions computation
    const auto actionsStart = std::chrono::high_resolution_clock::now();

    const auto engine = std::make_shared<ShardokEngine>(
            gameSettings_,
            shardokState->getShardokState(),
            criticalTileCoords_);

    const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);

    if (!commands || commands->empty()) { return {}; }

    // Filter commands using AICommandFilter (matching original MCTSAI behavior)
    // Use gameSettings for battalion type lookups
    const std::vector<size_t> filteredIndices = AICommandFilter::FilterCommands(
            commands,
            currentPlayer,
            IsDefenderPlayer(shardokState->getShardokState(), currentPlayer),
            shardokState->getShardokState(),
            *apdCache_,
            [this](BattalionTypeId typeId) {
                return gameSettings_->GetGetter().GetBattalionType(typeId);
            });

    // Convert only filtered commands to MCTSActions
    std::vector<std::unique_ptr<MCTSAction>> actions;
    actions.reserve(filteredIndices.size());
    std::vector<ShardokAction> cachedActions;
    cachedActions.reserve(filteredIndices.size());

    for (const size_t idx : filteredIndices) {
        if (idx < commands->size()) {
            const auto& cmd = commands->at(idx);

            // Extract essential fields directly from command (no proto conversion!)
            ShardokAction action(
                    idx,
                    cmd->GetCommandType(),
                    cmd->GetPlayerId(),
                    cmd->GetActorUnitId(),
                    cmd->GetTargetRow(),
                    cmd->GetTargetColumn(),
                    cmd->HasOdds());
            actions.push_back(action.clone());
            cachedActions.push_back(action);
        }
    }

    // Sort actions by weight (descending) to ensure MCTS explores high-value actions first
    // This is critical when maxPlayerFlips is low (e.g., 1), as only the first few actions
    // get explored deeply. Original indices are preserved in ShardokAction::getIndex()
    const std::vector<double> weights = getActionWeights(actions, state);

    actions = SortActionsByWeight(std::move(actions), weights);

    const auto actionsEnd = std::chrono::high_resolution_clock::now();
    timeInLegalActionsComputation_.fetch_add(
            std::chrono::duration_cast<std::chrono::microseconds>(actionsEnd - actionsStart)
                    .count(),
            std::memory_order_relaxed);

    // Store immutable action snapshots in the transposition table for future lookups.
    // Do not store ShardokEngine here: its command cache is mutable and not safe to share
    // across MCTS worker threads.
    // Use lazy_emplace_l to ensure thread-safe insertion (locks the bucket during construction)
    legalActionsCache_.lazy_emplace_l(
            cacheKey,
            [&](typename decltype(legalActionsCache_)::value_type& v) {
                // Update existing entry
                v.second.commandCount = commands->size();
                v.second.actions = cachedActions;
            },
            [&](const typename decltype(legalActionsCache_)::constructor& ctor) {
                // Create new entry
                ctor(cacheKey, LegalActionsCache{commands->size(), cachedActions});
            });

    return actions;
}

bool ShardokGameEngine::isTerminal(const MCTSGameState& state) const { return state.isTerminal(); }

double ShardokGameEngine::evaluateState(const MCTSGameState& state, MCTSPlayerId playerId) const {
    return state.score(playerId);
}

std::vector<size_t> ShardokGameEngine::filterActions(
        const std::vector<std::unique_ptr<MCTSAction>>& actions,
        const MCTSGameState& /*state*/) const {
    // All filtering is already done in getLegalActions() using AICommandFilter
    // This method is used by simulation policies and doesn't need additional filtering
    std::vector<size_t> indices(actions.size());
    std::iota(indices.begin(), indices.end(), 0);
    return indices;
}

std::vector<double> ShardokGameEngine::getActionWeights(
        const std::vector<std::unique_ptr<MCTSAction>>& actions,
        const MCTSGameState& state) const {
    // Cast to ShardokGameState to access Shardok-specific methods
    const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
    if (!shardokState) {
        throw MCTSInternalError(
                "ShardokGameEngine::getActionWeights called with non-Shardok state - this "
                "indicates a type mismatch in the MCTS adapter layer");
    }

    const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());

    // Determine if current player is defender (not root player!)
    // During simulation we need to use the correct perspective for action weighting
    const auto& gameState = shardokState->getShardokState();
    const bool currentPlayerIsDefender = IsDefenderPlayer(gameState, currentPlayer);

    // Use AIHeuristicWeighting for fast O(1) context-aware command weighting
    std::vector<double> weights;
    weights.reserve(actions.size());

    for (const auto& action : actions) {
        const auto* shardokAction = dynamic_cast<const ShardokAction*>(action.get());
        if (!shardokAction) {
            throw MCTSInternalError(
                    "ShardokGameEngine::getActionWeights encountered non-Shardok action - this "
                    "indicates a type mismatch in the MCTS adapter layer");
        }

        const auto [targetRow, targetColumn] = shardokAction->getTarget();

        weights.push_back(AIHeuristicWeighting::GetCommandWeight(
                static_cast<net::eagle0::shardok::common::CommandType>(shardokAction->getType()),
                shardokAction->getActorId(),
                shardokAction->getPlayer(),
                Coords{static_cast<int8_t>(targetRow), static_cast<int8_t>(targetColumn)},
                gameState,
                castleCoords_,
                apdCache_,
                currentPlayerIsDefender,  // Use current player's role, not root player's!
                [this](BattalionTypeId typeId) {
                    return gameSettings_->GetGetter().GetBattalionType(typeId);
                }));
    }

    return weights;
}

double ShardokGameEngine::getActionScore(
        const MCTSGameState& state,
        const MCTSAction& action,
        MCTSPlayerId playerId) const {
    auto newState = applyAction(state, action);
    if (!newState) { return 0.0; }

    return newState->score(playerId);
}

bool ShardokGameEngine::shouldStopSearch(
        const MCTSGameState& /*state*/,
        int /*iterations*/,
        std::chrono::steady_clock::time_point /*startTime*/) const {
    // Could add early termination logic here
    return false;
}

size_t ShardokGameEngine::mapFilteredIndexToOriginal(
        size_t filteredIndex,
        const MCTSGameState& state) const {
    // Get the filtered actions.
    auto actions = getLegalActions(state, state.currentPlayerId(), 0, 0);

    // Check bounds
    if (filteredIndex >= actions.size()) { return filteredIndex; }

    // Extract the original index from the ShardokAction
    const auto* shardokAction = dynamic_cast<const ShardokAction*>(actions[filteredIndex].get());
    if (!shardokAction) { return filteredIndex; }

    // ShardokAction stores the original unfiltered index
    return shardokAction->getIndex();
}

void ShardokGameEngine::reportCacheStatistics() const { reportCacheStatistics(std::cout); }

void ShardokGameEngine::reportCacheStatistics(std::ostream& output) const {
    const uint64_t hits = cacheHits_.load(std::memory_order_relaxed);
    const uint64_t misses = cacheMisses_.load(std::memory_order_relaxed);
    const uint64_t hashTime = timeInHashComputation_.load(std::memory_order_relaxed);
    const uint64_t actionsTime = timeInLegalActionsComputation_.load(std::memory_order_relaxed);
    const uint64_t totalLookups = hits + misses;

    if (totalLookups > 0) {
        const double hitRate = static_cast<double>(hits) / static_cast<double>(totalLookups);
        const double avgHashTimeUs =
                static_cast<double>(hashTime) / static_cast<double>(totalLookups);
        const double avgActionsTimeUs =
                misses > 0 ? static_cast<double>(actionsTime) / static_cast<double>(misses) : 0.0;

        // Calculate if transposition table is worth it
        const double timeWithCache = hashTime + actionsTime;
        const double timeWithoutCache =
                avgActionsTimeUs * static_cast<double>(totalLookups);  // All lookups recompute
        const double savings = (timeWithoutCache - timeWithCache) / timeWithoutCache * 100.0;

        const auto flags = output.flags();
        const auto precision = output.precision();

        output << "Legal Actions Cache Stats:\n"
               << "  Lookups: " << hits << " hits, " << misses << " misses, " << std::fixed
               << std::setprecision(1) << hitRate * 100.0 << "% hit rate, "
               << legalActionsCache_.size() << " entries\n"
               << "  Timing: " << std::setprecision(2) << avgHashTimeUs << " us avg hash, "
               << avgActionsTimeUs << " us avg actions (on miss)\n"
               << "  Total time: " << hashTime / 1000.0 << " ms in hash, " << actionsTime / 1000.0
               << " ms in actions\n"
               << "  Cache savings: " << std::setprecision(1) << savings << "% vs. no cache ("
               << std::setprecision(2) << (timeWithoutCache - timeWithCache) / 1000.0
               << " ms saved)\n";

        output.flags(flags);
        output.precision(precision);
    }
}

void ShardokGameEngine::resetCacheStatistics() {
    cacheHits_.store(0, std::memory_order_relaxed);
    cacheMisses_.store(0, std::memory_order_relaxed);
    timeInHashComputation_.store(0, std::memory_order_relaxed);
    timeInLegalActionsComputation_.store(0, std::memory_order_relaxed);
}

ChanceOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
        const MCTSGameState& state,
        const MCTSAction& action) const {
    const auto* shardokState = dynamic_cast<const ShardokGameState*>(&state);
    const auto* shardokAction = dynamic_cast<const ShardokAction*>(&action);

    if (!shardokState || !shardokAction) {
        throw ShardokInternalErrorException("Invalid state or action type in getBinaryOutcomeInfo");
    }

    // Check for multi-outcome commands (roll affects outcome quality, not just success/failure)
    // These use multiOutcome() with fixed seeds to sample the range of possible results
    using namespace net::eagle0::shardok::common;
    const auto commandType = static_cast<CommandType>(shardokAction->getType());

    switch (commandType) {
        case END_TURN_COMMAND:
            // END_TURN has random effects (fire spread, weather changes)
            return ChanceOutcomeInfo::multiOutcome(5);

        case MELEE_COMMAND:
        case ARCHERY_COMMAND:
        case CHARGE_COMMAND:
        case REDUCE_COMMAND:
            // Combat/siege commands: OpenEndedPercentile roll affects damage dealt
            // Use 5 outcomes to sample the roll distribution
            return ChanceOutcomeInfo::multiOutcome(5);

        case CHALLENGE_DUEL_COMMAND:
            // Duels have multiple combat rounds with rolls, so outcomes vary significantly
            return ChanceOutcomeInfo::multiOutcome(5);

        default:
            // Continue to binary outcome handling below
            break;
    }

    const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());

    const auto engine = std::make_shared<ShardokEngine>(
            gameSettings_,
            shardokState->getShardokState(),
            criticalTileCoords_,
            0,
            false);

    // Get command descriptors
    const auto descriptors = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
    const size_t actionIndex = shardokAction->getIndex();

    if (actionIndex >= descriptors->size()) {
        throw ShardokInternalErrorException("Action index out of range in getBinaryOutcomeInfo");
    }

    const auto& descriptor = descriptors->at(actionIndex);

    // Get success probability for binary outcome actions
    if (!descriptor->HasOdds()) {
        throw ShardokInternalErrorException("Action does not have odds in getBinaryOutcomeInfo");
    }

    const auto successChancePercentile = descriptor->GetOddsPercentile();
    const double successProbability = static_cast<double>(successChancePercentile) / 100.0;

    return ChanceOutcomeInfo::binary(successProbability);
}

void ShardokGameEngine::clearLegalActionsCache() { legalActionsCache_.clear(); }

// Extern-linkage function for testing
void clearLegalActionsCache_ForTesting() { ShardokGameEngine::clearLegalActionsCache(); }

}  // namespace shardok::mcts
