1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-21 21:17:49 +02:00

Split BattleAI::activeStack into several smaller methods

This commit is contained in:
Ivan Savenko 2023-07-18 16:29:02 +03:00
parent 5e8a778e7d
commit f27f5ebc7c
4 changed files with 170 additions and 138 deletions

View File

@ -93,6 +93,162 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
movesSkippedByDefense = 0;
}
BattleAction CBattleAI::useHealingTent(const CStack *stack)
{
auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
std::map<int, const CStack*> 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<PossibleSpellcast> 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<PossibleSpellcast> 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<PossibleSpellcast> 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<int, const CStack*> 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<PossibleSpellcast> 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<PossibleSpellcast> 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<PossibleSpellcast>(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 &)
{

View File

@ -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<PossibleSpellcast> 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

View File

@ -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)

View File

@ -683,7 +683,6 @@ void CClient::stopPlayerBattleAction(PlayerColor color)
}
playerActionThreads.erase(color);
}
}
void CClient::stopAllBattleActions()