From f27f5ebc7ccd0e63a16924dc88914c6cd3479355 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 18 Jul 2023 16:29:02 +0300 Subject: [PATCH] Split BattleAI::activeStack into several smaller methods --- AI/BattleAI/BattleAI.cpp | 295 +++++++++++++++++++----------------- AI/BattleAI/BattleAI.h | 4 + client/CPlayerInterface.cpp | 8 + client/Client.cpp | 1 - 4 files changed, 170 insertions(+), 138 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 048cce325..6b599a7c2 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -93,6 +93,162 @@ void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::share movesSkippedByDefense = 0; } +BattleAction CBattleAI::useHealingTent(const CStack *stack) +{ + auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); + std::map woundHpToStack; + for(const auto * stack : healingTargets) + { + if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) + woundHpToStack[woundHp] = stack; + } + + if(woundHpToStack.empty()) + return BattleAction::makeDefend(stack); + else + return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack +} + +std::optional CBattleAI::findBestCreatureSpell(const CStack *stack) +{ + //TODO: faerie dragon type spell should be selected by server + SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); + if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) + { + const CSpell * spell = creatureSpellToCast.toSpell(); + + if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) + { + std::vector possibleCasts; + spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); + for(auto & target : temp.findPotentialTargets()) + { + PossibleSpellcast ps; + ps.dest = target; + ps.spell = spell; + evaluateCreatureSpellcast(stack, ps); + possibleCasts.push_back(ps); + } + + std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); + if(!possibleCasts.empty() && possibleCasts.front().value > 0) + { + return possibleCasts.front(); + } + } + } + return std::nullopt; +} + +BattleAction CBattleAI::selectStackAction(const CStack * stack) +{ + //evaluate casting spell for spellcasting stack + std::optional bestSpellcast = findBestCreatureSpell(stack); + + HypotheticBattle hb(env.get(), cb); + + PotentialTargets targets(stack, hb); + BattleExchangeEvaluator scoreEvaluator(cb, env); + auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb); + + int64_t score = EvaluationResult::INEFFECTIVE_SCORE; + + + if(targets.possibleAttacks.empty() && bestSpellcast.has_value()) + { + movesSkippedByDefense = 0; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(!targets.possibleAttacks.empty()) + { +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Evaluating attack for %s", stack->getDescription()); +#endif + + auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb); + auto & bestAttack = evaluationResult.bestAttack; + + //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. + if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) + { + // return because spellcast value is damage dealt and score is dps reduce + movesSkippedByDefense = 0; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(evaluationResult.score > score) + { + score = evaluationResult.score; + + logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", + bestAttack.attackerState->unitType()->getJsonKey(), + bestAttack.affectedUnits[0]->unitType()->getJsonKey(), + (int)bestAttack.affectedUnits[0]->getCount(), + (int)bestAttack.from, + (int)bestAttack.attack.attacker->getPosition().hex, + bestAttack.attack.chargeDistance, + bestAttack.attack.attacker->speed(0, true), + bestAttack.defenderDamageReduce, + bestAttack.attackerDamageReduce, bestAttack.attackValue() + ); + + if (moveTarget.score <= score) + { + if(evaluationResult.wait) + { + return BattleAction::makeWait(stack); + } + else if(bestAttack.attack.shooting) + { + movesSkippedByDefense = 0; + return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); + } + else + { + movesSkippedByDefense = 0; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); + } + } + } + } + + //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. + if(moveTarget.score > score) + { + score = moveTarget.score; + + if(stack->waited()) + { + return goTowardsNearest(stack, moveTarget.positions); + } + else + { + return BattleAction::makeWait(stack); + } + } + + if(score <= EvaluationResult::INEFFECTIVE_SCORE + && !stack->hasBonusOfType(BonusType::FLYING) + && stack->unitSide() == BattleSide::ATTACKER + && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + { + auto brokenWallMoat = getBrokenWallMoatHexes(); + + if(brokenWallMoat.size()) + { + movesSkippedByDefense = 0; + + if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) + return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); + else + return goTowardsNearest(stack, brokenWallMoat); + } + } + + return BattleAction::makeDefend(stack); +} + BattleAction CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); @@ -105,17 +261,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack ) if(stack->creatureId() == CreatureID::CATAPULT) return useCatapult(stack); if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) - { - auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); - std::map woundHpToStack; - for(auto stack : healingTargets) - if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) - woundHpToStack[woundHp] = stack; - if(woundHpToStack.empty()) - return BattleAction::makeDefend(stack); - else - return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack - } + return useHealingTent(stack); attemptCastingSpell(); @@ -130,133 +276,8 @@ BattleAction CBattleAI::activeStack( const CStack * stack ) if(auto action = considerFleeingOrSurrendering()) return *action; - //best action is from effective owner point if view, we are effective owner as we received "activeStack" - - //evaluate casting spell for spellcasting stack - std::optional bestSpellcast(std::nullopt); - //TODO: faerie dragon type spell should be selected by server - SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); - if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) - { - const CSpell * spell = creatureSpellToCast.toSpell(); - - if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) - { - std::vector possibleCasts; - spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); - for(auto & target : temp.findPotentialTargets()) - { - PossibleSpellcast ps; - ps.dest = target; - ps.spell = spell; - evaluateCreatureSpellcast(stack, ps); - possibleCasts.push_back(ps); - } - - std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); - if(!possibleCasts.empty() && possibleCasts.front().value > 0) - { - bestSpellcast = std::optional(possibleCasts.front()); - } - } - } - - HypotheticBattle hb(env.get(), cb); - - PotentialTargets targets(stack, hb); - BattleExchangeEvaluator scoreEvaluator(cb, env); - auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb); - - int64_t score = EvaluationResult::INEFFECTIVE_SCORE; - - if(!targets.possibleAttacks.empty()) - { -#if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Evaluating attack for %s", stack->getDescription()); -#endif - - auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb); - auto & bestAttack = evaluationResult.bestAttack; - - //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. - if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) - { - // return because spellcast value is damage dealt and score is dps reduce - movesSkippedByDefense = 0; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - if(evaluationResult.score > score) - { - score = evaluationResult.score; - std::string action; - - if(evaluationResult.wait) - { - result = BattleAction::makeWait(stack); - action = "wait"; - } - else if(bestAttack.attack.shooting) - { - result = BattleAction::makeShotAttack(stack, bestAttack.attack.defender); - action = "shot"; - movesSkippedByDefense = 0; - } - else - { - result = BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); - action = "melee"; - movesSkippedByDefense = 0; - } - - logAi->debug("BattleAI: %s -> %s x %d, %s, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", - bestAttack.attackerState->unitType()->getJsonKey(), - bestAttack.affectedUnits[0]->unitType()->getJsonKey(), - (int)bestAttack.affectedUnits[0]->getCount(), action, (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex, - bestAttack.attack.chargeDistance, bestAttack.attack.attacker->speed(0, true), - bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, bestAttack.attackValue() - ); - } - } - else if(bestSpellcast.has_value()) - { - movesSkippedByDefense = 0; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. - if(moveTarget.score > score) - { - score = moveTarget.score; - - if(stack->waited()) - { - result = goTowardsNearest(stack, moveTarget.positions); - } - else - { - result = BattleAction::makeWait(stack); - } - } - - if(score <= EvaluationResult::INEFFECTIVE_SCORE - && !stack->hasBonusOfType(BonusType::FLYING) - && stack->unitSide() == BattleSide::ATTACKER - && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) - { - auto brokenWallMoat = getBrokenWallMoatHexes(); - - if(brokenWallMoat.size()) - { - movesSkippedByDefense = 0; - - if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) - result = BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); - else - result = goTowardsNearest(stack, brokenWallMoat); - } - } + result = selectStackAction(stack); } catch(boost::thread_interrupted &) { diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 5da4fc01d..368bb234e 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -77,6 +77,10 @@ public: void print(const std::string &text) const; BattleAction useCatapult(const CStack *stack); + BattleAction useHealingTent(const CStack *stack); + BattleAction selectStackAction(const CStack * stack); + std::optional findBestCreatureSpell(const CStack *stack); + void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 7d6be81d4..c86695d24 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -774,6 +774,14 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i logGlobal->trace("Awaiting command for %s", stack->nodeName()); auto stackId = stack->unitId(); auto stackName = stack->nodeName(); + + assert(!cb->battleIsFinished()); + if (cb->battleIsFinished()) + { + logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); + return BattleAction::makeDefend(stack); + } + if (autofightingAI) { if (isAutoFightOn) diff --git a/client/Client.cpp b/client/Client.cpp index fdfd691e6..5605c2297 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -683,7 +683,6 @@ void CClient::stopPlayerBattleAction(PlayerColor color) } playerActionThreads.erase(color); } - } void CClient::stopAllBattleActions()