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

#include "ShardokGameEngine.hpp"

#include <algorithm>
#include <chrono>
#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 {

// 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};

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)
    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());

    // Use cached engine if available (avoids recomputing GetAvailableCommands for same state)
    std::shared_ptr<ShardokEngine> engine;
    if (auto cachedEngine = shardokState->getCachedEngine()) {
        // Clone the cached engine to preserve command cache
        engine = std::make_shared<ShardokEngine>(*cachedEngine);
    } else {
        // Create fresh engine and populate command cache
        engine = std::make_shared<ShardokEngine>(
                gameSettings_,
                shardokState->getShardokState(),
                criticalTileCoords_,
                0,
                false);
        // Populate command cache (result intentionally unused, just populating cache)
        [[maybe_unused]] const auto commands =
                engine->GetAvailableCommandsForAIPlayer(currentPlayer);
        // Cache the engine for future use with this state
        shardokState->setCachedEngine(engine);
        // Clone it for applying the action (don't mutate the cached engine)
        engine = std::make_shared<ShardokEngine>(*engine);
    }

    // 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_);

    // Cache the engine on the new state so score() can use it for END_TURN normalization
    // The engine's command list may be stale after the action was applied, but that's OK -
    // we'll refresh it when we call GetAvailableCommandsForAIPlayer() in score()
    newState->setCachedEngine(engine);

    // 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());

    // Use cached engine if available
    std::shared_ptr<ShardokEngine> engine;
    if (auto cachedEngine = shardokState->getCachedEngine()) {
        engine = std::make_shared<ShardokEngine>(*cachedEngine);
    } else {
        engine = std::make_shared<ShardokEngine>(
                gameSettings_,
                shardokState->getShardokState(),
                criticalTileCoords_,
                0,
                false);
        // Populate command cache (result intentionally unused, just populating cache)
        [[maybe_unused]] const auto commands =
                engine->GetAvailableCommandsForAIPlayer(currentPlayer);
        shardokState->setCachedEngine(engine);
        engine = std::make_shared<ShardokEngine>(*engine);
    }

    engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
    shardokState->getMutableShardokState() = engine->GetCurrentGameState();
    // Clear the cached engine and hash since the state has been mutated
    shardokState->setCachedEngine(nullptr);
    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 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(stateHash); it != legalActionsCache_.end()) {
        cacheHits_.fetch_add(1, std::memory_order_relaxed);

        // Use cached engine
        shardokState->setCachedEngine(it->second.engine);

        // Get commands from the cached engine (Engine already caches these internally)
        const CommandListSPtr commands =
                it->second.engine->GetAvailableCommandsForAIPlayer(currentPlayer);

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

        // Convert to MCTSActions using stored filtered indices
        std::vector<std::unique_ptr<MCTSAction>> actions;
        actions.reserve(it->second.filteredIndices.size());

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

                // Extract essential fields directly from command (no proto conversion!)
                actions.push_back(std::make_unique<ShardokAction>(
                        origIdx,
                        cmd->GetCommandType(),
                        cmd->GetPlayerId(),
                        cmd->GetActorUnitId(),
                        cmd->GetTargetRow(),
                        cmd->GetTargetColumn(),
                        cmd->HasOdds()));
            }
        }

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

        std::vector<size_t> sortedIndices(actions.size());
        std::iota(sortedIndices.begin(), sortedIndices.end(), 0);

        std::sort(sortedIndices.begin(), sortedIndices.end(), [&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;
    }

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

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

    // Use cached engine if available, otherwise create and cache it
    std::shared_ptr<ShardokEngine> engine;
    if (auto cachedEngine = shardokState->getCachedEngine()) {
        engine = cachedEngine;
    } else {
        engine = std::make_shared<ShardokEngine>(
                gameSettings_,
                shardokState->getShardokState(),
                criticalTileCoords_);
        shardokState->setCachedEngine(engine);
    }

    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,
            isDefender_,
            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());

    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!)
            actions.push_back(std::make_unique<ShardokAction>(
                    idx,
                    cmd->GetCommandType(),
                    cmd->GetPlayerId(),
                    cmd->GetActorUnitId(),
                    cmd->GetTargetRow(),
                    cmd->GetTargetColumn(),
                    cmd->HasOdds()));
        }
    }

    // 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);

    // Create index vector for sorting
    std::vector<size_t> sortedIndices(actions.size());
    std::iota(sortedIndices.begin(), sortedIndices.end(), 0);

    // Sort indices by weight (descending)
    std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
        return weights[a] > weights[b];
    });

    // Reorder actions according to sorted indices
    std::vector<std::unique_ptr<MCTSAction>> sortedActions;
    sortedActions.reserve(actions.size());
    for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
    actions = std::move(sortedActions);

    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 in transposition table for future lookups
    // Note: We only store filtered indices and the engine (which caches commands internally)
    // This avoids duplicating heavy protocol buffer objects
    // Use lazy_emplace_l to ensure thread-safe insertion (locks the bucket during construction)
    legalActionsCache_.lazy_emplace_l(
            stateHash,
            [&](typename decltype(legalActionsCache_)::value_type& v) {
                // Update existing entry
                v.second.filteredIndices = filteredIndices;
                v.second.engine = engine;
            },
            [&](const typename decltype(legalActionsCache_)::constructor& ctor) {
                // Create new entry
                ctor(stateHash, LegalActionsCache{filteredIndices, engine});
            });

    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;
    indices.reserve(actions.size());
    for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
    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");
    }

    // Get cached engine and command list for looking up command protos
    auto cachedEngine = shardokState->getCachedEngine();
    if (!cachedEngine) {
        throw MCTSInternalError(
                "ShardokGameEngine::getActionWeights called with state that has no cached engine");
    }

    const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
    const CommandListSPtr commands = cachedEngine->GetAvailableCommandsForAIPlayer(currentPlayer);

    // Determine if current player is defender (not root player!)
    // During simulation we need to use the correct perspective for action weighting
    bool currentPlayerIsDefender = false;
    const auto& gameState = shardokState->getShardokState();
    for (const auto* pi : *gameState->player_infos()) {
        if (pi->player_id() == currentPlayer) {
            currentPlayerIsDefender = pi->is_defender();
            break;
        }
    }

    // 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");
        }

        // Look up command proto from cached engine using action's index
        const size_t cmdIndex = shardokAction->getIndex();
        if (cmdIndex >= commands->size()) {
            throw MCTSInternalError(
                    "ShardokGameEngine::getActionWeights: action index out of bounds");
        }

        const auto& cmd = commands->at(cmdIndex);

        weights.push_back(AIHeuristicWeighting::GetCommandWeight(
                cmd->GetCommandType(),
                cmd->GetActorUnitId(),
                cmd->GetPlayerId(),
                Coords{cmd->GetTargetRow(), cmd->GetTargetColumn()},
                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 (uses cached engine)
    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 {
    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;

        printf("Legal Actions Cache Stats:\n");
        printf("  Lookups: %llu hits, %llu misses, %.1f%% hit rate, %zu entries\n",
               static_cast<unsigned long long>(hits),
               static_cast<unsigned long long>(misses),
               hitRate * 100.0,
               legalActionsCache_.size());
        printf("  Timing: %.2f us avg hash, %.2f us avg actions (on miss)\n",
               avgHashTimeUs,
               avgActionsTimeUs);
        printf("  Total time: %.2f ms in hash, %.2f ms in actions\n",
               hashTime / 1000.0,
               actionsTime / 1000.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;
        printf("  Cache savings: %.1f%% vs. no cache (%.2f ms saved)\n",
               savings,
               (timeWithoutCache - timeWithCache) / 1000.0);
    }
}

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());

    // Get or create the engine for this state
    std::shared_ptr<ShardokEngine> engine;
    if (auto cachedEngine = shardokState->getCachedEngine()) {
        engine = cachedEngine;
    } else {
        engine = std::make_shared<ShardokEngine>(
                gameSettings_,
                shardokState->getShardokState(),
                criticalTileCoords_,
                0,
                false);
        // Populate command cache
        [[maybe_unused]] const auto commands =
                engine->GetAvailableCommandsForAIPlayer(currentPlayer);
        shardokState->setCachedEngine(engine);
    }

    // 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
