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:
parent
5e8a778e7d
commit
f27f5ebc7c
@ -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 &)
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -683,7 +683,6 @@ void CClient::stopPlayerBattleAction(PlayerColor color)
|
||||
}
|
||||
playerActionThreads.erase(color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CClient::stopAllBattleActions()
|
||||
|
Loading…
x
Reference in New Issue
Block a user