diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index b1e5172e0..9b15eafb6 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -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 diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index f36c2cb26..18df749f0 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface bool wasWaitingForRealize; bool wasUnlockingGs; int movesSkippedByDefense; - bool skipCastUntilNextBattle; public: CBattleAI(); diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index d591b8d7d..a9f32affd 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -119,6 +119,58 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const return result; } +std::vector BattleEvaluator::getCastleHexes() +{ + std::vector result; + + // Loop through all wall parts + + std::vector 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 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 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 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); diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index 0f00ffc7c..7385a0809 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -53,6 +53,8 @@ public: std::optional findBestCreatureSpell(const CStack * stack); BattleAction goTowardsNearest(const CStack * stack, std::vector hexes, const PotentialTargets & targets); std::vector getBrokenWallMoatHexes() const; + static std::vector 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); diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 07576eb1f..357df8f70 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -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 hb) + std::shared_ptr hb, + bool siegeDefense) { EvaluationResult result(targets.bestAction()); + std::vector 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::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(enemySpeed); - auto multiplier = speedRatio > 1 ? 1 : speedRatio; + auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier; for(auto & hex : hexes) { diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 7ba3886df..dbbbaab3d 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -159,7 +159,8 @@ public: const battle::Unit * activeStack, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb); + std::shared_ptr hb, + bool siegeDefense = false); float evaluateExchange( const AttackPossibility & ap, diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index b366fa176..a8eb72c34 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -309,6 +309,8 @@ std::vector ArmyManager::getArmyAvailableToBuy( ? dynamic_cast(dwelling) : nullptr; + std::set alreadyDisbanded; + for(int i = dwelling->creatures.size() - 1; i >= 0; i--) { auto ci = infoFromDC(dwelling->creatures[i]); @@ -322,18 +324,71 @@ std::vector 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::max(); + bool shouldDisband = false; if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack { - if(!freeHeroSlots) //no more place for stacks - continue; + 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); diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5b9e0a12b..dd7173b74 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -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) { diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 738196e2d..b8bb5e470 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -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))); } } } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 86bd7e77c..a76008523 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -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(task.get())) + { + if (recruitGoal->getHero() == hero) + { + heroAlreadyHiredInOtherTown = true; + break; + } + } + } + if (heroAlreadyHiredInOtherTown) + continue; + auto myHeroes = ai->cb->getHeroesInfo(); #if NKAI_TRACE_LEVEL >= 1 diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 16c19ed62..fd87347a6 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -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())) { diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c21f1c57d..b2c3d0eb1 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -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 { diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 840f5052f..fe08da693 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1006,6 +1006,9 @@ public: Goals::ExecuteHeroChain & chain = dynamic_cast(*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, diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index ee983e43b..a403ee6bd 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -118,6 +118,8 @@ public: HIGH_PRIO_EXPLORE, HUNTER_GATHER, LOW_PRIO_EXPLORE, + FAR_KILL, + FAR_HUNTER_GATHER, DEFEND }; diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index 861097eb8..cdd7d9d84 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai) 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::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(); } } diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 4d3a86a28..8db9230cc 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade( for(auto & creatureToBuy : buyArmy) { auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature()); - - target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); - target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; - target->requireBuyArmy = true; + if (targetSlot.validSlot()) + { + target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); + target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; + target->requireBuyArmy = true; + } } }