1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-12 10:03:53 +02:00

Merge pull request #4274 from Xilmi/develop

Rule based AI
This commit is contained in:
Ivan Savenko 2024-11-26 18:32:58 +02:00 committed by GitHub
commit 4180ea9523
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1057 additions and 289 deletions

View File

@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest); cast.castEval(state->getServerCallback(), ps.dest);
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{ {
@ -731,7 +731,55 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
} }
//! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell
for (const auto& unit : all)
{
if (!unit->isValidTarget())
continue;
bool isDead = true;
for (const auto& remainingUnit : allUnits)
{
if (remainingUnit->unitId() == unit->unitId())
isDead = false;
}
if (isDead)
{
auto newHealth = 0;
auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0);
if (oldHealth != newHealth)
{
auto damage = std::abs(oldHealth - newHealth);
auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId());
auto dpsReduce = AttackPossibility::calculateDamageReduce(
nullptr,
originalDefender && originalDefender->alive() ? originalDefender : unit,
damage,
innerCache,
state);
auto ourUnit = unit->unitSide() == side ? 1 : -1;
auto goodEffect = newHealth > oldHealth ? 1 : -1;
if (ourUnit * goodEffect == 1)
{
if (ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
}
else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace(
"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
ps.dest.at(0).hexValue.hex,
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
#endif
}
}
}
for(const auto & unit : allUnits) for(const auto & unit : allUnits)
{ {
if(!unit->isValidTarget(true)) if(!unit->isValidTarget(true))
@ -771,11 +819,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace( // Ensure ps.dest is not empty before accessing the first element
"Spell affects %s (%d), dps: %2f", if (!ps.dest.empty())
unit->creatureId().toCreature()->getNameSingularTranslated(), {
unit->getCount(), logAi->trace(
dpsReduce); "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
else
{
// Handle the case where ps.dest is empty
logAi->trace(
"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
#endif #endif
} }
} }

View File

@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
{ {
std::vector<const battle::Unit *> result; std::vector<const battle::Unit *> result;
for(int i = 0; i < turnOrder.size(); i++) for(int i = 0; i < turnOrder.size(); i++, turn++)
{ {
auto & turnQueue = turnOrder[i]; auto & turnQueue = turnOrder[i];
HypotheticBattle turnBattle(env.get(), cb); HypotheticBattle turnBattle(env.get(), cb);

View File

@ -148,7 +148,7 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength) bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
{ {
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength(); const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
if(dangerStrength) if(dangerStrength)
{ {

View File

@ -152,16 +152,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
uint64_t armyValue = 0; uint64_t armyValue = 0;
TemporaryArmy newArmyInstance; TemporaryArmy newArmyInstance;
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
for(auto bonus : *bonusModifiers)
{
// army bonuses will change and object bonuses are temporary
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
{
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
}
}
while(allowedFactions.size() < alignmentMap.size()) while(allowedFactions.size() < alignmentMap.size())
{ {

View File

@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
for(int upgradeIndex : {1, 0}) for(int upgradeIndex : {1, 0})
{ {
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex)); BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building)) if(!vstd::contains(buildings, building))
continue; // no such building in town continue; // no such building in town
@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) 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_1});
otherBuildings.push_back({BuildingID::HORDE_2}); otherBuildings.push_back({BuildingID::HORDE_2});
} }
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
otherBuildings.push_back({ BuildingID::MARKETPLACE });
for(auto & buildingSet : otherBuildings) for(auto & buildingSet : otherBuildings)
{ {
for(auto & buildingID : buildingSet) for(auto & buildingID : buildingSet)
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
auto towns = ai->cb->getTownsInfo(); auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns) for(const CGTownInstance* town : towns)
{ {
logAi->trace("Checking town %s", town->getNameTranslated()); logAi->trace("Checking town %s", town->getNameTranslated());
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
requiredResources += developmentInfo.requiredResources; requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost; totalDevelopmentCost += developmentInfo.townDevelopmentCost;
for(auto building : developmentInfo.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += developmentInfo.armyCost; armyCost += developmentInfo.armyCost;
for(auto bi : developmentInfo.toBuild) for(auto bi : developmentInfo.toBuild)
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
updateDailyIncome(); updateDailyIncome();
if(ai->cb->getDate(Date::DAY) == 1) goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
{
goldPressure = 1;
}
else
{
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
}
logAi->trace("Gold pressure: %f", goldPressure); logAi->trace("Gold pressure: %f", goldPressure);
} }
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
logAi->trace("checking %s", info.name); logAi->trace("checking %s", info.name);
logAi->trace("buildInfo %s", info.toString()); logAi->trace("buildInfo %s", info.toString());
int highestFort = 0;
for (auto twn : ai->cb->getTownsInfo())
{
highestFort = std::max(highestFort, (int)twn->fortLevel());
}
if(!town->hasBuilt(building)) if(!town->hasBuilt(building))
{ {
auto canBuild = ai->cb->canBuildStructure(town, building); auto canBuild = ai->cb->canBuildStructure(town, building);
@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
prerequisite.baseCreatureID = info.baseCreatureID; prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++; prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost; prerequisite.armyCost = info.armyCost;
prerequisite.dailyIncome = info.dailyIncome; bool haveSameOrBetterFort = false;
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
haveSameOrBetterFort = true;
if(!haveSameOrBetterFort)
prerequisite.dailyIncome = info.dailyIncome;
return prerequisite; return prerequisite;
} }

View File

@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN; heroes[hero->tempOwner][hero] = HeroRole::MAIN;
} }
if(obj->ID == Obj::TOWN) if(obj->ID == Obj::TOWN)
{ {
auto town = dynamic_cast<const CGTownInstance *>(obj); auto town = dynamic_cast<const CGTownInstance *>(obj);
@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
newThreat.hero = path.targetHero; newThreat.hero = path.targetHero;
newThreat.turn = path.turn(); newThreat.turn = path.turn();
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
newThreat.danger = path.getHeroStrength(); newThreat.danger = path.getHeroStrength();
if(newThreat.value() > node.maximumDanger.value()) if(newThreat.value() > node.maximumDanger.value())

View File

@ -22,6 +22,7 @@ struct HitMapInfo
uint64_t danger; uint64_t danger;
uint8_t turn; uint8_t turn;
float threat;
HeroPtr hero; HeroPtr hero;
HitMapInfo() HitMapInfo()
@ -33,6 +34,7 @@ struct HitMapInfo
{ {
danger = 0; danger = 0;
turn = 255; turn = 255;
threat = 0;
hero = HeroPtr(); hero = HeroPtr();
} }

View File

@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
{ {
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
} }
void HeroManager::update() void HeroManager::update()
@ -108,7 +108,7 @@ void HeroManager::update()
for(auto & hero : myHeroes) for(auto & hero : myHeroes)
{ {
scores[hero] = evaluateFightingStrength(hero); scores[hero] = evaluateFightingStrength(hero);
knownFightingStrength[hero->id] = hero->getFightingStrength(); knownFightingStrength[hero->id] = hero->getHeroStrength();
} }
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@ -147,7 +147,10 @@ void HeroManager::update()
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
{ {
return heroRoles.at(hero); if (heroRoles.find(hero) != heroRoles.end())
return heroRoles.at(hero);
else
return HeroRole::SCOUT;
} }
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@ -188,10 +191,9 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero); return evaluateFightingStrength(hero);
} }
bool HeroManager::heroCapReached() const bool HeroManager::heroCapReached(bool includeGarrisoned) const
{ {
const bool includeGarnisoned = true; int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
return heroCount >= ALLOWED_ROAMING_HEROES return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= ai->settings->getMaxRoamingHeroes() || heroCount >= ai->settings->getMaxRoamingHeroes()
@ -204,7 +206,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
auto cached = knownFightingStrength.find(hero->id); auto cached = knownFightingStrength.find(hero->id);
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?) //FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength(); return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
} }
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@ -281,7 +283,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
return nullptr; return nullptr;
} }
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
{ {
const CGHeroInstance * weakestHero = nullptr; const CGHeroInstance * weakestHero = nullptr;
auto myHeroes = ai->cb->getHeroesInfo(); auto myHeroes = ai->cb->getHeroesInfo();
@ -292,12 +294,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|| existingHero->getArmyStrength() >armyLimit || existingHero->getArmyStrength() >armyLimit
|| getHeroRole(existingHero) == HeroRole::MAIN || getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining() || existingHero->movementPointsRemaining()
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
{ {
continue; continue;
} }
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
{ {
weakestHero = existingHero; weakestHero = existingHero;
} }

View File

@ -56,9 +56,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const; float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
float evaluateHero(const CGHeroInstance * hero) const; float evaluateHero(const CGHeroInstance * hero) const;
bool canRecruitHero(const CGTownInstance * t = nullptr) const; bool canRecruitHero(const CGTownInstance * t = nullptr) const;
bool heroCapReached() const; bool heroCapReached(bool includeGarrisoned = true) const;
const CGHeroInstance * findHeroWithGrail() const; const CGHeroInstance * findHeroWithGrail() const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const; const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
float getMagicStrength(const CGHeroInstance * hero) const; float getMagicStrength(const CGHeroInstance * hero) const;
float getFightingStrengthCached(const CGHeroInstance * hero) const; float getFightingStrengthCached(const CGHeroInstance * hero) const;

View File

@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
{ {
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord); auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
blockers = ai->cb->getVisitableObjs(node.coord); if (ai->cb->isVisible(node.coord))
blockers = ai->cb->getVisitableObjs(node.coord);
if(guardPos.valid()) if(guardPos.valid() && ai->cb->isVisible(guardPos))
{ {
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord)); auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY) if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue; continue;
ClusterMap::accessor cluster; ClusterMap::accessor cluster;
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero); heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY) if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue; continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f; bool interestingObject = path.turn() <= 2 || priority > 0.5f;

View File

@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo(); auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh(); auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
ai->dangerHitMap->updateHitMap();
for(auto & developmentInfo : developmentInfos) for(auto & developmentInfo : developmentInfos)
{ {
for(auto & buildingInfo : developmentInfo.toBuild) bool emergencyDefense = false;
uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
{ {
if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) closestThreat = std::min(closestThreat, threat.turn);
}
for (auto& buildingInfo : developmentInfo.toBuild)
{
if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
{ {
if(buildingInfo.notEnoughRes) if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
{ {
if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
continue;
Composition composition;
composition.addNext(BuildThis(buildingInfo, developmentInfo));
composition.addNext(SaveResources(buildingInfo.buildCost));
tasks.push_back(sptr(composition));
}
else
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo))); tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
emergencyDefense = true;
}
}
}
if (!emergencyDefense)
{
for (auto& buildingInfo : developmentInfo.toBuild)
{
if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
{
if (buildingInfo.notEnoughRes)
{
if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
continue;
Composition composition;
composition.addNext(BuildThis(buildingInfo, developmentInfo));
composition.addNext(SaveResources(buildingInfo.buildCost));
tasks.push_back(sptr(composition));
}
else
{
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
}
}
} }
} }
} }

View File

@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
{ {
Goals::TGoalVec tasks; Goals::TGoalVec tasks;
if(ai->cb->getDate(Date::DAY) == 1)
return tasks;
auto heroes = cb->getHeroesInfo(); auto heroes = cb->getHeroesInfo();
if(heroes.empty()) if(heroes.empty())
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
return tasks; return tasks;
} }
ai->dangerHitMap->updateHitMap();
for(auto town : cb->getTownsInfo()) for(auto town : cb->getTownsInfo())
{ {
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
{
return tasks;
}
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet( auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
town, town,
ai->getFreeResources()); ai->getFreeResources());
for(const CGHeroInstance * targetHero : heroes) for(const CGHeroInstance * targetHero : heroes)
{ {
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
{
continue;
}
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN) if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
{ {
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet( auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(

View File

@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
logAi->trace("Path found %s", path.toString()); logAi->trace("Path found %s", path.toString());
#endif #endif
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
#endif
continue;
}
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
auto hero = path.targetHero; auto hero = path.targetHero;
auto danger = path.getTotalDanger(); auto danger = path.getTotalDanger();
if (hero->getOwner() != nullkiller->playerID)
continue;
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0) && (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1) && path.exchangeCount > 1)

View File

@ -41,6 +41,9 @@ 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;
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true; return false;
} }
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{ {
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
{ {
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true; return false;
} }
} }
} }
@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
{ {
return; return;
} }
if(!threatNode.fastestDanger.hero) if(!threatNode.fastestDanger.hero)
{ {
logAi->trace("No threat found for town %s", town->getNameTranslated()); logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
if (!path.targetHero->canBeMergedWith(*town))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Can't merge armies of hero %s and town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
// dismiss creatures we are not able to pick to be able to hide in garrison // dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0 || town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{ {
tasks.push_back( tasks.push_back(
@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
} }
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{ {
if(town->garrisonHero) if(town->garrisonHero && town->garrisonHero != path.targetHero)
{ {
if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
{
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(), path.targetHero->getObjectName(),
town->getObjectName()); town->getObjectName());
#endif #endif
continue; continue;
}
} }
else if(path.turn() == 0) else if(path.turn() == 0)
{ {
@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
{ {
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
return;
if(town->hasBuilt(BuildingID::TAVERN) if(town->hasBuilt(BuildingID::TAVERN)
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) && ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{ {
@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
} }
else if(ai->heroManager->heroCapReached()) else if(ai->heroManager->heroCapReached())
{ {
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
if(!heroToDismiss) if(!heroToDismiss)
continue; continue;

View File

@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
{ {
Goals::TGoalVec tasks; Goals::TGoalVec tasks;
for(auto obj : ai->memory->visitableObjs) for (auto obj : ai->memory->visitableObjs)
{ {
if(!vstd::contains(ai->memory->alreadyVisited, obj)) switch (obj->ID.num)
{ {
switch(obj->ID.num)
{
case Obj::REDWOOD_OBSERVATORY: case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE: case Obj::PILLAR_OF_FIRE:
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj)))); {
auto rObj = dynamic_cast<const CRewardableObject*>(obj);
if (!rObj->wasScouted(ai->playerID))
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
break; break;
}
case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY: case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE: case Obj::SUBTERRANEAN_GATE:
case Obj::WHIRLPOOL: case Obj::WHIRLPOOL:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
{
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
}
break;
}
}
else
{
switch(obj->ID.num)
{ {
case Obj::MONOLITH_TWO_WAY: auto tObj = dynamic_cast<const CGTeleport*>(obj);
case Obj::SUBTERRANEAN_GATE: for (auto exit : cb->getTeleportChannelExits(tObj->channel))
case Obj::WHIRLPOOL:
auto tObj = dynamic_cast<const CGTeleport *>(obj);
if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
break;
for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
{ {
if(!cb->getObj(exit)) if (exit != tObj->id)
{ {
// Always attempt to visit two-way teleports if one of channel exits is not visible if (!cb->isVisible(cb->getObjInstance(exit)))
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj)))); tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
break;
} }
} }
break;
} }
} }
} }

View File

@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif #endif
if (path.targetHero->getOwner() != ai->playerID)
continue;
if(path.containsHero(hero)) if(path.containsHero(hero))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
continue; continue;
} }
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
if(ai->arePathHeroesLocked(path)) if(ai->arePathHeroesLocked(path))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
continue; continue;
} }
auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT
&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero if(!upgrader->garrisonHero
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
armyToGetOrBuy.addArmyToBuy(
ai->armyManager->toSlotInfo(
ai->armyManager->getArmyAvailableToBuy(
path.heroArmy,
upgrader,
ai->getFreeResources(),
path.turn())));
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
{ {
for(auto hero : cb->getAvailableHeroes(upgrader)) for(auto hero : cb->getAvailableHeroes(upgrader))
{ {
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader) auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->getFreeGold() >20000 && ai->getFreeGold() >20000

View File

@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
auto ourHeroes = ai->heroManager->getHeroRoles(); auto ourHeroes = ai->heroManager->getHeroRoles();
auto minScoreToHireMain = std::numeric_limits<float>::max(); auto minScoreToHireMain = std::numeric_limits<float>::max();
int currentArmyValue = 0;
for(auto hero : ourHeroes) for(auto hero : ourHeroes)
{ {
currentArmyValue += hero.first->getArmyCost();
if(hero.second != HeroRole::MAIN) if(hero.second != HeroRole::MAIN)
continue; continue;
@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
minScoreToHireMain = newScore; minScoreToHireMain = newScore;
} }
} }
// If we don't have any heros we might want to lower our expectations.
if (ourHeroes.empty())
minScoreToHireMain = 0;
const CGHeroInstance* bestHeroToHire = nullptr;
const CGTownInstance* bestTownToHireFrom = nullptr;
float bestScore = 0;
bool haveCapitol = false;
ai->dangerHitMap->updateHitMap();
int treasureSourcesCount = 0;
for(auto town : towns) for(auto town : towns)
{ {
uint8_t closestThreat = UINT8_MAX;
for (auto threat : ai->dangerHitMap->getTownThreats(town))
{
closestThreat = std::min(closestThreat, threat.turn);
}
//Don't hire a hero where there already is one present
if (town->visitingHero && town->garrisonHero)
continue;
float visitability = 0;
for (auto checkHero : ourHeroes)
{
if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
visitability++;
}
if(ai->heroManager->canRecruitHero(town)) if(ai->heroManager->canRecruitHero(town))
{ {
auto availableHeroes = ai->cb->getAvailableHeroes(town); auto availableHeroes = ai->cb->getAvailableHeroes(town);
for(auto hero : availableHeroes) for (auto obj : ai->objectClusterizer->getNearbyObjects())
{ {
auto score = ai->heroManager->evaluateHero(hero); if ((obj->ID == Obj::RESOURCE)
if(score > minScoreToHireMain)
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
break;
}
}
int treasureSourcesCount = 0;
for(auto obj : ai->objectClusterizer->getNearbyObjects())
{
if((obj->ID == Obj::RESOURCE)
|| obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE || obj->ID == Obj::CAMPFIRE
|| isWeeklyRevisitable(ai, obj) || isWeeklyRevisitable(ai, obj)
|| obj->ID ==Obj::ARTIFACT) || obj->ID == Obj::ARTIFACT)
{ {
auto tile = obj->visitablePos(); auto tile = obj->visitablePos();
auto closestTown = ai->dangerHitMap->getClosestTown(tile); auto closestTown = ai->dangerHitMap->getClosestTown(tile);
if(town == closestTown) if (town == closestTown)
treasureSourcesCount++; treasureSourcesCount++;
} }
} }
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000)) for(auto hero : availableHeroes)
continue;
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{ {
tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); auto score = ai->heroManager->evaluateHero(hero);
if(score > minScoreToHireMain)
{
score *= score / minScoreToHireMain;
}
score *= (hero->getArmyCost() + currentArmyValue);
if (hero->getFactionID() == town->getFactionID())
score *= 1.5;
if (vstd::isAlmostZero(visitability))
score *= 30 * town->getTownLevel();
else
score *= town->getTownLevel() / visitability;
if (score > bestScore)
{
bestScore = score;
bestHeroToHire = hero;
bestTownToHireFrom = town;
}
} }
} }
if (town->hasCapitol())
haveCapitol = true;
}
if (bestHeroToHire && bestTownToHireFrom)
{
if (ai->cb->getHeroesInfo().size() == 0
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
{
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
}
} }
return tasks; return tasks;

View File

@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
for(auto town : towns) for(auto town : towns)
{ {
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
continue;
ai->pathfinder->calculatePathInfo(paths, town->visitablePos()); ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
for(auto & path : paths) for(auto & path : paths)
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
if(town->visitingHero && town->visitingHero.get() != path.targetHero) if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue; continue;
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit()) if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
{ {
if(path.targetHero->mana == path.targetHero->manaLimit())
continue;
Composition stayAtTown; Composition stayAtTown;
stayAtTown.addNextSequence({ stayAtTown.addNextSequence({

View File

@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
{ {
objectDanger += evaluateDanger(hero->visitedTown.get()); objectDanger += evaluateDanger(hero->visitedTown.get());
} }
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
}
if (objWithID<Obj::TOWN>(dangerousObject))
{
auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
auto hero = town->garrisonHero;
if (hero)
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
} }
if(objectDanger) if(objectDanger)
@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
{ {
auto fortLevel = town->fortLevel(); auto fortLevel = town->fortLevel();
if(fortLevel == CGTownInstance::EFortLevel::CASTLE) if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger += 10000; danger = std::max(danger * 2, danger + 10000);
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL) else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
danger += 4000; danger = std::max(ui64(danger * 1.4), danger + 4000);
} }
return danger; return danger;

View File

@ -122,11 +122,14 @@ void TaskPlan::merge(TSubgoal task)
{ {
TGoalVec blockers; TGoalVec blockers;
if (task->asTask()->priority <= 0)
return;
for(auto & item : tasks) for(auto & item : tasks)
{ {
for(auto objid : item.affectedObjects) for(auto objid : item.affectedObjects)
{ {
if(task == item.task || task->asTask()->isObjectAffected(objid)) if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
{ {
if(item.task->asTask()->priority >= task->asTask()->priority) if(item.task->asTask()->priority >= task->asTask()->priority)
return; return;
@ -166,20 +169,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
return taskptr(*bestTask); return taskptr(*bestTask);
} }
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
{ {
TaskPlan taskPlan; TaskPlan taskPlan;
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r) tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
{ {
auto evaluator = this->priorityEvaluators->acquire(); auto evaluator = this->priorityEvaluators->acquire();
for(size_t i = r.begin(); i != r.end(); i++) for(size_t i = r.begin(); i != r.end(); i++)
{ {
auto task = tasks[i]; auto task = tasks[i];
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
if(task->asTask()->priority <= 0) task->asTask()->priority = evaluator->evaluate(task, priorityTier);
task->asTask()->priority = evaluator->evaluate(task);
} }
}); });
@ -326,7 +328,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
if(lockReason != HeroLockedReason::NOT_LOCKED) if(lockReason != HeroLockedReason::NOT_LOCKED)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString()); logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString());
#endif #endif
return true; return true;
} }
@ -347,12 +349,24 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker); boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10; const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState(); resetAiState();
Goals::TGoalVec bestTasks; Goals::TGoalVec bestTasks;
#if NKAI_TRACE_LEVEL >= 1
float totalHeroStrength = 0;
int totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++) for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{ {
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
@ -360,17 +374,21 @@ void Nullkiller::makeTurn()
Goals::TTask bestTask = taskptr(Goals::Invalid()); Goals::TTask bestTask = taskptr(Goals::Invalid());
for(;i <= settings->getMaxPass(); i++) while(true)
{ {
bestTasks.clear(); bestTasks.clear();
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(BuyArmyBehavior()), 1); decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
decompose(bestTasks, sptr(BuildingBehavior()), 1); decompose(bestTasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(bestTasks); bestTask = choseBestTask(bestTasks);
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY) if(bestTask->priority > 0)
{ {
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask)) if(!executeTask(bestTask))
return; return;
@ -382,7 +400,6 @@ void Nullkiller::makeTurn()
} }
} }
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1); decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@ -392,12 +409,24 @@ void Nullkiller::makeTurn()
if(!isOpenMap()) if(!isOpenMap())
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH); decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty()) TTaskVec selectedTasks;
#if NKAI_TRACE_LEVEL >= 1
int prioOfTask = 0;
#endif
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
{ {
decompose(bestTasks, sptr(StartupBehavior()), 1); #if NKAI_TRACE_LEVEL >= 1
prioOfTask = prio;
#endif
selectedTasks = buildPlan(bestTasks, prio);
if (!selectedTasks.empty() || settings->isUseFuzzy())
break;
} }
auto selectedTasks = buildPlan(bestTasks); std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b)
{
return a->priority > b->priority;
});
logAi->debug("Decision madel in %ld", timeElapsed(start)); logAi->debug("Decision madel in %ld", timeElapsed(start));
@ -438,7 +467,7 @@ void Nullkiller::makeTurn()
bestTask->priority); bestTask->priority);
} }
if(bestTask->priority < MIN_PRIORITY) if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
{ {
auto heroes = cb->getHeroesInfo(); auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@ -463,7 +492,9 @@ void Nullkiller::makeTurn()
continue; continue;
} }
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask)) if(!executeTask(bestTask))
{ {
if(hasAnySuccess) if(hasAnySuccess)
@ -471,13 +502,27 @@ void Nullkiller::makeTurn()
else else
return; return;
} }
hasAnySuccess = true; hasAnySuccess = true;
} }
hasAnySuccess |= handleTrading();
if(!hasAnySuccess) if(!hasAnySuccess)
{ {
logAi->trace("Nothing was done this turn. Ending turn."); logAi->trace("Nothing was done this turn. Ending turn.");
#if NKAI_TRACE_LEVEL >= 1
totalHeroStrength = 0;
totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
return; return;
} }
@ -554,4 +599,102 @@ void Nullkiller::lockResources(const TResources & res)
lockedResources += res; lockedResources += res;
} }
bool Nullkiller::handleTrading()
{
bool haveTraded = false;
bool shouldTryToTrade = true;
int marketId = -1;
for (auto town : cb->getTownsInfo())
{
if (town->hasBuiltSomeTradeBuilding())
{
marketId = town->id;
}
}
if (marketId == -1)
return false;
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
{
if (const auto* m = dynamic_cast<const IMarket*>(obj))
{
while (shouldTryToTrade)
{
shouldTryToTrade = false;
buildAnalyzer->update();
TResources required = buildAnalyzer->getTotalResourcesRequired();
TResources income = buildAnalyzer->getDailyIncome();
TResources available = cb->getResourceAmount();
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("Available %s", available.toString());
logAi->debug("Required %s", required.toString());
#endif
int mostWanted = -1;
int mostExpendable = -1;
float minRatio = std::numeric_limits<float>::max();
float maxRatio = std::numeric_limits<float>::min();
for (int i = 0; i < required.size(); ++i)
{
if (required[i] <= 0)
continue;
float ratio = static_cast<float>(available[i]) / required[i];
if (ratio < minRatio) {
minRatio = ratio;
mostWanted = i;
}
}
for (int i = 0; i < required.size(); ++i)
{
float ratio = available[i];
if (required[i] > 0)
ratio = static_cast<float>(available[i]) / required[i];
else
ratio = available[i];
bool okToSell = false;
if (i == GameResID::GOLD)
{
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
okToSell = true;
}
else
{
if (required[i] <= 0 && income[i] > 0)
okToSell = true;
}
if (ratio > maxRatio && okToSell) {
maxRatio = ratio;
mostExpendable = i;
}
}
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
#endif
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
return false;
int toGive;
int toGet;
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
//TODO trade only as much as needed
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
{
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
#endif
haveTraded = true;
shouldTryToTrade = true;
}
}
}
}
return haveTraded;
}
} }

View File

@ -120,13 +120,14 @@ public:
ScanDepth getScanDepth() const { return scanDepth; } ScanDepth getScanDepth() const { return scanDepth; }
bool isOpenMap() const { return openMap; } bool isOpenMap() const { return openMap; }
bool isObjectGraphAllowed() const { return useObjectGraph; } bool isObjectGraphAllowed() const { return useObjectGraph; }
bool handleTrading();
private: private:
void resetAiState(); void resetAiState();
void updateAiState(int pass, bool fast = false); void updateAiState(int pass, bool fast = false);
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const; void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const; Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const; Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
bool executeTask(Goals::TTask task); bool executeTask(Goals::TTask task);
bool areAffectedObjectsPresent(Goals::TTask task) const; bool areAffectedObjectsPresent(Goals::TTask task) const;
HeroRole getTaskRole(Goals::TTask task) const; HeroRole getTaskRole(Goals::TTask task) const;

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h" #include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h" #include "../../../lib/StartInfo.h"
@ -37,7 +39,7 @@ namespace NKAI
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f; const float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller * ai) EvaluationContext::EvaluationContext(const Nullkiller* ai)
: movementCost(0.0), : movementCost(0.0),
manaCost(0), manaCost(0),
danger(0), danger(0),
@ -51,9 +53,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
heroRole(HeroRole::SCOUT), heroRole(HeroRole::SCOUT),
turn(0), turn(0),
strategicalValue(0), strategicalValue(0),
conquestValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0), enemyHeroDangerRatio(0),
armyGrowth(0) threat(0),
armyGrowth(0),
armyInvolvement(0),
defenseValue(0),
isDefend(false),
threatTurns(INT_MAX),
involvesSailing(false),
isTradeBuilding(false),
isExchange(false),
isArmyUpgrade(false),
isHero(false),
isEnemy(false),
explorePriority(0)
{ {
} }
@ -225,7 +240,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
auto creature = creLevel.second.back().toCreature(); auto creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1; auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree) if(!creaturesAreFree)
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first; cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
} }
} }
@ -251,6 +266,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
switch(art->aClass) switch(art->aClass)
{ {
case CArtifact::EartClass::ART_TREASURE:
//FALL_THROUGH
case CArtifact::EartClass::ART_MINOR: case CArtifact::EartClass::ART_MINOR:
classValue = 1000; classValue = 1000;
break; break;
@ -289,6 +306,8 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::SPELL_SCROLL:
//FALL_THROUGH
case Obj::ARTIFACT: case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType()); return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
case Obj::HERO: case Obj::HERO:
@ -479,7 +498,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result; return result;
} }
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
{ {
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit())); return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
} }
@ -581,6 +600,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
return 0; return 0;
} }
float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
{
if (!target)
return 0;
if (target->getOwner() == ai->playerID)
return 0;
switch (target->ID)
{
case Obj::TOWN:
{
if (ai->buildAnalyzer->getDevelopmentInfo().empty())
return 10.0f;
auto town = dynamic_cast<const CGTownInstance*>(target);
if (town->getOwner() == ai->playerID)
{
auto armyIncome = townArmyGrowth(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
}
auto fortLevel = town->fortLevel();
auto booster = 1.0f;
if (town->hasCapitol())
return booster * 1.5;
if (fortLevel < CGTownInstance::CITADEL)
return booster * (town->hasFort() ? 1.0 : 0.8);
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
}
case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
: 0;
case Obj::KEYMASTER:
return 0.6f;
default:
return 0;
}
}
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
{ {
auto rewardable = dynamic_cast<const CRewardableObject *>(hut); auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@ -705,7 +772,7 @@ int32_t getArmyCost(const CArmedInstance * army)
for(auto stack : army->Slots()) for(auto stack : army->Slots())
{ {
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count; value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
} }
return value; return value;
@ -786,7 +853,9 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
evaluationContext.isExchange = true;
} }
}; };
@ -804,6 +873,7 @@ public:
evaluationContext.armyReward += upgradeValue; evaluationContext.armyReward += upgradeValue;
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
evaluationContext.isArmyUpgrade = true;
} }
}; };
@ -818,22 +888,46 @@ public:
int tilesDiscovered = task->value; int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
{
switch (obj->ID.num)
{
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
evaluationContext.explorePriority = 1;
break;
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
evaluationContext.explorePriority = 2;
break;
}
}
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
evaluationContext.explorePriority = 1;
if (evaluationContext.explorePriority == 0)
evaluationContext.explorePriority = 3;
} }
}; };
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{ {
public: public:
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
{ {
if(task->goalType != Goals::STAY_AT_TOWN) if (task->goalType != Goals::STAY_AT_TOWN)
return; return;
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task); Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); if (evaluationContext.armyReward == 0)
evaluationContext.movementCost += stayAtTown.getMovementWasted(); evaluationContext.isDefend = true;
else
{
evaluationContext.movementCost += stayAtTown.getMovementWasted();
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
}
} }
}; };
@ -844,15 +938,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger) if(enemyDanger.danger)
{ {
auto dangerRatio = enemyDanger.danger / (double)ourStrength; auto dangerRatio = enemyDanger.danger / (double)ourStrength;
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
if(isAI)
{
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
}
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
} }
} }
@ -896,6 +983,10 @@ public:
else else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
evaluationContext.defenseValue = town->fortLevel();
evaluationContext.isDefend = true;
evaluationContext.threatTurns = treat.turn;
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
} }
@ -926,6 +1017,8 @@ public:
for(auto & node : path.nodes) for(auto & node : path.nodes)
{ {
vstd::amax(costsPerHero[node.targetHero], node.cost); vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
} }
for(auto pair : costsPerHero) for(auto pair : costsPerHero)
@ -952,10 +1045,18 @@ public:
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
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)
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)
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
} }
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); 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());
vstd::amax(evaluationContext.turn, path.turn()); vstd::amax(evaluationContext.turn, path.turn());
} }
@ -996,6 +1097,7 @@ public:
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -1021,6 +1123,14 @@ public:
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task); Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
for (auto obj : swapCommand.getAffectedObjects())
{
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
}
if (garrisonHero)
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{ {
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@ -1029,6 +1139,9 @@ public:
evaluationContext.movementCost += mpLeft; evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft;
evaluationContext.heroRole = defenderRole; evaluationContext.heroRole = defenderRole;
evaluationContext.isDefend = true;
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
} }
} }
}; };
@ -1072,8 +1185,14 @@ public:
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD]; int32_t cost = bi.buildCost[EGameResID::GOLD];
evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1; evaluationContext.closestWayRatio = 1;
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
evaluationContext.isTradeBuilding = true;
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
if(bi.creatureID != CreatureID::NONE) if(bi.creatureID != CreatureID::NONE)
{ {
@ -1100,7 +1219,18 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{ {
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
{
evaluationContext.armyInvolvement += hero->getArmyCost();
}
} }
int sameTownBonus = 0;
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
{
if (buildThis.town->getFaction() == town->getFaction())
sameTownBonus += town->getTownLevel();
}
evaluationContext.armyReward *= sameTownBonus;
if(evaluationContext.goldReward) if(evaluationContext.goldReward)
{ {
@ -1162,6 +1292,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
for(auto subgoal : parts) for(auto subgoal : parts)
{ {
context.goldCost += subgoal->goldCost; context.goldCost += subgoal->goldCost;
context.buildingCost += subgoal->buildingCost;
for(auto builder : evaluationContextBuilders) for(auto builder : evaluationContextBuilders)
{ {
@ -1172,7 +1303,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
return context; return context;
} }
float PriorityEvaluator::evaluate(Goals::TSubgoal task) float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{ {
auto evaluationContext = buildEvaluationContext(task); auto evaluationContext = buildEvaluationContext(task);
@ -1185,36 +1316,256 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
double result = 0; double result = 0;
try if (ai->settings->isUseFuzzy())
{ {
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); float fuzzyResult = 0;
heroRoleVariable->setValue(evaluationContext.heroRole); try
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); {
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
goldRewardVariable->setValue(goldRewardPerTurn); heroRoleVariable->setValue(evaluationContext.heroRole);
armyRewardVariable->setValue(evaluationContext.armyReward); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
armyGrowthVariable->setValue(evaluationContext.armyGrowth); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
skillRewardVariable->setValue(evaluationContext.skillReward); goldRewardVariable->setValue(goldRewardPerTurn);
dangerVariable->setValue(evaluationContext.danger); armyRewardVariable->setValue(evaluationContext.armyReward);
rewardTypeVariable->setValue(rewardType); armyGrowthVariable->setValue(evaluationContext.armyGrowth);
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); skillRewardVariable->setValue(evaluationContext.skillReward);
strategicalValueVariable->setValue(evaluationContext.strategicalValue); dangerVariable->setValue(evaluationContext.danger);
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); rewardTypeVariable->setValue(rewardType);
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
turnVariable->setValue(evaluationContext.turn); strategicalValueVariable->setValue(evaluationContext.strategicalValue);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
turnVariable->setValue(evaluationContext.turn);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
engine->process(); engine->process();
result = value->getValue(); fuzzyResult = value->getValue();
}
catch (fl::Exception& fe)
{
logAi->error("evaluate VisitTile: %s", fe.getWhat());
}
result = fuzzyResult;
} }
catch(fl::Exception & fe) else
{ {
logAi->error("evaluate VisitTile: %s", fe.getWhat()); float score = 0;
float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25;
bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
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",
priorityTier,
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT],
goldRewardPerTurn,
evaluationContext.goldCost,
evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
evaluationContext.explorePriority,
evaluationContext.isDefend);
#endif
switch (priorityTier)
{
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
{
if (evaluationContext.turn > 0)
return 0;
if(evaluationContext.conquestValue > 0)
score = 1000;
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;
}
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
{
if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
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
{
if (evaluationContext.turn > 0 && evaluationContext.isHero)
return 0;
if (arriveNextWeek && evaluationContext.isEnemy)
return 0;
if (evaluationContext.conquestValue > 0)
score = 1000;
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;
}
case PriorityTier::UPGRADE:
{
if (!evaluationContext.isArmyUpgrade)
return 0;
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HIGH_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
return 0;
if (evaluationContext.buildingCost.marketValue() > 0)
return 0;
if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
return 0;
if (evaluationContext.explorePriority == 3)
return 0;
if (evaluationContext.isArmyUpgrade)
return 0;
if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
score -= evaluationContext.goldCost;
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
if (score > 0)
{
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
}
break;
}
case PriorityTier::LOW_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 3)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score = 1000;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
{
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
return 0;
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
score = 1000;
score *= evaluationContext.closestWayRatio;
score /= (evaluationContext.turn + 1);
break;
}
case PriorityTier::BUILDINGS: //For buildings and buying army
{
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
//If we already have locked resources, we don't look at other buildings
if (ai->getLockedResources().marketValue() > 0)
return 0;
score += evaluationContext.conquestValue * 1000;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
if (evaluationContext.buildingCost.marketValue() > 0)
{
if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
{
logAi->trace("Should make sure to build market-place instead of %s", task->toString());
for (auto town : ai->cb->getTownsInfo())
{
if (!town->hasBuiltSomeTradeBuilding())
return 0;
}
}
score += 1000;
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
auto income = ai->buildAnalyzer->getDailyIncome();
if(ai->buildAnalyzer->isGoldPressureHigh())
score /= evaluationContext.buildingCost.marketValue();
if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
{
TResources needed = evaluationContext.buildingCost - resourcesAvailable;
needed.positive();
int turnsTo = needed.maxPurchasableCount(income);
if (turnsTo == INT_MAX)
return 0;
else
score /= turnsTo;
}
}
else
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
return 0;
}
break;
}
}
result = score;
//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
if (std::isnan(result))
return 0;
} }
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", 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",
priorityTier,
task->toString(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
@ -1223,9 +1574,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
goldRewardPerTurn, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger, evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue, evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio, evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
result); result);

View File

@ -41,6 +41,7 @@ public:
float getResourceRequirementStrength(int resType) const; float getResourceRequirementStrength(int resType) const;
float getResourceRequirementStrength(const TResources & res) const; float getResourceRequirementStrength(const TResources & res) const;
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const; float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
float getConquestValue(const CGObjectInstance* target) const;
float getTotalResourceRequirementStrength(int resType) const; float getTotalResourceRequirementStrength(int resType) const;
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
@ -48,7 +49,7 @@ public:
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) const; uint64_t townArmyGrowth(const CGTownInstance * town) const;
uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
}; };
struct DLL_EXPORT EvaluationContext struct DLL_EXPORT EvaluationContext
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
int32_t goldCost; int32_t goldCost;
float skillReward; float skillReward;
float strategicalValue; float strategicalValue;
float conquestValue;
HeroRole heroRole; HeroRole heroRole;
uint8_t turn; uint8_t turn;
RewardEvaluator evaluator; RewardEvaluator evaluator;
float enemyHeroDangerRatio; float enemyHeroDangerRatio;
float threat;
float armyInvolvement;
int defenseValue;
bool isDefend;
int threatTurns;
TResources buildingCost;
bool involvesSailing;
bool isTradeBuilding;
bool isExchange;
bool isArmyUpgrade;
bool isHero;
bool isEnemy;
int explorePriority;
EvaluationContext(const Nullkiller * ai); EvaluationContext(const Nullkiller * ai);
@ -91,7 +106,20 @@ public:
~PriorityEvaluator(); ~PriorityEvaluator();
void initVisitTile(); void initVisitTile();
float evaluate(Goals::TSubgoal task); float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
enum PriorityTier : int32_t
{
BUILDINGS = 0,
INSTAKILL,
INSTADEFEND,
KILL,
UPGRADE,
HIGH_PRIO_EXPLORE,
HUNTER_GATHER,
LOW_PRIO_EXPLORE,
DEFEND
};
private: private:
const Nullkiller * ai; const Nullkiller * ai;

View File

@ -30,7 +30,8 @@ namespace NKAI
maxpass(10), maxpass(10),
allowObjectGraph(true), allowObjectGraph(true),
useTroopsFromGarrisons(false), useTroopsFromGarrisons(false),
openMap(true) openMap(true),
useFuzzy(false)
{ {
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings"); JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
@ -69,6 +70,11 @@ namespace NKAI
openMap = node.Struct()["openMap"].Bool(); openMap = node.Struct()["openMap"].Bool();
} }
if (!node.Struct()["useFuzzy"].isNull())
{
useFuzzy = node.Struct()["useFuzzy"].Bool();
}
if(!node.Struct()["useTroopsFromGarrisons"].isNull()) if(!node.Struct()["useTroopsFromGarrisons"].isNull())
{ {
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool(); useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();

View File

@ -29,6 +29,7 @@ namespace NKAI
bool allowObjectGraph; bool allowObjectGraph;
bool useTroopsFromGarrisons; bool useTroopsFromGarrisons;
bool openMap; bool openMap;
bool useFuzzy;
public: public:
Settings(); Settings();
@ -41,5 +42,6 @@ namespace NKAI
bool isObjectGraphAllowed() const { return allowObjectGraph; } bool isObjectGraphAllowed() const { return allowObjectGraph; }
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; } bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
bool isOpenMap() const { return openMap; } bool isOpenMap() const { return openMap; }
bool isUseFuzzy() const { return useFuzzy; }
}; };
} }

View File

@ -104,6 +104,7 @@ namespace Goals
bool isAbstract; SETTER(bool, isAbstract) bool isAbstract; SETTER(bool, isAbstract)
int value; SETTER(int, value) int value; SETTER(int, value)
ui64 goldCost; SETTER(ui64, goldCost) ui64 goldCost; SETTER(ui64, goldCost)
TResources buildingCost; SETTER(TResources, buildingCost)
int resID; SETTER(int, resID) int resID; SETTER(int, resID)
int objid; SETTER(int, objid) int objid; SETTER(int, objid)
int aid; SETTER(int, aid) int aid; SETTER(int, aid)

View File

@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated()); throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
} }
if (hero->inTownGarrison)
ai->myCb->swapGarrisonHero(hero->visitedTown);
auto wait = cb->waitTillRealize; auto wait = cb->waitTillRealize;
cb->waitTillRealize = true; cb->waitTillRealize = true;

View File

@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
if(!town->garrisonHero) if(!town->garrisonHero)
{ {
while(upperArmy->stacksCount() != 0) if (!garrisonHero->canBeMergedWith(*town))
{ {
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first); while (upperArmy->stacksCount() != 0)
{
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
}
} }
} }

View File

@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
{ {
hero = path.targetHero; hero = path.targetHero;
tile = path.targetTile(); tile = path.targetTile();
closestWayRatio = 1;
if(obj) if(obj)
{ {
@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid); ai->nullkiller->setTargetObject(objid);
ai->nullkiller->objectClusterizer->reset();
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false); auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);

View File

@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex); std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
ai->nullkiller->heroManager->update(); ai->nullkiller->heroManager->update();
ai->nullkiller->objectClusterizer->reset();
} }
} }

View File

@ -44,6 +44,7 @@ namespace Goals
} }
std::string toString() const override; std::string toString() const override;
const CGHeroInstance* getHero() const override { return heroToBuy; }
void accept(AIGateway * ai) override; void accept(AIGateway * ai) override;
}; };
} }

View File

@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
{ {
return "Stay at town " + town->getNameTranslated() return "Stay at town " + town->getNameTranslated()
+ " hero " + hero->getNameTranslated() + " hero " + hero->getNameTranslated()
+ ", mana: " + std::to_string(hero->mana); + ", mana: " + std::to_string(hero->mana)
+ " / " + std::to_string(hero->manaLimit());
} }
void StayAtTown::accept(AIGateway * ai) void StayAtTown::accept(AIGateway * ai)
{ {
if(hero->visitedTown != town)
{
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
}
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE); ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
} }

View File

@ -719,6 +719,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(node->action == EPathNodeAction::BATTLE if(node->action == EPathNodeAction::BATTLE
|| node->action == EPathNodeAction::TELEPORT_BATTLE || node->action == EPathNodeAction::TELEPORT_BATTLE
|| node->action == EPathNodeAction::TELEPORT_NORMAL || node->action == EPathNodeAction::TELEPORT_NORMAL
|| node->action == EPathNodeAction::DISEMBARK
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT) || node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
{ {
continue; continue;
@ -961,7 +962,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
// do not allow our own heroes in garrison to act on map // do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID if(hero.first->getOwner() == ai->playerID
&& hero.first->inTownGarrison && hero.first->inTownGarrison
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
{ {
continue; continue;
} }
@ -1196,6 +1197,11 @@ void AINodeStorage::calculateTownPortal(
continue; continue;
} }
if (targetTown->visitingHero
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|| targetTown->getUpperArmy()->stacksCount()))
continue;
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
if(nodeOptional) if(nodeOptional)
@ -1418,6 +1424,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.heroArmy = node.actor->creatureSet; path.heroArmy = node.actor->creatureSet;
path.armyLoss = node.armyLoss; path.armyLoss = node.armyLoss;
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle); path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
for (auto pathNode : path.nodes)
{
path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
}
if(path.targetObjectDanger > 0) if(path.targetObjectDanger > 0)
{ {
@ -1564,7 +1574,7 @@ uint8_t AIPath::turn() const
uint64_t AIPath::getHeroStrength() const uint64_t AIPath::getHeroStrength() const
{ {
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
} }
uint64_t AIPath::getTotalDanger() const uint64_t AIPath::getTotalDanger() const

View File

@ -29,8 +29,8 @@ namespace NKAI
{ {
namespace AIPathfinding namespace AIPathfinding
{ {
const int BUCKET_COUNT = 3; const int BUCKET_COUNT = 1;
const int BUCKET_SIZE = 7; const int BUCKET_SIZE = 32;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE; const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int CHAIN_MAX_DEPTH = 4; const int CHAIN_MAX_DEPTH = 4;
} }

View File

@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
initialMovement = hero->movementPointsRemaining(); initialMovement = hero->movementPointsRemaining();
initialTurn = 0; initialTurn = 0;
armyValue = getHeroArmyStrengthWithCommander(hero, hero); armyValue = getHeroArmyStrengthWithCommander(hero, hero);
heroFightingStrength = hero->getFightingStrength(); heroFightingStrength = hero->getHeroStrength();
tiCache.reset(new TurnInfo(hero)); tiCache.reset(new TurnInfo(hero));
} }

View File

@ -1112,6 +1112,6 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr<ElementInfo> info, bool
labelName->alignment = ETextAlignment::CENTER; labelName->alignment = ETextAlignment::CENTER;
labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y)); labelName->moveTo(Point(pos.x + LABEL_POS_X, labelName->pos.y));
} }
labelName->setText(info->name); labelName->setText(info->getNameForList());
labelName->setColor(color); labelName->setColor(color);
} }

View File

@ -6,5 +6,5 @@
"maxGoldPressure" : 0.3, "maxGoldPressure" : 0.3,
"useTroopsFromGarrisons" : true, "useTroopsFromGarrisons" : true,
"openMap": true, "openMap": true,
"allowObjectGraph": true "allowObjectGraph": false
} }

View File

@ -361,6 +361,14 @@ ui64 CCreatureSet::getArmyStrength() const
return ret; return ret;
} }
ui64 CCreatureSet::getArmyCost() const
{
ui64 ret = 0;
for (const auto& elem : stacks)
ret += elem.second->getMarketValue();
return ret;
}
ui64 CCreatureSet::getPower(const SlotID & slot) const ui64 CCreatureSet::getPower(const SlotID & slot) const
{ {
return getStack(slot).getPower(); return getStack(slot).getPower();
@ -853,6 +861,12 @@ ui64 CStackInstance::getPower() const
return static_cast<ui64>(getType()->getAIValue()) * count; return static_cast<ui64>(getType()->getAIValue()) * count;
} }
ui64 CStackInstance::getMarketValue() const
{
assert(getType());
return getType()->getFullRecruitCost().marketValue() * count;
}
ArtBearer::ArtBearer CStackInstance::bearerType() const ArtBearer::ArtBearer CStackInstance::bearerType() const
{ {
return ArtBearer::CREATURE; return ArtBearer::CREATURE;

View File

@ -107,6 +107,8 @@ public:
FactionID getFactionID() const override; FactionID getFactionID() const override;
virtual ui64 getPower() const; virtual ui64 getPower() const;
/// Returns total market value of resources needed to recruit this unit
virtual ui64 getMarketValue() const;
CCreature::CreatureQuantityId getQuantityID() const; CCreature::CreatureQuantityId getQuantityID() const;
std::string getQuantityTXT(bool capitalized = true) const; std::string getQuantityTXT(bool capitalized = true) const;
virtual int getExpRank() const; virtual int getExpRank() const;
@ -272,6 +274,7 @@ public:
int stacksCount() const; int stacksCount() const;
virtual bool needsLastStack() const; //true if last stack cannot be taken virtual bool needsLastStack() const; //true if last stack cannot be taken
ui64 getArmyStrength() const; //sum of AI values of creatures ui64 getArmyStrength() const; //sum of AI values of creatures
ui64 getArmyCost() const; //sum of cost of creatures
ui64 getPower(const SlotID & slot) const; //value of specific stack ui64 getPower(const SlotID & slot) const; //value of specific stack
std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack
std::string getArmyDescription() const; std::string getArmyDescription() const;

View File

@ -148,6 +148,26 @@ public:
return ret; return ret;
} }
//Returns how many items of "this" we can afford with provided funds
int maxPurchasableCount(const ResourceSet& availableFunds) {
int ret = 0; // Initialize to 0 because we want the maximum number of accumulations
for (size_t i = 0; i < container.size(); ++i) {
if (container.at(i) > 0) { // We only care about fulfilling positive needs
if (availableFunds[i] == 0) {
// If income is 0 and we need a positive amount, it's impossible to fulfill
return INT_MAX;
}
else {
// Calculate the number of times we need to accumulate income to fulfill the need
int ceiledResult = vstd::divideAndCeil(container.at(i), availableFunds[i]);
ret = std::max(ret, ceiledResult);
}
}
}
return ret;
}
ResourceSet & operator=(const TResource &rhs) ResourceSet & operator=(const TResource &rhs)
{ {
for(int & i : container) for(int & i : container)
@ -169,17 +189,6 @@ public:
return this->container == rhs.container; return this->container == rhs.container;
} }
// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i]
// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a
// bool operator<(const ResourceSet &rhs)
// {
// for(int i = 0; i < size(); i++)
// if(at(i) >= rhs[i])
// return false;
//
// return true;
// }
template <typename Handler> void serialize(Handler &h) template <typename Handler> void serialize(Handler &h)
{ {
h & container; h & container;

View File

@ -339,7 +339,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector<CGHeroInstance *> heroe
{ {
range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b) range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b)
{ {
return a->getHeroStrength() > b->getHeroStrength(); return a->getHeroStrengthForCampaign() > b->getHeroStrengthForCampaign();
}); });
logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated());

View File

@ -711,7 +711,25 @@ double CGHeroInstance::getFightingStrength() const
double CGHeroInstance::getMagicStrength() const double CGHeroInstance::getMagicStrength() const
{ {
return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER))); if (!hasSpellbook())
return 1;
bool atLeastOneCombatSpell = false;
for (auto spell : spells)
{
if (spellbookContainsSpell(spell) && spell.toSpell()->isCombat())
{
atLeastOneCombatSpell = true;
break;
}
}
if (!atLeastOneCombatSpell)
return 1;
return sqrt((1.0 + 0.05*getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * mana / manaLimit()) * (1.0 + 0.05*getPrimSkillLevel(PrimarySkill::SPELL_POWER) * mana / manaLimit()));
}
double CGHeroInstance::getMagicStrengthForCampaign() const
{
return sqrt((1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::KNOWLEDGE)) * (1.0 + 0.05 * getPrimSkillLevel(PrimarySkill::SPELL_POWER)));
} }
double CGHeroInstance::getHeroStrength() const double CGHeroInstance::getHeroStrength() const
@ -719,9 +737,14 @@ double CGHeroInstance::getHeroStrength() const
return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0)); return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrength(), 2.0));
} }
double CGHeroInstance::getHeroStrengthForCampaign() const
{
return sqrt(pow(getFightingStrength(), 2.0) * pow(getMagicStrengthForCampaign(), 2.0));
}
ui64 CGHeroInstance::getTotalStrength() const ui64 CGHeroInstance::getTotalStrength() const
{ {
double ret = getFightingStrength() * getArmyStrength(); double ret = getHeroStrength() * getArmyStrength();
return static_cast<ui64>(ret); return static_cast<ui64>(ret);
} }
@ -1896,5 +1919,11 @@ const IOwnableObject * CGHeroInstance::asOwnable() const
return this; return this;
} }
int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const
{
std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast<int>(which));
auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
return valOfBonuses(selector, cachingStr);
}
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END

View File

@ -223,10 +223,13 @@ public:
int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const; int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const;
double getFightingStrength() const; // takes attack / defense skill into account double getFightingStrength() const; // takes attack / defense skill into account
double getMagicStrength() const; // takes knowledge / spell power skill into account double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account
double getMagicStrengthForCampaign() const; // takes knowledge / spell power skill into account
double getHeroStrength() const; // includes fighting and magic strength double getHeroStrength() const; // includes fighting and magic strength
double getHeroStrengthForCampaign() const; // includes fighting and the for-campaign-version of magic strength
ui64 getTotalStrength() const; // includes fighting strength and army strength ui64 getTotalStrength() const; // includes fighting strength and army strength
TExpType calculateXp(TExpType exp) const; //apply learning skill TExpType calculateXp(TExpType exp) const; //apply learning skill
int getBasePrimarySkillValue(PrimarySkill which) const; //the value of a base-skill without items or temporary bonuses
CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const; CStackBasicDescriptor calculateNecromancy (const BattleResult &battleResult) const;
void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const; void showNecromancyDialog(const CStackBasicDescriptor &raisedStack, vstd::RNG & rand) const;

View File

@ -886,7 +886,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveme
return complainRet("Cannot disembark hero, tile is blocked!"); return complainRet("Cannot disembark hero, tile is blocked!");
if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD) if(distance(h->pos, dst) >= 1.5 && movementMode == EMovementMode::STANDARD)
return complainRet("Tiles are not neighboring!"); return complainRet("Tiles " + h->pos.toString()+ " and "+ dst.toString() +" are not neighboring!");
if(h->inTownGarrison) if(h->inTownGarrison)
return complainRet("Can not move garrisoned hero!"); return complainRet("Can not move garrisoned hero!");

View File

@ -237,6 +237,52 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne
for (auto obj : state.getOwnedObjects()) for (auto obj : state.getOwnedObjects())
incomeHandicapped += obj->asOwnable()->dailyIncome(); incomeHandicapped += obj->asOwnable()->dailyIncome();
if (!state.isHuman())
{
// Initialize bonuses for different resources
std::array<int, GameResID::COUNT> weeklyBonuses = {};
// Calculate weekly bonuses based on difficulty
if (gameHandler->gameState()->getStartInfo()->difficulty == 0)
{
weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * (0.75 - 1) * 7));
}
else if (gameHandler->gameState()->getStartInfo()->difficulty == 3)
{
weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.25 * 7));
weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.39 * 7));
weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.39 * 7));
weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.14 * 7));
weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.14 * 7));
weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.14 * 7));
weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.14 * 7));
}
else if (gameHandler->gameState()->getStartInfo()->difficulty == 4)
{
weeklyBonuses[EGameResID::GOLD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GOLD] * 0.5 * 7));
weeklyBonuses[EGameResID::WOOD] = static_cast<int>(std::round(incomeHandicapped[EGameResID::WOOD] * 0.53 * 7));
weeklyBonuses[EGameResID::ORE] = static_cast<int>(std::round(incomeHandicapped[EGameResID::ORE] * 0.53 * 7));
weeklyBonuses[EGameResID::MERCURY] = static_cast<int>(std::round(incomeHandicapped[EGameResID::MERCURY] * 0.28 * 7));
weeklyBonuses[EGameResID::CRYSTAL] = static_cast<int>(std::round(incomeHandicapped[EGameResID::CRYSTAL] * 0.28 * 7));
weeklyBonuses[EGameResID::SULFUR] = static_cast<int>(std::round(incomeHandicapped[EGameResID::SULFUR] * 0.28 * 7));
weeklyBonuses[EGameResID::GEMS] = static_cast<int>(std::round(incomeHandicapped[EGameResID::GEMS] * 0.28 * 7));
}
// Distribute weekly bonuses over 7 days, depending on the current day of the week
for (int i = 0; i < GameResID::COUNT; ++i)
{
int dailyBonus = weeklyBonuses[i] / 7;
int remainderBonus = weeklyBonuses[i] % 7;
// Apply the daily bonus for each day, and distribute the remainder accordingly
incomeHandicapped[static_cast<GameResID>(i)] += dailyBonus;
if (gameHandler->gameState()->getDate(Date::DAY_OF_WEEK) - 1 < remainderBonus)
{
incomeHandicapped[static_cast<GameResID>(i)] += 1;
}
}
}
return incomeHandicapped; return incomeHandicapped;
} }

@ -1 +1 @@
Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59 Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1