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:
@ -167,14 +167,12 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
|
||||
|
||||
result = evaluator.selectStackAction(stack);
|
||||
|
||||
if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
|
||||
if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
|
||||
{
|
||||
auto spelCasted = evaluator.attemptCastingSpell(stack);
|
||||
|
||||
if(spelCasted)
|
||||
return;
|
||||
|
||||
skipCastUntilNextBattle = true;
|
||||
}
|
||||
|
||||
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);
|
||||
side = Side;
|
||||
|
||||
skipCastUntilNextBattle = false;
|
||||
}
|
||||
|
||||
void CBattleAI::print(const std::string &text) const
|
||||
|
@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
|
||||
bool wasWaitingForRealize;
|
||||
bool wasUnlockingGs;
|
||||
int movesSkippedByDefense;
|
||||
bool skipCastUntilNextBattle;
|
||||
|
||||
public:
|
||||
CBattleAI();
|
||||
|
@ -119,6 +119,58 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
|
||||
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)
|
||||
{
|
||||
//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);
|
||||
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())
|
||||
{
|
||||
@ -174,7 +239,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
||||
logAi->trace("Evaluating attack for %s", stack->getDescription());
|
||||
#endif
|
||||
|
||||
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
|
||||
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
|
||||
auto & bestAttack = evaluationResult.bestAttack;
|
||||
|
||||
cachedAttack.ap = bestAttack;
|
||||
@ -227,15 +292,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
||||
return BattleAction::makeDefend(stack);
|
||||
}
|
||||
|
||||
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
|
||||
{
|
||||
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
|
||||
bool isTargetOutsideFort = std::none_of(castleHexes.begin(), castleHexes.end(),
|
||||
[&](const BattleHex& hex) {
|
||||
return hex == bestAttack.from;
|
||||
});
|
||||
|
||||
bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
|
||||
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
||||
&& !bestAttack.attack.shooting
|
||||
&& hb->battleGetFortifications().hasMoat
|
||||
&& hasWorkingTowers()
|
||||
&& !enemyMellee.empty()
|
||||
&& isTargetOutsideFort;
|
||||
|
||||
@ -349,6 +412,28 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
|
||||
auto reachability = cb->getBattle(battleID)->getReachability(stack);
|
||||
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
|
||||
{
|
||||
return BattleAction::makeDefend(stack);
|
||||
|
@ -53,6 +53,8 @@ public:
|
||||
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
|
||||
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
|
||||
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 print(const std::string & text) const;
|
||||
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
|
||||
|
@ -9,6 +9,7 @@
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "BattleExchangeVariant.h"
|
||||
#include "BattleEvaluator.h"
|
||||
#include "../../lib/CStack.h"
|
||||
|
||||
AttackerValue::AttackerValue()
|
||||
@ -213,9 +214,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
const battle::Unit * activeStack,
|
||||
PotentialTargets & targets,
|
||||
DamageCache & damageCache,
|
||||
std::shared_ptr<HypotheticBattle> hb)
|
||||
std::shared_ptr<HypotheticBattle> hb,
|
||||
bool siegeDefense)
|
||||
{
|
||||
EvaluationResult result(targets.bestAction());
|
||||
std::vector<BattleHex> castleHexes = BattleEvaluator::getCastleHexes();
|
||||
|
||||
if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
|
||||
{
|
||||
@ -231,6 +234,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
|
||||
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);
|
||||
|
||||
if(score > result.score)
|
||||
@ -263,6 +269,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
||||
|
||||
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);
|
||||
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
|
||||
|
||||
@ -350,11 +359,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
|
||||
if(distance <= speed)
|
||||
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 hexes = enemy->getSurroundingHexes();
|
||||
auto enemySpeed = enemy->getMovementRange();
|
||||
auto speedRatio = speed / static_cast<float>(enemySpeed);
|
||||
auto multiplier = speedRatio > 1 ? 1 : speedRatio;
|
||||
auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
|
||||
|
||||
for(auto & hex : hexes)
|
||||
{
|
||||
|
@ -159,7 +159,8 @@ public:
|
||||
const battle::Unit * activeStack,
|
||||
PotentialTargets & targets,
|
||||
DamageCache & damageCache,
|
||||
std::shared_ptr<HypotheticBattle> hb);
|
||||
std::shared_ptr<HypotheticBattle> hb,
|
||||
bool siegeDefense = false);
|
||||
|
||||
float evaluateExchange(
|
||||
const AttackPossibility & ap,
|
||||
|
@ -309,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
|
||||
? dynamic_cast<const CGTownInstance *>(dwelling)
|
||||
: nullptr;
|
||||
|
||||
std::set<SlotID> alreadyDisbanded;
|
||||
|
||||
for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
|
||||
{
|
||||
auto ci = infoFromDC(dwelling->creatures[i]);
|
||||
@ -322,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
|
||||
|
||||
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);
|
||||
|
||||
// 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(!freeHeroSlots) //no more place for stacks
|
||||
if(!freeHeroSlots) // No free slots; consider replacing
|
||||
{
|
||||
// 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
|
||||
{
|
||||
freeHeroSlots--; //new slot will be occupied
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
creaturesInDwellings.push_back(ci);
|
||||
|
@ -505,7 +505,7 @@ void ObjectClusterizer::clusterizeObject(
|
||||
else if (priority <= 0)
|
||||
continue;
|
||||
|
||||
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
||||
bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
|
||||
|
||||
if(interestingObject)
|
||||
{
|
||||
|
@ -64,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
if(reinforcement)
|
||||
{
|
||||
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5)));
|
||||
tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +41,6 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
|
||||
for(auto town : ai->cb->getTownsInfo())
|
||||
{
|
||||
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;
|
||||
@ -422,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
|
||||
if(hero->getTotalStrength() < threat.danger)
|
||||
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();
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
|
@ -124,6 +124,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
if (ai->cb->getHeroesInfo().size() == 0
|
||||
|| 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] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||
{
|
||||
|
@ -397,7 +397,12 @@ void Nullkiller::makeTurn()
|
||||
if(!executeTask(bestTask))
|
||||
return;
|
||||
|
||||
updateAiState(i, true);
|
||||
bool fastUpdate = true;
|
||||
|
||||
if (bestTask->getHero() != nullptr)
|
||||
fastUpdate = false;
|
||||
|
||||
updateAiState(i, fastUpdate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1006,6 +1006,9 @@ public:
|
||||
Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
|
||||
const AIPath & path = chain.getPath();
|
||||
|
||||
if (vstd::isAlmostZero(path.movementCost()))
|
||||
return;
|
||||
|
||||
vstd::amax(evaluationContext.danger, path.getTotalDanger());
|
||||
evaluationContext.movementCost += path.movementCost();
|
||||
evaluationContext.closestWayRatio = chain.closestWayRatio;
|
||||
@ -1019,12 +1022,20 @@ public:
|
||||
evaluationContext.involvesSailing = true;
|
||||
}
|
||||
|
||||
float highestCostForSingleHero = 0;
|
||||
for(auto pair : costsPerHero)
|
||||
{
|
||||
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
|
||||
|
||||
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;
|
||||
bool checkGold = evaluationContext.danger == 0;
|
||||
@ -1046,13 +1057,13 @@ public:
|
||||
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||
if (target->ID == Obj::HERO)
|
||||
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.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
|
||||
evaluationContext.armyInvolvement += army->getArmyCost();
|
||||
if(evaluationContext.danger > 0)
|
||||
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
|
||||
}
|
||||
evaluationContext.armyInvolvement += army->getArmyCost();
|
||||
|
||||
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
#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,
|
||||
task->toString(),
|
||||
evaluationContext.armyLossPersentage,
|
||||
(int)evaluationContext.turn,
|
||||
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||
evaluationContext.armyInvolvement,
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
@ -1378,7 +1390,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
evaluationContext.closestWayRatio,
|
||||
evaluationContext.enemyHeroDangerRatio,
|
||||
evaluationContext.explorePriority,
|
||||
evaluationContext.isDefend);
|
||||
evaluationContext.isDefend,
|
||||
evaluationContext.isEnemy,
|
||||
arriveNextWeek);
|
||||
#endif
|
||||
|
||||
switch (priorityTier)
|
||||
@ -1387,13 +1401,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
{
|
||||
if (evaluationContext.turn > 0)
|
||||
return 0;
|
||||
if (evaluationContext.movementCost >= 1)
|
||||
return 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()))
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
@ -1404,17 +1419,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
score = evaluationContext.armyInvolvement;
|
||||
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
break;
|
||||
}
|
||||
case PriorityTier::KILL: //Take towns / kill heroes that are further away
|
||||
//FALL_THROUGH
|
||||
case PriorityTier::FAR_KILL:
|
||||
{
|
||||
if (evaluationContext.turn > 0 && evaluationContext.isHero)
|
||||
return 0;
|
||||
if (arriveNextWeek && evaluationContext.isEnemy)
|
||||
return 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()))
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
@ -1432,8 +1448,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
@ -1446,13 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
}
|
||||
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
|
||||
//FALL_THROUGH
|
||||
case PriorityTier::FAR_HUNTER_GATHER:
|
||||
{
|
||||
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
|
||||
return 0;
|
||||
@ -1468,6 +1488,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score += evaluationContext.strategicalValue * 1000;
|
||||
score += evaluationContext.goldReward;
|
||||
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)
|
||||
{
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
}
|
||||
@ -1492,8 +1513,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
return 0;
|
||||
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||
return 0;
|
||||
if (evaluationContext.closestWayRatio < 1.0)
|
||||
return 0;
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
if (evaluationContext.movementCost > 0)
|
||||
score /= evaluationContext.movementCost;
|
||||
break;
|
||||
@ -1503,8 +1525,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
|
||||
return 0;
|
||||
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
|
||||
score = 1000;
|
||||
score *= evaluationContext.closestWayRatio;
|
||||
score = evaluationContext.armyInvolvement;
|
||||
score /= (evaluationContext.turn + 1);
|
||||
break;
|
||||
}
|
||||
@ -1563,13 +1584,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
}
|
||||
|
||||
#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,
|
||||
task->toString(),
|
||||
evaluationContext.armyLossPersentage,
|
||||
(int)evaluationContext.turn,
|
||||
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||
evaluationContext.armyInvolvement,
|
||||
goldRewardPerTurn,
|
||||
evaluationContext.goldCost,
|
||||
evaluationContext.armyReward,
|
||||
|
@ -118,6 +118,8 @@ public:
|
||||
HIGH_PRIO_EXPLORE,
|
||||
HUNTER_GATHER,
|
||||
LOW_PRIO_EXPLORE,
|
||||
FAR_KILL,
|
||||
FAR_HUNTER_GATHER,
|
||||
DEFEND
|
||||
};
|
||||
|
||||
|
@ -57,8 +57,37 @@ void BuyArmy::accept(AIGateway * ai)
|
||||
vstd::amin(ci.count, res / ci.creID.toCreature()->getFullRecruitCost());
|
||||
|
||||
if(ci.count)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -374,12 +374,14 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
|
||||
for(auto & creatureToBuy : buyArmy)
|
||||
{
|
||||
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->requireBuyArmy = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(target->getArmyStrength() <= army->getArmyStrength())
|
||||
{
|
||||
|
Reference in New Issue
Block a user