1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-08 22:26:51 +02:00

Merge pull request #5021 from Xilmi/develop

Fix for AI not defending in some cases
This commit is contained in:
Ivan Savenko
2024-12-14 16:33:17 +02:00
committed by GitHub
16 changed files with 288 additions and 47 deletions

View File

@ -167,14 +167,12 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
result = evaluator.selectStackAction(stack); result = evaluator.selectStackAction(stack);
if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
{ {
auto spelCasted = evaluator.attemptCastingSpell(stack); auto spelCasted = evaluator.attemptCastingSpell(stack);
if(spelCasted) if(spelCasted)
return; return;
skipCastUntilNextBattle = true;
} }
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
@ -256,8 +254,6 @@ void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
side = Side; side = Side;
skipCastUntilNextBattle = false;
} }
void CBattleAI::print(const std::string &text) const void CBattleAI::print(const std::string &text) const

View File

@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
bool wasWaitingForRealize; bool wasWaitingForRealize;
bool wasUnlockingGs; bool wasUnlockingGs;
int movesSkippedByDefense; int movesSkippedByDefense;
bool skipCastUntilNextBattle;
public: public:
CBattleAI(); CBattleAI();

View File

@ -119,6 +119,58 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
return result; return result;
} }
std::vector<BattleHex> BattleEvaluator::getCastleHexes()
{
std::vector<BattleHex> result;
// Loop through all wall parts
std::vector<BattleHex> wallHexes;
wallHexes.push_back(50);
wallHexes.push_back(183);
wallHexes.push_back(182);
wallHexes.push_back(130);
wallHexes.push_back(78);
wallHexes.push_back(29);
wallHexes.push_back(12);
wallHexes.push_back(97);
wallHexes.push_back(45);
wallHexes.push_back(62);
wallHexes.push_back(112);
wallHexes.push_back(147);
wallHexes.push_back(165);
for (BattleHex wallHex : wallHexes) {
// Get the starting x-coordinate of the wall hex
int startX = wallHex.getX();
// Initialize current hex with the wall hex
BattleHex currentHex = wallHex;
while (currentHex.isValid()) {
// Check if the x-coordinate has wrapped (smaller than the starting x)
if (currentHex.getX() < startX) {
break;
}
// Add the hex to the result
result.push_back(currentHex);
// Move to the next hex to the right
currentHex = currentHex.cloneInDirection(BattleHex::RIGHT, false);
}
}
return result;
}
bool BattleEvaluator::hasWorkingTowers() const
{
bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
return keepIntact || upperIntact || bottomIntact;
}
std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack) std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
{ {
//TODO: faerie dragon type spell should be selected by server //TODO: faerie dragon type spell should be selected by server
@ -161,6 +213,19 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
float score = EvaluationResult::INEFFECTIVE_SCORE; float score = EvaluationResult::INEFFECTIVE_SCORE;
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
{
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
});
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& !stack->canShoot()
&& hasWorkingTowers()
&& !enemyMellee.empty();
std::vector<BattleHex> castleHexes = getCastleHexes();
for (auto hex : castleHexes)
{
logAi->trace("Castlehex ID: %d Y: %d X: %d", hex, hex.getY(), hex.getX());
}
if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
{ {
@ -174,7 +239,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
logAi->trace("Evaluating attack for %s", stack->getDescription()); logAi->trace("Evaluating attack for %s", stack->getDescription());
#endif #endif
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
auto & bestAttack = evaluationResult.bestAttack; auto & bestAttack = evaluationResult.bestAttack;
cachedAttack.ap = bestAttack; cachedAttack.ap = bestAttack;
@ -227,15 +292,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);
} }
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool bool isTargetOutsideFort = std::none_of(castleHexes.begin(), castleHexes.end(),
{ [&](const BattleHex& hex) {
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u); return hex == bestAttack.from;
}); });
bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& !bestAttack.attack.shooting && !bestAttack.attack.shooting
&& hb->battleGetFortifications().hasMoat && hasWorkingTowers()
&& !enemyMellee.empty() && !enemyMellee.empty()
&& isTargetOutsideFort; && isTargetOutsideFort;
@ -349,6 +412,28 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
auto reachability = cb->getBattle(battleID)->getReachability(stack); auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
{
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
});
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& hasWorkingTowers()
&& !enemyMellee.empty();
if (siegeDefense)
{
vstd::erase_if(avHexes, [&](const BattleHex& hex) {
std::vector<BattleHex> castleHexes = getCastleHexes();
bool isOutsideWall = std::none_of(castleHexes.begin(), castleHexes.end(),
[&](const BattleHex& checkhex) {
return checkhex == hex;
});
return isOutsideWall;
});
}
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{ {
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);

View File

@ -53,6 +53,8 @@ public:
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack); std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets); BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
std::vector<BattleHex> getBrokenWallMoatHexes() const; std::vector<BattleHex> getBrokenWallMoatHexes() const;
static std::vector<BattleHex> getCastleHexes();
bool hasWorkingTowers() const;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void print(const std::string & text) const; void print(const std::string & text) const;
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets); BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);

View File

@ -9,6 +9,7 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "BattleExchangeVariant.h" #include "BattleExchangeVariant.h"
#include "BattleEvaluator.h"
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
AttackerValue::AttackerValue() AttackerValue::AttackerValue()
@ -213,9 +214,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) std::shared_ptr<HypotheticBattle> hb,
bool siegeDefense)
{ {
EvaluationResult result(targets.bestAction()); EvaluationResult result(targets.bestAction());
std::vector<BattleHex> castleHexes = BattleEvaluator::getCastleHexes();
if(!activeStack->waited() && !activeStack->acquireState()->hadMorale) if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
{ {
@ -231,6 +234,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
for(auto & ap : targets.possibleAttacks) for(auto & ap : targets.possibleAttacks)
{ {
if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
continue;
float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited); float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
if(score > result.score) if(score > result.score)
@ -263,6 +269,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
for(auto & ap : targets.possibleAttacks) for(auto & ap : targets.possibleAttacks)
{ {
if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
continue;
float score = evaluateExchange(ap, 0, targets, damageCache, hb); float score = evaluateExchange(ap, 0, targets, damageCache, hb);
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait; bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
@ -350,11 +359,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
if(distance <= speed) if(distance <= speed)
continue; continue;
float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
float closestAllyDistance = std::numeric_limits<float>::max();
for (const battle::Unit* ally : hb->battleAliveUnits()) {
if (ally == activeStack)
continue;
if (ally->unitSide() != activeStack->unitSide())
continue;
float allyDistance = dists.distToNearestNeighbour(ally, enemy);
if (allyDistance < closestAllyDistance)
{
closestAllyDistance = allyDistance;
}
}
// If an ally is closer to the enemy, compute the penaltyMultiplier
if (closestAllyDistance < distance) {
penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
}
auto turnsToRich = (distance - 1) / speed + 1; auto turnsToRich = (distance - 1) / speed + 1;
auto hexes = enemy->getSurroundingHexes(); auto hexes = enemy->getSurroundingHexes();
auto enemySpeed = enemy->getMovementRange(); auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed); auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = speedRatio > 1 ? 1 : speedRatio; auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
for(auto & hex : hexes) for(auto & hex : hexes)
{ {

View File

@ -159,7 +159,8 @@ public:
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb); std::shared_ptr<HypotheticBattle> hb,
bool siegeDefense = false);
float evaluateExchange( float evaluateExchange(
const AttackPossibility & ap, const AttackPossibility & ap,

View File

@ -309,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
? dynamic_cast<const CGTownInstance *>(dwelling) ? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr; : nullptr;
std::set<SlotID> alreadyDisbanded;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--) for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{ {
auto ci = infoFromDC(dwelling->creatures[i]); auto ci = infoFromDC(dwelling->creatures[i]);
@ -322,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
if(!ci.count) continue; if(!ci.count) continue;
// Calculate the market value of the new stack
TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
SlotID dst = hero->getSlotFor(ci.creID); SlotID dst = hero->getSlotFor(ci.creID);
// Keep track of the least valuable slot in the hero's army
SlotID leastValuableSlot;
TResources leastValuableStackValue;
leastValuableStackValue[6] = std::numeric_limits<int>::max();
bool shouldDisband = false;
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
{ {
if(!freeHeroSlots) //no more place for stacks if(!freeHeroSlots) // No free slots; consider replacing
continue; {
// Check for the least valuable existing stack
for (auto& slot : hero->Slots())
{
if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
continue;
if(slot.second->getCreatureID() != CreatureID::NONE)
{
TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
continue;
if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
{
leastValuableStackValue = currentStackValue;
leastValuableSlot = slot.first;
}
}
}
// Decide whether to replace the least valuable stack
if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
{
continue; // Skip if the new stack isn't worth replacing
}
else
{
shouldDisband = true;
}
}
else else
{
freeHeroSlots--; //new slot will be occupied freeHeroSlots--; //new slot will be occupied
}
} }
vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
if(!ci.count) continue; int disbandMalus = 0;
if (shouldDisband)
{
disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
alreadyDisbanded.insert(leastValuableSlot);
}
ci.count -= disbandMalus;
if(ci.count <= 0)
continue;
ci.level = i; //this is important for Dungeon Summoning Portal ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci); creaturesInDwellings.push_back(ci);

View File

@ -505,7 +505,7 @@ void ObjectClusterizer::clusterizeObject(
else if (priority <= 0) else if (priority <= 0)
continue; continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f; bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
if(interestingObject) if(interestingObject)
{ {

View File

@ -64,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
if(reinforcement) if(reinforcement)
{ {
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5))); tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement)));
} }
} }
} }

View File

@ -41,9 +41,6 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
for(auto town : ai->cb->getTownsInfo()) for(auto town : ai->cb->getTownsInfo())
{ {
evaluateDefence(tasks, town, ai); evaluateDefence(tasks, town, ai);
//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
if (!tasks.empty())
break;
} }
return tasks; return tasks;
@ -422,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
if(hero->getTotalStrength() < threat.danger) if(hero->getTotalStrength() < threat.danger)
continue; continue;
bool heroAlreadyHiredInOtherTown = false;
for (const auto& task : tasks)
{
if (auto recruitGoal = dynamic_cast<Goals::RecruitHero*>(task.get()))
{
if (recruitGoal->getHero() == hero)
{
heroAlreadyHiredInOtherTown = true;
break;
}
}
}
if (heroAlreadyHiredInOtherTown)
continue;
auto myHeroes = ai->cb->getHeroesInfo(); auto myHeroes = ai->cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1

View File

@ -124,6 +124,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
{ {
if (ai->cb->getHeroesInfo().size() == 0 if (ai->cb->getHeroesInfo().size() == 0
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5 || treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|| bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol) || (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh())) || (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{ {

View File

@ -397,7 +397,12 @@ void Nullkiller::makeTurn()
if(!executeTask(bestTask)) if(!executeTask(bestTask))
return; return;
updateAiState(i, true); bool fastUpdate = true;
if (bestTask->getHero() != nullptr)
fastUpdate = false;
updateAiState(i, fastUpdate);
} }
else else
{ {

View File

@ -1006,6 +1006,9 @@ public:
Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task); Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
const AIPath & path = chain.getPath(); const AIPath & path = chain.getPath();
if (vstd::isAlmostZero(path.movementCost()))
return;
vstd::amax(evaluationContext.danger, path.getTotalDanger()); vstd::amax(evaluationContext.danger, path.getTotalDanger());
evaluationContext.movementCost += path.movementCost(); evaluationContext.movementCost += path.movementCost();
evaluationContext.closestWayRatio = chain.closestWayRatio; evaluationContext.closestWayRatio = chain.closestWayRatio;
@ -1019,12 +1022,20 @@ public:
evaluationContext.involvesSailing = true; evaluationContext.involvesSailing = true;
} }
float highestCostForSingleHero = 0;
for(auto pair : costsPerHero) for(auto pair : costsPerHero)
{ {
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first); auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
evaluationContext.movementCostByRole[role] += pair.second; evaluationContext.movementCostByRole[role] += pair.second;
if (pair.second > highestCostForSingleHero)
highestCostForSingleHero = pair.second;
} }
if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
{
//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
return;
}
evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
auto hero = task->hero; auto hero = task->hero;
bool checkGold = evaluationContext.danger == 0; bool checkGold = evaluationContext.danger == 0;
@ -1046,13 +1057,13 @@ public:
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target); evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
if (target->ID == Obj::HERO) if (target->ID == Obj::HERO)
evaluationContext.isHero = true; evaluationContext.isHero = true;
if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES) if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
evaluationContext.isEnemy = true; evaluationContext.isEnemy = true;
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
evaluationContext.armyInvolvement += army->getArmyCost();
if(evaluationContext.danger > 0) if(evaluationContext.danger > 0)
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength(); evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
} }
evaluationContext.armyInvolvement += army->getArmyCost();
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength()); vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
@ -1353,17 +1364,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget(); const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
bool arriveNextWeek = false; bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7) if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
arriveNextWeek = true; arriveNextWeek = true;
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d", logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d",
priorityTier, priorityTier,
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.armyInvolvement,
goldRewardPerTurn, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
@ -1378,7 +1390,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
evaluationContext.closestWayRatio, evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
evaluationContext.explorePriority, evaluationContext.explorePriority,
evaluationContext.isDefend); evaluationContext.isDefend,
evaluationContext.isEnemy,
arriveNextWeek);
#endif #endif
switch (priorityTier) switch (priorityTier)
@ -1387,13 +1401,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{ {
if (evaluationContext.turn > 0) if (evaluationContext.turn > 0)
return 0; return 0;
if (evaluationContext.movementCost >= 1)
return 0;
if(evaluationContext.conquestValue > 0) if(evaluationContext.conquestValue > 0)
score = 1000; score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
break; break;
@ -1404,17 +1419,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
score = evaluationContext.armyInvolvement; score = evaluationContext.armyInvolvement;
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
score *= evaluationContext.closestWayRatio;
break; break;
} }
case PriorityTier::KILL: //Take towns / kill heroes that are further away case PriorityTier::KILL: //Take towns / kill heroes that are further away
//FALL_THROUGH
case PriorityTier::FAR_KILL:
{ {
if (evaluationContext.turn > 0 && evaluationContext.isHero) if (evaluationContext.turn > 0 && evaluationContext.isHero)
return 0; return 0;
if (arriveNextWeek && evaluationContext.isEnemy) if (arriveNextWeek && evaluationContext.isEnemy)
return 0; return 0;
if (evaluationContext.conquestValue > 0) if (evaluationContext.conquestValue > 0)
score = 1000; score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty())) if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
@ -1432,8 +1448,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000; score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
break; break;
@ -1446,13 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000; score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
break; break;
} }
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
//FALL_THROUGH
case PriorityTier::FAR_HUNTER_GATHER:
{ {
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend) if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
return 0; return 0;
@ -1468,6 +1488,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score += evaluationContext.strategicalValue * 1000; score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward; score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05; score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
@ -1478,7 +1500,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
if (score > 0) if (score > 0)
{ {
score = 1000; score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
} }
@ -1492,8 +1513,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
return 0; return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0) if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0; return 0;
if (evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000; score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0) if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost; score /= evaluationContext.movementCost;
break; break;
@ -1503,8 +1525,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange) if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
return 0; return 0;
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade) if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
score = 1000; score = evaluationContext.armyInvolvement;
score *= evaluationContext.closestWayRatio;
score /= (evaluationContext.turn + 1); score /= (evaluationContext.turn + 1);
break; break;
} }
@ -1563,13 +1584,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
} }
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f", logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
priorityTier, priorityTier,
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.armyInvolvement,
goldRewardPerTurn, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,

View File

@ -118,6 +118,8 @@ public:
HIGH_PRIO_EXPLORE, HIGH_PRIO_EXPLORE,
HUNTER_GATHER, HUNTER_GATHER,
LOW_PRIO_EXPLORE, LOW_PRIO_EXPLORE,
FAR_KILL,
FAR_HUNTER_GATHER,
DEFEND DEFEND
}; };

View File

@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai)
if(ci.count) if(ci.count)
{ {
cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level); if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE)
{
SlotID lowestValueSlot;
int lowestValue = std::numeric_limits<int>::max();
for (auto slot : town->getUpperArmy()->Slots())
{
if (slot.second->getCreatureID() != CreatureID::NONE)
{
int currentStackMarketValue =
slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount();
if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
continue;
if (currentStackMarketValue < lowestValue)
{
lowestValue = currentStackMarketValue;
lowestValueSlot = slot.first;
}
}
}
if (lowestValueSlot.validSlot())
{
cb->dismissCreature(town->getUpperArmy(), lowestValueSlot);
}
}
if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction
{
cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
}
valueBought += ci.count * ci.creID.toCreature()->getAIValue(); valueBought += ci.count * ci.creID.toCreature()->getAIValue();
} }
} }

View File

@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
for(auto & creatureToBuy : buyArmy) for(auto & creatureToBuy : buyArmy)
{ {
auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature()); auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
if (targetSlot.validSlot())
target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); {
target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
target->requireBuyArmy = true; target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
target->requireBuyArmy = true;
}
} }
} }