//
//  ShardokEngine.cpp
//  eagle0
//
//  Created by Dan Crosby on 1/30/15.
//  Copyright (c) 2015 none. All rights reserved.
//

#include "ShardokEngine.hpp"

#include <algorithm>
#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/FallIntoWaterAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/NewRoundAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/PerformUndeadCommandsAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/StartPlayerTurnAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/UpdateOpponentKnowledgeAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifierWithCoords.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/PlayerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/WeatherHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/ActionResultFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/UnitFilter.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_with_resulting_state.pb.h"

namespace shardok {

using net::eagle0::shardok::api::GameStateView;
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
using TileModifierProto = net::eagle0::shardok::common::TileModifier;

[[nodiscard]] auto ShardokEngine::GetCurrentGameStateBytes() const -> byte_vector {
    return gameState.ToByteVector();
}

ShardokEngine::ShardokEngine(
        const GameSettingsSPtr &settings,
        const GameStateW &gsBuffer,
        const int32_t startingHistoryCount,
        const bool trackHistory)
    : gameSettings(settings),
      settingsGetter(settings->GetGetter()),
      availableCommandsFactory(
              AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
      gameState(gsBuffer),
      trackHistory(trackHistory),
      startingHistoryCount(startingHistoryCount),
      startingHistoryState(gsBuffer),
      criticalTileCoords(GetCriticalTileLocations(GetCurrentGameState()->hex_map())) {}

ShardokEngine::ShardokEngine(
        const GameSettingsSPtr &settings,
        vector<Unit> &&unplacedUnorderedUnits,
        const HexMapProto &map,
        const int month,
        const GameId &existingGameId,
        const vector<PlayerInfoProto> &playerInfos,
        const bool trackHistory)
    : gameSettings(settings),
      settingsGetter(settings->GetGetter()),
      availableCommandsFactory(
              AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
      gameState(fb::SetupInitialGameState(
              existingGameId,
              map,
              playerInfos,
              unplacedUnorderedUnits,
              month,
              MonthIsWinter(month),
              settingsGetter)),
      trackHistory(trackHistory),
      criticalTileCoords(GetCriticalTileLocations(gameState->hex_map())) {
    ActionResultProto setupStartAction{};
    setupStartAction.set_type(net::eagle0::shardok::common::SETUP_START);
    setupStartAction.mutable_game_status()->set_state(
            GameStatusProto::State::GameStatus_State_SET_UP);
    *setupStartAction.mutable_game_status()->mutable_description() = "Game setup has begun!";

    ApplyAndAddActionResult(setupStartAction);
}

void ShardokEngine::ApplyAndAddActionResults(const vector<ActionResultProto> &results) {
    for (const ActionResultProto &result : results) { ApplyAndAddActionResult(result); }
}

void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
    gameState = ApplyResult(std::move(gameState), result, settingsGetter);

    if (trackHistory) {
        actionHistory.emplace_back();
        *actionHistory.back().mutable_action_result() = result;
        *actionHistory.back().mutable_state_after_fb() = gameState.ToByteString();
    }
}

ShardokEngine::ShardokEngine(
        const GameSettingsSPtr &settings,
        const vector<ShardokActionWithResultingState> &history,
        const bool trackHistory)
    : gameSettings(settings),
      settingsGetter(settings->GetGetter()),
      availableCommandsFactory(
              AvailableCommandsFactory::MakeAvailableCommandsFactory(settingsGetter)),
      gameState(GameStateW::FromByteString(history.back().state_after_fb())),
      trackHistory(trackHistory),
      actionHistory(history),
      criticalTileCoords(gameState->hex_map()) {}

ShardokEngine::ShardokEngine(const ShardokEngine &toCopy, const bool trackHistory)
    : gameSettings(toCopy.gameSettings),
      settingsGetter(toCopy.gameSettings->GetGetter()),
      availableCommandsFactory(toCopy.availableCommandsFactory),
      gameState(toCopy.gameState),
      trackHistory(trackHistory),
      startingHistoryCount(toCopy.startingHistoryCount),
      criticalTileCoords(toCopy.criticalTileCoords),
      cachedAvailableCommands(toCopy.cachedAvailableCommands) {}

auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
        -> GameStateW {
    if (startingActionId < startingHistoryCount) {
        throw ShardokInternalErrorException("Trying to get a state before history");
    } else if (startingActionId == startingHistoryCount) {
        return startingHistoryState;
    } else {
        return GameStateW::FromByteString(
                actionHistory[startingActionId - startingHistoryCount - 1].state_after_fb());
    }
}

auto ShardokEngine::GetGameHistory(const ActionId lastUpdatedActionId) const
        -> vector<ShardokActionWithResultingState> {
    if (lastUpdatedActionId >= static_cast<int64_t>(actionHistory.size()) + startingHistoryCount) {
        return vector<ShardokActionWithResultingState>();
    }
    return vector<ShardokActionWithResultingState>(
            std::begin(actionHistory) + lastUpdatedActionId - startingHistoryCount,
            std::end(actionHistory));
}

auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
        -> net::eagle0::shardok::api::GameStateView {
    return GameStateFilteredForPlayer(settingsGetter, GetCurrentGameState(), askingPlayer);
}

[[nodiscard]] auto ShardokEngine::FilterNewResults(
        PlayerId askingPlayer,
        int64_t previousActionCount) const -> vector<net::eagle0::shardok::api::ActionResultView> {
    if (previousActionCount < startingHistoryCount) {
        throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
    }

    vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);

    vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};

    GameStateW startingState = (previousActionCount == 0)
                                       ? GameStateW{}
                                       : GetGameStateAtStartOfAction(previousActionCount);
    GameStateView startingView =
            (previousActionCount == 0)
                    ? GameStateView{}
                    : GameStateFilteredForPlayer(settingsGetter, startingState, askingPlayer);

    GameStateW &previousState = startingState;
    GameState *previousStatePtr = nullptr;
    GameStateView &previousView = startingView;

    for (vector<ShardokActionWithResultingState> newHistory =
                 {unfilteredHistory.begin(), unfilteredHistory.end()};
         const ShardokActionWithResultingState &awrs : newHistory) {
        GameStateView viewAfter = GameStateFilteredForPlayer(
                settingsGetter,
                GameStateW::FromByteString(awrs.state_after_fb()),
                askingPlayer);

        if (auto filteredResult = ActionResultFilteredForPlayer(
                    previousStatePtr,
                    previousView,
                    viewAfter,
                    settingsGetter,
                    awrs.action_result(),
                    askingPlayer);
            filteredResult.has_value()) {
            filteredHistory.push_back(*filteredResult);
        }
        previousState = GameStateW::FromByteString(awrs.state_after_fb());
        previousStatePtr = previousState.Get();
        previousView = viewAfter;
    }
    return filteredHistory;
}

auto ShardokEngine::GetFilteredGameHistory(const PlayerId askingPlayer) const
        -> vector<net::eagle0::shardok::api::ActionResultView> {
    return FilterNewResults(askingPlayer, 0);
}

auto ShardokEngine::GetUnitById(const PlayerId askingPlayer, const UnitId unitId) const
        -> net::eagle0::shardok::api::UnitView {
    return UnitFilteredForPlayer(
            settingsGetter,
            GetUnit(unitId),
            askingPlayer,
            AlliedPids(GetCurrentGameState(), askingPlayer));
}

void ShardokEngine::PostWhileCurrentPlayerHasOnlyOneOption(
        std::shared_ptr<RandomGenerator> randomGenerator) {
    while (GetGameStatus()->state() ==
                   net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
           GetCurrentPlayerId() != UNCONTROLLED_PLAYER_ID) {
        if (!PostWhilePlayerHasOnlyOneOption(
                    GetCurrentGameState()->current_player(),
                    randomGenerator))
            break;
    }
}

auto ShardokEngine::PostWhilePlayerHasOnlyOneOption(
        const PlayerId playerId,
        std::shared_ptr<RandomGenerator> randomGenerator) -> bool {
    if (randomGenerator == nullptr) randomGenerator = settingsGetter.GetRandomGenerator();
    bool acted = false;
    while (GetGameStatus()->state() ==
                   net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
           playerId == GetCurrentPlayerId() && playerId != UNCONTROLLED_PLAYER_ID) {
        // if there's only one command available, go ahead and take it.
        cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
                gameState,
                playerId,
                /* includeFollowUps=*/true);
        if (cachedAvailableCommands->size() == 1) {
            const auto loneCommand = cachedAvailableCommands->front();
            cachedAvailableCommands = nullptr;
            PostActionUnchecked(loneCommand, randomGenerator, std::nullopt);
            acted = true;
        } else {
            break;
        }

        auto updateAction = UpdateGameStatusAction(
                GetCurrentGameState(),
                criticalTileCoords,
                settingsGetter,
                false /* atEndOfRound */);
        ApplyAndAddActionResults(updateAction.Execute(gameState, randomGenerator));
    }

    return acted;
}

void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &randomGenerator) {
    if (static_cast<uint32_t>(GetCurrentGameState()->current_player()) >=
        GetCurrentGameState()->player_infos()->size()) {
        ApplyAndAddActionResults(UpdateGameStatusAction(
                                         GetCurrentGameState(),
                                         criticalTileCoords,
                                         settingsGetter,
                                         true /* atEndOfRound */)
                                         .Execute(gameState, randomGenerator));

        ApplyAndAddActionResults(
                UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));

        ApplyAndAddActionResults(
                NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));

        ApplyAndAddActionResults(
                StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
                        .Execute(gameState, randomGenerator));

        ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
                                         .Execute(gameState, randomGenerator));

        ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
                                         .Execute(gameState, randomGenerator));

    } else {
        const auto updateAction = UpdateGameStatusAction(
                GetCurrentGameState(),
                criticalTileCoords,
                settingsGetter,
                false /* atEndOfRound */);
        ApplyAndAddActionResults(updateAction.Execute(gameState, randomGenerator));

        const auto startPlayerTurnAction =
                StartPlayerTurnAction(gameState, gameState->current_player(), settingsGetter);
        ApplyAndAddActionResults(startPlayerTurnAction.Execute(gameState, randomGenerator));
    }
}

void ShardokEngine::PostPlacementCommands(
        const PlayerId player,
        const vector<UnitPlacementInfo> &placementInfos,
        const std::shared_ptr<RandomGenerator> &randomGenerator) {
    if (GetCurrentPlayerId() != player) {
        throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
    }

    const auto placementCommands =
            availableCommandsFactory->GetPlayerSetupCommands(gameState, player);

    // first make sure they're all valid and there are no duplicates
    for (int i = 0; i < placementInfos.size(); i++) {
        const UnitPlacementInfo &pi = placementInfos[i];

        if (auto command = common::FindIf(
                    *placementCommands,
                    [pi](const CommandSPtr &cmd) {
                        return cmd->GetCommandProto().actor().value() == pi.unitId &&
                               cmd->GetCommandProto().target() == pi.location;
                    });
            !command.has_value()) {
            throw ShardokClientErrorException("No such placement info found");
        }

        // check that we're not double-filling any location or double-placing any unit
        for (int j = i + 1; j < placementInfos.size(); j++) {
            const UnitPlacementInfo &other = placementInfos[j];

            if (pi.unitId == other.unitId)
                throw ShardokClientErrorException("Trying to place the same unit twice");
            if (pi.location == other.location)
                throw ShardokClientErrorException(
                        "Trying to place multiple units in the same location");
        }
    }

    cachedAvailableCommands = nullptr;

    // now execute
    for (const auto &pi : placementInfos) {
        auto command = common::FindIf(*placementCommands, [pi](const CommandSPtr &cmd) {
            return cmd->GetCommandProto().actor().value() == pi.unitId &&
                   cmd->GetCommandProto().target() == pi.location;
        });
        for (vector<ActionResult> onePlacementResults =
                     (*command)->Execute(gameState, randomGenerator);
             const ActionResultProto &oneResult : onePlacementResults) {
            HandleActionResult(oneResult, randomGenerator);
        }
    }

    const auto updateAction = UpdateGameStatusAction(
            GetCurrentGameState(),
            criticalTileCoords,
            settingsGetter,
            false /* atEndOfRound */);
    ApplyAndAddActionResults(updateAction.Execute(gameState, randomGenerator));

    if (GetGameStatus()->state() ==
                net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
        GetCurrentGameState()->current_player() != player) {
        HandlePlayerTurnEnd(randomGenerator);
    }
}

void ShardokEngine::PostFinishedPlacementCommand(
        const PlayerId player,
        std::shared_ptr<RandomGenerator> randomGenerator) {
    if (randomGenerator == nullptr) randomGenerator = settingsGetter.GetRandomGenerator();

    if (GetCurrentPlayerId() != player) {
        throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
    }

    const auto placementCommands =
            availableCommandsFactory->GetPlayerSetupCommands(gameState, player);

    const auto command = common::FindIf(*placementCommands, [](const CommandSPtr &cmd) {
        return cmd->GetCommandProto().type() ==
               net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND;
    });

    if (!command.has_value()) {
        throw ShardokClientErrorException("No finish placement command found");
    }
    cachedAvailableCommands = nullptr;

    PostActionUnchecked(command.value(), randomGenerator, std::nullopt);
}

void ShardokEngine::PostCommand(
        const PlayerId player,
        const int64_t commandIndex,
        std::shared_ptr<RandomGenerator> randomGenerator,
        const std::optional<int32_t> roll) {
    if (randomGenerator == nullptr) randomGenerator = settingsGetter.GetRandomGenerator();

    if (GetCurrentPlayerId() != player) {
        throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
    }

    // Make sure this command is available & legal
    if (!cachedAvailableCommands) {
        cachedAvailableCommands = availableCommandsFactory->GetAvailableCommands(
                gameState,
                player,
                /* includeFollowUps=*/true);
    }

    if (commandIndex >= static_cast<int64_t>(cachedAvailableCommands->size()) || commandIndex < 0) {
        throw ShardokClientErrorException(
                string("Illegal command index ") + std::to_string(commandIndex) +
                string(", max is ") + std::to_string(cachedAvailableCommands->size()));
    }

    const CommandSPtr command = (*cachedAvailableCommands)[commandIndex];
    cachedAvailableCommands = nullptr;

    PostActionUnchecked(command, randomGenerator, roll);
}

void ShardokEngine::PostActionUnchecked(
        const CommandSPtr &command,
        const std::shared_ptr<RandomGenerator> &randomGenerator,
        const std::optional<int32_t> roll) {
    for (const vector<ActionResultProto> results =
                 command->CanUseClientRoll()
                         ? command->ExecuteWithRoll(gameState, randomGenerator, roll)
                         : command->Execute(gameState, randomGenerator);
         const ActionResultProto &result : results) {
        HandleActionResult(result, randomGenerator);
    }

    const auto updateAction = UpdateGameStatusAction(
            GetCurrentGameState(),
            criticalTileCoords,
            settingsGetter,
            false /* atEndOfRound */);
    ApplyAndAddActionResults(updateAction.Execute(gameState, randomGenerator));

    if (GetGameStatus()->state() ==
                net::eagle0::shardok::storage::fb::GameStatus_::State_GAME_RUNNING &&
        GetCurrentGameState()->current_player() != command->GetPlayerId()) {
        HandlePlayerTurnEnd(randomGenerator);
    }
}

void ShardokEngine::HandleActionResult(
        const ActionResultProto &actionResult,
        const std::shared_ptr<RandomGenerator> &randomGenerator) {
    ApplyAndAddActionResult(actionResult);

    for (const auto &modifierWithCoords : actionResult.changed_tile_modifiers()) {
        const Coords modifiedCoords = FromCoordsProto(modifierWithCoords.coords());
        const TileModifierProto &modifier = modifierWithCoords.modifiers();

        const Unit *occupant = gameState.GetOccupant(modifiedCoords);
        // Check for swept away hero
        if (const Terrain *terrain = GetTerrain(GetCurrentGameState()->hex_map(), modifiedCoords);
            occupant && IsWater(terrain->type()) && !IsTraversible(modifier) &&
            settingsGetter.GetBattalionType(occupant->battalion().type())
                            ->GetCostToEnterTerrain(terrain)
                            .type == ActionCost::impossible) {
            for (const auto fallResults =
                         HandleUnitFallingIntoWater(terrain, occupant, randomGenerator);
                 const auto &fallResult : fallResults) {
                HandleActionResult(fallResult, randomGenerator);
            }
        }
    }
}

auto ShardokEngine::HandleUnitFallingIntoWater(
        const Terrain *terrain,
        const Unit *unit,
        std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResultProto> {
    // Do nothing if this unit can travel across water
    if (const auto &battType = settingsGetter.GetBattalionType(unit->battalion().type());
        battType->GetCostToEnterTerrain(terrain).type != ActionCost::impossible) {
        return {};
    }

    const auto fallAction = FallIntoWaterAction(
            unit->unit_id(),
            settingsGetter,
            settingsGetter.Backing().fall_in_water_damage_per_troop(),
            settingsGetter.Backing().fall_in_water_base_escape_odds());
    return fallAction.Execute(gameState, std::move(randomGenerator));
}

auto CommandProtosFromActionList(const CommandList &actionList) -> vector<CommandProto> {
    vector<CommandProto> protos;

    std::transform(
            std::begin(actionList),
            std::end(actionList),
            std::back_inserter(protos),
            [](const CommandSPtr &action) { return action->GetCommandProto(); });

    return protos;
}

auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> vector<CommandProto> {
    if (GameIsOver(GetGameStatus())) {
        // The game is over
        return {};
    }

    if (playerId == GetCurrentPlayerId()) return {};

    return CommandProtosFromActionList(
            *availableCommandsFactory->GetPlayerSetupCommands(gameState, playerId));
}

auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) const
        -> CommandListSPtr {
    if (GameIsOver(GetGameStatus())) {
        // The game is over
        return {};
    }

    if (playerId != GetCurrentPlayerId()) return {};

    if (!cachedAvailableCommands) {
        cachedAvailableCommands = UncachedGetAvailableCommands(playerId, false);
    }

    return cachedAvailableCommands;
}

auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const bool includeFollowUps)
        const -> vector<CommandProto> {
    if (GameIsOver(GetGameStatus())) {
        // The game is over
        return {};
    }

    if (playerId != GetCurrentPlayerId()) return {};

    if (!cachedAvailableCommands) {
        cachedAvailableCommands = UncachedGetAvailableCommands(playerId, includeFollowUps);
    }

    return CommandProtosFromActionList(*cachedAvailableCommands);
}

auto ShardokEngine::UncachedGetAvailableCommands(
        const PlayerId playerId,
        const bool includeFollowUps) const -> CommandListSPtr {
    return availableCommandsFactory->GetAvailableCommands(
            gameState,
            playerId,
            /* includeFollowUps=*/includeFollowUps);
}

void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Units &from) {
    for (const auto &unit : from) {
        net::eagle0::shardok::storage::ResolvedUnit ru{};

        switch (unit->status()) {
            case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
                throw ShardokInternalErrorException("Unknown unit status");
            case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
                ru.set_status(net::eagle0::shardok::storage::ResolvedUnit::DESTROYED_SUMMONED_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
                ru.set_status(net::eagle0::shardok::storage::ResolvedUnit::NORMAL_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
                ru.set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
                ru.set_status(
                        net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
                ru.set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
                ru.set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT);
                break;
            case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
            case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
                ru.set_status(
                        net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT);
                break;
        }
        AddUnitToResolved(ru, *unit);

        to.push_back(ru);
    }
}

auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
    if (const auto state = GetGameStatus()->state();
        state != net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
        throw ShardokInternalErrorException(
                "Trying to get the end game units before the game is over");
    }

    const auto &gs = GetCurrentGameState();

    vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
    AddUnits(endgameUnits, *gs->units());

    return endgameUnits;
}

}  // namespace shardok
