1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-28 08:48:48 +02:00

nkai: fixes and skill rewards

This commit is contained in:
Andrii Danylchenko 2023-07-27 15:58:49 +03:00
parent 202e13ce2e
commit c93bb0a502
9 changed files with 261 additions and 129 deletions

View File

@ -1058,6 +1058,11 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
int count = d->creatures[i].first;
CreatureID creID = d->creatures[i].second.back();
if(!recruiter->getSlotFor(creID).validSlot())
{
continue;
}
vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
if(count > 0)
cb->recruitCreatures(d, recruiter, creID, count, i);

View File

@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
logAi->trace("Checking other buildings");
std::vector<std::vector<BuildingID>> otherBuildings = {
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}
{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL},
{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
};
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
}
for(auto & buildingSet : otherBuildings)
{
for(auto & buildingID : buildingSet)
{
if(!developmentInfo.town->hasBuilt(buildingID))
if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
{
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
const CCreature * creature = nullptr;
CreatureID baseCreatureID;
int creatureLevel = -1;
int creatureUpgrade = 0;
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{
int level = toBuild - BuildingID::DWELL_FIRST;
auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN);
auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN
? creatures.at(level / GameConstants::CREATURES_PER_TOWN)
creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
}
else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(0);
}
else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR)
{
creatureLevel = townInfo->hordeLvl.at(1);
}
if(creatureLevel >= 0)
{
auto creatures = townInfo->creatures.at(creatureLevel);
auto creatureID = creatures.size() > creatureUpgrade
? creatures.at(creatureUpgrade)
: creatures.front();
baseCreatureID = creatures.front();
@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo(
}
else
{
creatureGrows = creature->getGrowth();
if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST)
{
creatureGrows = creature->getGrowth();
if(town->hasBuilt(BuildingID::CASTLE))
creatureGrows *= 2;
else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2;
if(town->hasBuilt(BuildingID::CASTLE))
creatureGrows *= 2;
else if(town->hasBuilt(BuildingID::CITADEL))
creatureGrows += creatureGrows / 2;
}
else
{
creatureGrows = creature->getHorde();
}
}
armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);

View File

@ -213,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
{
captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL)
if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
}

View File

@ -49,41 +49,98 @@ Goals::TGoalVec DefenceBehavior::decompose() const
return tasks;
}
bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector<AIPath> & paths)
{
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
for(const AIPath & path : paths)
{
bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{
if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace(
"Hero %s can eliminate danger for town %s using path %s.",
path.targetHero->getObjectName(),
town->getObjectName(),
path.toString());
#endif
return true;
}
}
}
return false;
}
void handleCounterAttack(
const CGTownInstance * town,
const HitMapInfo & treat,
const HitMapInfo & maximumDanger,
Goals::TGoalVec & tasks)
{
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn))
{
auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
for(int i = 0; i < heroCapturingPaths.size(); i++)
{
AIPath & path = heroCapturingPaths[i];
TSubgoal goal = goals[i];
if(!goal || goal->invalid() || !goal->isElementar()) continue;
Composition composition;
composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
}
}
bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks)
{
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
return true;
}
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true;
}
return false;
}
void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
{
logAi->trace("Evaluating defence for %s", town->getNameTranslated());
auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
if(town->garrisonHero)
{
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
return;
}
if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
{
logAi->trace(
"Extracting hero %s from garrison of town %s",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return;
}
}
if(!treatNode.fastestDanger.hero)
{
@ -91,6 +148,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
return;
}
std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks))
{
return;
}
uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
@ -111,74 +177,12 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
std::to_string(treat.turn),
treat.hero->getNameTranslated());
bool treatIsUnderControl = false;
handleCounterAttack(town, treat, treatNode.maximumDanger, tasks);
for(AIPath & path : paths)
if(isTreatUnderControl(town, treat, paths))
{
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < town->visitingHero->getHeroStrength())
continue;
}
else
{
if(town->visitingHero)
continue;
}
if(treat.hero.validAndSet()
&& treat.turn <= 1
&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn))
{
auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
for(int i = 0; i < heroCapturingPaths.size(); i++)
{
AIPath & path = heroCapturingPaths[i];
TSubgoal goal = goals[i];
if(!goal || goal->invalid() || !goal->isElementar()) continue;
Composition composition;
composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
tasks.push_back(Goals::sptr(composition));
}
}
bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
if(treatIsWeak && !needToSaveGrowth)
{
if((path.exchangeCount == 1 && path.turn() < treat.turn)
|| path.turn() < treat.turn - 1
|| (path.turn() < treat.turn && treat.turn >= 2))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace(
"Hero %s can eliminate danger for town %s using path %s.",
path.targetHero->getObjectName(),
town->getObjectName(),
path.toString());
#endif
treatIsUnderControl = true;
break;
}
}
}
if(treatIsUnderControl)
continue;
}
evaluateRecruitingHero(tasks, treat, town);
@ -205,6 +209,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
path.movementCost(),
path.toString());
#endif
auto townDefenseStrength = town->garrisonHero
? town->garrisonHero->getTotalStrength()
: (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength());
if(town->visitingHero && path.targetHero == town->visitingHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
{
if(path.getHeroStrength() < townDefenseStrength)
continue;
}
else
{
if(town->visitingHero)
continue;
}
if(path.turn() <= treat.turn - 2)
{
#if NKAI_TRACE_LEVEL >= 1
@ -296,7 +321,20 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
composition.addNext(DefendTown(town, treat, path));
TGoalVec sequence;
if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1)
{
composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE));
tasks.push_back(Goals::sptr(composition));
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Locking hero %s in garrison of %s",
town->garrisonHero.get()->getObjectName(),
town->getObjectName());
#endif
continue;
}
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
{

View File

@ -16,6 +16,7 @@
#include "../Markers/HeroExchange.h"
#include "../Markers/ArmyUpgrade.h"
#include "GatherArmyBehavior.h"
#include "CaptureObjectsBehavior.h"
#include "../AIUtility.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
@ -235,6 +236,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
#endif
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
#if NKAI_TRACE_LEVEL >= 1
@ -251,11 +254,23 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
hasMainAround = true;
}
for(const AIPath & path : paths)
for(int i = 0; i < paths.size(); i++)
{
auto & path = paths[i];
auto visitGoal = goals[i];
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif
if(visitGoal->invalid())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Not valid way.");
#endif
continue;
}
if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1))
{
#if NKAI_TRACE_LEVEL >= 2
@ -370,11 +385,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
if(isSafe)
{
ExecuteHeroChain newWay(path, upgrader);
newWay.closestWayRatio = 1;
tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay)));
tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal)));
}
}

View File

@ -118,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
void Nullkiller::resetAiState()
{
lockedResources = TResources();
scanDepth = ScanDepth::FULL;
scanDepth = ScanDepth::MAIN_FULL;
playerID = ai->playerID;
lockedHeroes.clear();
dangerHitMap->reset();
@ -158,11 +158,15 @@ void Nullkiller::updateAiState(int pass, bool fast)
PathfinderSettings cfg;
cfg.useHeroChain = useHeroChain;
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
if(scanDepth != ScanDepth::FULL)
if(scanDepth == ScanDepth::SMALL)
{
cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1);
cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
}
if(scanDepth != ScanDepth::ALL_FULL)
{
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
}
boost::this_thread::interruption_point();
@ -233,8 +237,8 @@ void Nullkiller::makeTurn()
updateAiState(i);
Goals::TTask bestTask = taskptr(Goals::Invalid());
do
for(;i <= MAXPASS; i++)
{
Goals::TTaskVec fastTasks = {
choseBestTask(sptr(BuyArmyBehavior()), 1),
@ -248,7 +252,11 @@ void Nullkiller::makeTurn()
executeTask(bestTask);
updateAiState(i, true);
}
} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY);
else
{
break;
}
}
Goals::TTaskVec bestTasks = {
bestTask,
@ -267,7 +275,6 @@ void Nullkiller::makeTurn()
bestTask = choseBestTask(bestTasks);
HeroPtr hero = bestTask->getHero();
HeroRole heroRole = HeroRole::MAIN;
if(hero.validAndSet())
@ -276,20 +283,39 @@ void Nullkiller::makeTurn()
if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
useHeroChain = false;
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::FULL)
&& scanDepth == ScanDepth::MAIN_FULL)
{
useHeroChain = false;
scanDepth = ScanDepth::SMALL;
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth",
"Goal %s has low priority %f so decreasing scan depth to gain performance.",
bestTask->toString(),
bestTask->priority);
}
if(bestTask->priority < MIN_PRIORITY)
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
bestTask->toString(),
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
}
logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString());
return;

View File

@ -40,9 +40,11 @@ enum class HeroLockedReason
enum class ScanDepth
{
FULL = 0,
MAIN_FULL = 0,
SMALL = 1
SMALL = 1,
ALL_FULL = 2
};
class Nullkiller

View File

@ -915,6 +915,7 @@ public:
evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
evaluationContext.closestWayRatio = 1;
if(bi.creatureID != CreatureID::NONE)
{
@ -938,7 +939,12 @@ public:
evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
}
else
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
}
if(evaluationContext.goldReward)
{
auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();

View File

@ -5,7 +5,7 @@ InputVariable: mainTurnDistance
enabled: true
range: 0.000 10.000
lock-range: true
term: LOWEST Ramp 0.250 0.000
term: LOWEST Ramp 0.400 0.000
term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000
term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000
term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800
@ -238,4 +238,22 @@ RuleBlock: gold
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL
rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH
rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM
rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL
rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL
RuleBlock: skill reward
enabled: true
conjunction: AlgebraicProduct
disjunction: AlgebraicSum
implication: AlgebraicProduct
activation: General
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL
rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH