diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index d3c4ba13c..b3986de94 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -863,7 +863,7 @@ std::optional CBattleAI::considerFleeingOrSurrendering() bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); - if(!bs.canFlee || !bs.canSurrender) + if(!bs.canFlee && !bs.canSurrender) { return std::nullopt; } diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 16cd34849..8be0aa310 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -29,7 +29,7 @@ namespace NKAI { // our to enemy strength ratio constants -const float SAFE_ATTACK_CONSTANT = 1.2f; +const float SAFE_ATTACK_CONSTANT = 1.1f; const float RETREAT_THRESHOLD = 0.3f; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; @@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose) LOG_TRACE(logAi); NET_EVENT_HANDLER; - validateObject(details.id); //enemy hero may have left visible area auto hero = cb->getHero(details.id); + if(!hero) + validateObject(details.id); //enemy hero may have left visible area + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); @@ -777,28 +779,21 @@ void AIGateway::makeTurn() boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); + cb->sendMessage("vcmieagles"); + + retrieveVisitableObjs(); + if(cb->getDate(Date::DAY_OF_WEEK) == 1) { - std::vector objs; - retrieveVisitableObjs(objs, true); - - for(const CGObjectInstance * obj : objs) + for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs) { if(isWeeklyRevisitable(obj)) { - addVisitableObj(obj); nullkiller->memory->markObjectUnvisited(obj); } } } - cb->sendMessage("vcmieagles"); - - if(cb->getDate(Date::DAY) == 1) - { - retrieveVisitableObjs(); - } - #if NKAI_TRACE_LEVEL == 0 try { @@ -872,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); + for(auto army : armies) + { + // move first stack at first slot if empty to avoid can not take away last creature + if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0) + { + cb->mergeOrSwapStacks( + army, + army, + SlotID(0), + army->Slots().begin()->first); + } + } + //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot { @@ -1059,6 +1067,11 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re int count = d->creatures[i].first; CreatureID creID = d->creatures[i].second.back(); + if(!recruiter->getSlotFor(creID).validSlot()) + { + continue; + } + vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); @@ -1102,26 +1115,13 @@ void AIGateway::waitTillFree() status.waitTillFree(); } -void AIGateway::retrieveVisitableObjs(std::vector & out, bool includeOwned) const -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(includeOwned || obj->tempOwner != playerID) - out.push_back(obj); - } - }); -} - void AIGateway::retrieveVisitableObjs() { foreach_tile_pos([&](const int3 & pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { - if(obj->tempOwner != playerID) - addVisitableObj(obj); + addVisitableObj(obj); } }); } diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index cab6ce797..3a9c23b31 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -195,7 +195,6 @@ public: void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it - void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 296c47780..e6962673e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) if(dynamic_cast(obj)) return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; switch(obj->ID) { - case Obj::STABLES: - case Obj::MAGIC_WELL: case Obj::HILL_FORT: return true; case Obj::BORDER_GATE: diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 71ce9630f..37dfb7a76 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../../../CCallback.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/GameConstants.h" namespace NKAI { @@ -33,6 +34,45 @@ public: } }; +void ArmyUpgradeInfo::addArmyToBuy(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + upgradeCost += slot.creature->getFullRecruitCost() * slot.count; + } +} + +void ArmyUpgradeInfo::addArmyToGet(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + } +} + +std::vector ArmyManager::toSlotInfo(std::vector army) const +{ + std::vector result; + + for(auto i : army) + { + SlotInfo slot; + + slot.creature = VLC->creh->objects[i.cre->getId()]; + slot.count = i.count; + slot.power = evaluateStackPower(i.cre, i.count); + + result.push_back(slot); + } + + return result; +} + uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const { return howManyReinforcementsCanGet(hero, hero, source); @@ -136,7 +176,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, { if(vstd::contains(allowedFactions, slot.creature->getFaction())) { - auto slotID = newArmyInstance.getSlotFor(slot.creature); + auto slotID = newArmyInstance.getSlotFor(slot.creature->getId()); if(slotID.validSlot()) { @@ -238,7 +278,8 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( ui64 ArmyManager::howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const + const TResources & availableResources, + uint8_t turn) const { ui64 aivalue = 0; auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources); @@ -259,17 +300,29 @@ std::vector ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her std::vector ArmyManager::getArmyAvailableToBuy( const CCreatureSet * hero, const CGDwelling * dwelling, - TResources availableRes) const + TResources availableRes, + uint8_t turn) const { std::vector creaturesInDwellings; int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); + bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7; + + const CGTownInstance * town = dwelling->ID == Obj::TOWN + ? dynamic_cast(dwelling) + : nullptr; for(int i = dwelling->creatures.size() - 1; i >= 0; i--) { auto ci = infoFromDC(dwelling->creatures[i]); - if(!ci.count || ci.creID == -1) - continue; + if(ci.creID == -1) continue; + + if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) + { + ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth(); + } + + if(!ci.count) continue; SlotID dst = hero->getSlotFor(ci.creID); if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack @@ -282,8 +335,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford - if(!ci.count) - continue; + if(!ci.count) continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); @@ -307,7 +359,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, return newArmy > oldArmy ? newArmy - oldArmy : 0; } -uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const +uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const { return creature->getAIValue() * count; } diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index b6f27adf9..1617bd1bd 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -34,6 +34,9 @@ struct ArmyUpgradeInfo std::vector resultingArmy; uint64_t upgradeValue = 0; TResources upgradeCost; + + void addArmyToBuy(std::vector army); + void addArmyToGet(std::vector army); }; class DLL_EXPORT IArmyManager //: public: IAbstractManager @@ -45,20 +48,33 @@ public: virtual ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const = 0; + const TResources & availableResources, + uint8_t turn = 0) const = 0; + virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0; - virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; + virtual ui64 howManyReinforcementsCanGet( + const IBonusBearer * armyCarrier, + const CCreatureSet * target, + const CCreatureSet * source) const = 0; + virtual std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector::iterator getWeakestCreature(std::vector & army) const = 0; virtual std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0; - virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const = 0; - virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; + virtual std::vector toSlotInfo(std::vector creatures) const = 0; + + virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; + virtual std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const = 0; + + virtual uint64_t evaluateStackPower(const Creature * creature, int count) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, const CGObjectInstance * upgrader, const TResources & availableResources) const = 0; - virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; virtual std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0; }; @@ -74,20 +90,30 @@ private: public: ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} void update() override; + ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const override; + const TResources & availableResources, + uint8_t turn = 0) const override; + ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override; ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector::iterator getWeakestCreature(std::vector & army) const override; std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override; - std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const override; + std::vector toSlotInfo(std::vector creatures) const override; + std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; + std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const override; + std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; - uint64_t evaluateStackPower(const CCreature * creature, int count) const override; + uint64_t evaluateStackPower(const Creature * creature, int count) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 8318718c8..359ccc1ca 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) logAi->trace("Checking other buildings"); std::vector> otherBuildings = { - {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} + {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}, + {BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5} }; if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); + otherBuildings.push_back({BuildingID::HORDE_1}); + otherBuildings.push_back({BuildingID::HORDE_2}); } for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) { - if(!developmentInfo.town->hasBuilt(buildingID)) + if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID)) { developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); @@ -163,8 +166,8 @@ void BuildAnalyzer::update() } else { - goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f + + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); } logAi->trace("Gold preasure: %f", goldPreasure); @@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( const CCreature * creature = nullptr; CreatureID baseCreatureID; + int creatureLevel = -1; + int creatureUpgrade = 0; + if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) { - int level = toBuild - BuildingID::DWELL_FIRST; - auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN); - auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN - ? creatures.at(level / GameConstants::CREATURES_PER_TOWN) + creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; + creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + } + else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(0); + } + else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(1); + } + + if(creatureLevel >= 0) + { + auto creatures = townInfo->creatures.at(creatureLevel); + auto creatureID = creatures.size() > creatureUpgrade + ? creatures.at(creatureUpgrade) : creatures.front(); baseCreatureID = creatures.front(); @@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo( } else { - creatureGrows = creature->getGrowth(); + if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST) + { + creatureGrows = creature->getGrowth(); - if(town->hasBuilt(BuildingID::CASTLE)) - creatureGrows *= 2; - else if(town->hasBuilt(BuildingID::CITADEL)) - creatureGrows += creatureGrows / 2; + if(town->hasBuilt(BuildingID::CASTLE)) + creatureGrows *= 2; + else if(town->hasBuilt(BuildingID::CITADEL)) + creatureGrows += creatureGrows / 2; + } + else + { + creatureGrows = creature->getHorde(); + } } armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows); diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 83bb0a02b..efc4bde8e 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -17,20 +17,29 @@ namespace NKAI HitMapInfo HitMapInfo::NoTreat; +double HitMapInfo::value() const +{ + return danger / std::sqrt(turn / 3.0f + 1); +} + void DangerHitMapAnalyzer::updateHitMap() { - if(upToDate) + if(hitMapUpToDate) return; logAi->trace("Update danger hitmap"); - upToDate = true; + hitMapUpToDate = true; auto start = std::chrono::high_resolution_clock::now(); auto cb = ai->cb.get(); auto mapSize = ai->cb->getMapSize(); - hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + enemyHeroAccessibleObjects.clear(); + townTreats.clear(); std::map> heroes; @@ -67,27 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap() if(path.getFirstBlockedAction()) continue; - auto tileDanger = path.getHeroStrength(); - auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) - || (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)) + HitMapInfo newTreat; + + newTreat.hero = path.targetHero; + newTreat.turn = path.turn(); + newTreat.danger = path.getHeroStrength(); + + if(newTreat.value() > node.maximumDanger.value()) { - node.maximumDanger.danger = tileDanger; - node.maximumDanger.turn = turn; - node.maximumDanger.hero = path.targetHero; + node.maximumDanger = newTreat; } - if(turn < node.fastestDanger.turn - || (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)) + if(newTreat.turn < node.fastestDanger.turn + || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger)) { - node.fastestDanger.danger = tileDanger; - node.fastestDanger.turn = turn; - node.fastestDanger.hero = path.targetHero; + node.fastestDanger = newTreat; } - if(turn == 0) + if(newTreat.turn == 0) { auto objects = cb->getVisitableObjs(pos, false); @@ -95,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap() { if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) enemyHeroAccessibleObjects[path.targetHero].insert(obj); + + if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) + { + auto & treats = townTreats[obj->id]; + auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + { + return i.hero.hid == path.targetHero->id; + }); + + if(treat == treats.end()) + { + treats.emplace_back(); + treat = std::prev(treats.end(), 1); + } + + if(newTreat.value() > treat->value()) + { + *treat = newTreat; + } + } } } } @@ -104,6 +132,122 @@ void DangerHitMapAnalyzer::updateHitMap() logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); } +void DangerHitMapAnalyzer::calculateTileOwners() +{ + if(tileOwnersUpToDate) return; + + tileOwnersUpToDate = true; + + auto cb = ai->cb.get(); + auto mapSize = ai->cb->getMapSize(); + + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + std::map townHeroes; + std::map heroTownMap; + PathfinderSettings pathfinderSettings; + + pathfinderSettings.mainTurnDistanceLimit = 5; + + auto addTownHero = [&](const CGTownInstance * town) + { + auto townHero = new CGHeroInstance(); + CRandomGenerator rng; + auto visitablePos = town->visitablePos(); + + townHero->setOwner(ai->playerID); // lets avoid having multiple colors + townHero->initHero(rng, static_cast(0)); + townHero->pos = townHero->convertFromVisitablePos(visitablePos); + townHero->initObj(rng); + + heroTownMap[townHero] = town; + townHeroes[townHero] = HeroRole::MAIN; + }; + + for(auto obj : ai->memory->visitableObjs) + { + if(obj && obj->ID == Obj::TOWN) + { + addTownHero(dynamic_cast(obj)); + } + } + + for(auto town : cb->getTownsInfo()) + { + addTownHero(town); + } + + ai->pathfinder->updatePaths(townHeroes, PathfinderSettings()); + + pforeachTilePos(mapSize, [&](const int3 & pos) + { + float ourDistance = std::numeric_limits::max(); + float enemyDistance = std::numeric_limits::max(); + const CGTownInstance * enemyTown = nullptr; + const CGTownInstance * ourTown = nullptr; + + for(AIPath & path : ai->pathfinder->getPathInfo(pos)) + { + if(!path.targetHero || path.getFirstBlockedAction()) + continue; + + auto town = heroTownMap[path.targetHero]; + + if(town->getOwner() == ai->playerID) + { + if(ourDistance > path.movementCost()) + { + ourDistance = path.movementCost(); + ourTown = town; + } + } + else + { + if(enemyDistance > path.movementCost()) + { + enemyDistance = path.movementCost(); + enemyTown = town; + } + } + } + + if(ourDistance == enemyDistance) + { + hitMap[pos.x][pos.y][pos.z].closestTown = nullptr; + } + else if(!enemyTown || ourDistance < enemyDistance) + { + hitMap[pos.x][pos.y][pos.z].closestTown = ourTown; + } + else + { + hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown; + } + }); +} + +const std::vector & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const +{ + static const std::vector empty = {}; + + auto result = townTreats.find(town->id); + + return result == townTreats.end() ? empty : result->second; +} + +PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const +{ + auto town = hitMap[tile.x][tile.y][tile.z].closestTown; + + return town ? town->getOwner() : PlayerColor::NEUTRAL; +} + +const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const +{ + return hitMap[tile.x][tile.y][tile.z].closestTown; +} + uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const { int3 tile = path.targetTile(); @@ -144,7 +288,7 @@ const std::set & DangerHitMapAnalyzer::getOneTurnAcces void DangerHitMapAnalyzer::reset() { - upToDate = false; + hitMapUpToDate = false; } } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 660dfd593..79c56c2c4 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -35,6 +35,8 @@ struct HitMapInfo turn = 255; hero = HeroPtr(); } + + double value() const; }; struct HitMapNode @@ -42,6 +44,8 @@ struct HitMapNode HitMapInfo maximumDanger; HitMapInfo fastestDanger; + const CGTownInstance * closestTown = nullptr; + HitMapNode() = default; void reset() @@ -56,18 +60,25 @@ class DangerHitMapAnalyzer private: boost::multi_array hitMap; std::map> enemyHeroAccessibleObjects; - bool upToDate; + bool hitMapUpToDate = false; + bool tileOwnersUpToDate = false; const Nullkiller * ai; + std::map> townTreats; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} void updateHitMap(); + void calculateTileOwners(); uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getTileTreat(const int3 & tile) const; const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); + void resetTileOwners() { tileOwnersUpToDate = false; } + PlayerColor getTileOwner(const int3 & tile) const; + const CGTownInstance * getClosestTown(const int3 & tile) const; + const std::vector & getTownTreats(const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 089c4436a..b896ab728 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -125,6 +125,7 @@ void HeroManager::update() } std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); + heroRoles.clear(); for(auto hero : myHeroes) { @@ -180,6 +181,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } +bool HeroManager::heroCapReached() const +{ + const bool includeGarnisoned = true; + int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + + return heroCount >= ALLOWED_ROAMING_HEROES + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); +} + bool HeroManager::canRecruitHero(const CGTownInstance * town) const { if(!town) @@ -191,13 +201,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) return false; - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); - - if(heroCount >= ALLOWED_ROAMING_HEROES) - return false; - - if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if(heroCapReached()) return false; if(!cb->getAvailableHeroes(town).size()) @@ -225,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +{ + const CGHeroInstance * weakestHero = nullptr; + auto myHeroes = ai->cb->getHeroesInfo(); + + for(auto existingHero : myHeroes) + { + if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE + || existingHero->getArmyStrength() >armyLimit + || getHeroRole(existingHero) == HeroRole::MAIN + || existingHero->movementPointsRemaining() + || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) + { + continue; + } + + if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + { + weakestHero = existingHero; + } + } + + return weakestHero; +} + SecondarySkillScoreMap::SecondarySkillScoreMap(std::map scoreMap) :scoreMap(scoreMap) { diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 9c98443f3..1009fd31e 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -31,7 +31,9 @@ public: virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0; virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; + virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; + virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -71,7 +73,9 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override; bool canRecruitHero(const CGTownInstance * t = nullptr) const override; + bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 3aee0dcbf..5e2b41977 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize() auto obj = objs[i]; if(!shouldVisitObject(obj)) - return; + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); +#endif + continue; + } #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index e63e26c3a..155f45af6 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector tasks.reserve(paths.size()); - const AIPath * closestWay = nullptr; + std::unordered_map closestWaysByRole; std::vector waysToVisitObj; for(auto & path : paths) @@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::SCOUT - && (!closestWay || closestWay->movementCost() > path.movementCost())) + auto & closestWay = closestWaysByRole[heroRole]; + + if(!closestWay || closestWay->movementCost() > path.movementCost()) { closestWay = &path; } @@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } } - if(closestWay) + for(auto way : waysToVisitObj) { - for(auto way : waysToVisitObj) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero); + auto closestWay = closestWaysByRole[heroRole]; + + if(closestWay) { way->closestWayRatio = closestWay->movementCost() / way->getPath().movementCost(); @@ -209,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const { captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); - if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL) + if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL) captureObjects(ai->nullkiller->objectClusterizer->getFarObjects()); } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 2a1bbf4c5..3e1ee23f2 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -49,37 +49,119 @@ Goals::TGoalVec DefenceBehavior::decompose() const return tasks; } -void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const +bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector & paths) { - logAi->trace("Evaluating defence for %s", town->getNameTranslated()); - - auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - auto treats = { treatNode.maximumDanger, treatNode.fastestDanger }; - int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); - if(town->garrisonHero) + for(const AIPath & path : paths) { - if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; + bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; + + if(treatIsWeak && !needToSaveGrowth) { - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + if((path.exchangeCount == 1 && path.turn() < treat.turn) + || path.turn() < treat.turn - 1 + || (path.turn() < treat.turn && treat.turn >= 2)) { +#if NKAI_TRACE_LEVEL >= 1 logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + "Hero %s can eliminate danger for town %s using path %s.", + path.targetHero->getObjectName(), + town->getObjectName(), + path.toString()); +#endif - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - - return; + return true; } } + } + return false; +} + +void handleCounterAttack( + const CGTownInstance * town, + const HitMapInfo & treat, + const HitMapInfo & maximumDanger, + Goals::TGoalVec & tasks) +{ + if(treat.hero.validAndSet() + && treat.turn <= 1 + && (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn)) + { + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); + + for(int i = 0; i < heroCapturingPaths.size(); i++) + { + AIPath & path = heroCapturingPaths[i]; + TSubgoal goal = goals[i]; + + if(!goal || goal->invalid() || !goal->isElementar()) continue; + + Composition composition; + + composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + + tasks.push_back(Goals::sptr(composition)); + } + } +} + +bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks) +{ + if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + { logAi->trace( "Hero %s in garrison of town %s is suposed to defend the town", town->garrisonHero->getNameTranslated(), town->getNameTranslated()); + return true; + } + + if(!town->visitingHero) + { + if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); + + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + + return true; + } + else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) + { + auto armyDismissLimit = 1000; + auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit); + + if(heroToDismiss) + { + tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); + + return true; + } + } + } + + return false; +} + +void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const +{ + logAi->trace("Evaluating defence for %s", town->getNameTranslated()); + + auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); + std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + + treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there + + if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks)) + { return; } @@ -109,103 +191,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta std::to_string(treat.turn), treat.hero->getNameTranslated()); - bool treatIsUnderControl = false; + handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); - for(AIPath & path : paths) + if(isTreatUnderControl(town, treat, paths)) { - if(town->visitingHero && path.targetHero != town->visitingHero.get()) - continue; - - if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; - - if(treat.hero.validAndSet() - && treat.turn <= 1 - && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn) - && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) - { - Composition composition; - - composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get())); - - tasks.push_back(Goals::sptr(composition)); - } - - bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; - bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; - - if(treatIsWeak && !needToSaveGrowth) - { - if((path.exchangeCount == 1 && path.turn() < treat.turn) - || path.turn() < treat.turn - 1 - || (path.turn() < treat.turn && treat.turn >= 2)) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace( - "Hero %s can eliminate danger for town %s using path %s.", - path.targetHero->getObjectName(), - town->getObjectName(), - path.toString()); -#endif - - treatIsUnderControl = true; - - break; - } - } - } - - if(treatIsUnderControl) continue; - - if(!town->visitingHero - && town->hasBuilt(BuildingID::TAVERN) - && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) - { - auto heroesInTavern = cb->getAvailableHeroes(town); - - for(auto hero : heroesInTavern) - { - if(hero->getTotalStrength() > treat.danger) - { - auto myHeroes = cb->getHeroesInfo(); - - if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); -#endif - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1))); - continue; - } - else - { - const CGHeroInstance * weakestHero = nullptr; - - for(auto existingHero : myHeroes) - { - if(ai->nullkiller->isHeroLocked(existingHero) - || existingHero->getArmyStrength() > hero->getArmyStrength() - || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN - || existingHero->movementPointsRemaining() - || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) - continue; - - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) - { - weakestHero = existingHero; - } - - if(weakestHero) - { - tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero))); - } - } - } - } - } } + evaluateRecruitingHero(tasks, treat, town); + if(paths.empty()) { logAi->trace("No ways to defend town %s", town->getNameTranslated()); @@ -229,6 +223,22 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta path.movementCost(), path.toString()); #endif + + auto townDefenseStrength = town->garrisonHero + ? town->garrisonHero->getTotalStrength() + : (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength()); + + if(town->visitingHero && path.targetHero == town->visitingHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + if(path.turn() <= treat.turn - 2) { #if NKAI_TRACE_LEVEL >= 1 @@ -275,9 +285,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta tasks.push_back( Goals::sptr(Composition() .addNext(DefendTown(town, treat, path)) - .addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get())) - .addNext(ExecuteHeroChain(path, town)) - .addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)))); + .addNextSequence({ + sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())), + sptr(ExecuteHeroChain(path, town)), + sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)) + }))); continue; } @@ -313,15 +325,58 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } } - -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Move %s to defend town %s", - path.targetHero->getObjectName(), - town->getObjectName()); -#endif Composition composition; - composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town)); + composition.addNext(DefendTown(town, treat, path)); + TGoalVec sequence; + + if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1) + { + composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE)); + tasks.push_back(Goals::sptr(composition)); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Locking hero %s in garrison of %s", + town->garrisonHero.get()->getObjectName(), + town->getObjectName()); +#endif + + continue; + } + else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) + { + if(town->garrisonHero) + { + if(ai->nullkiller->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 + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + } + else if(path.turn() == 0) + { + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + } + } + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Move %s to defend town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + + sequence.push_back(sptr(ExecuteHeroChain(path, town))); + composition.addNextSequence(sequence); auto firstBlockedAction = path.getFirstBlockedAction(); if(firstBlockedAction) @@ -350,4 +405,70 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->debug("Found %d tasks", tasks.size()); } +void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const +{ + if(town->hasBuilt(BuildingID::TAVERN) + && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) + { + auto heroesInTavern = cb->getAvailableHeroes(town); + + for(auto hero : heroesInTavern) + { + if(hero->getTotalStrength() < treat.danger) + continue; + + auto myHeroes = cb->getHeroesInfo(); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); +#endif + bool needSwap = false; + const CGHeroInstance * heroToDismiss = nullptr; + + if(town->visitingHero) + { + if(!town->garrisonHero) + needSwap = true; + else + { + if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength()) + { + if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + + heroToDismiss = town->visitingHero.get(); + } + else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + else + { + needSwap = true; + heroToDismiss = town->garrisonHero.get(); + } + } + } + else if(ai->nullkiller->heroManager->heroCapReached()) + { + heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); + + if(!heroToDismiss) + continue; + } + + TGoalVec sequence; + Goals::Composition recruitHeroComposition; + + if(needSwap) + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + + if(heroToDismiss) + sequence.push_back(sptr(DismissHero(heroToDismiss))); + + sequence.push_back(sptr(Goals::RecruitHero(town, hero))); + + tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence))); + } + } +} + } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index bd606cd36..b9b7c486e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -15,8 +15,12 @@ namespace NKAI { + +struct HitMapInfo; + namespace Goals { + class DefenceBehavior : public CGoal { public: @@ -35,6 +39,7 @@ namespace Goals private: void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; + void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c2622cdc0..f5eb28c79 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -12,10 +12,13 @@ #include "../Engine/Nullkiller.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/Composition.h" +#include "../Goals/RecruitHero.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" +#include "CaptureObjectsBehavior.h" #include "../AIUtility.h" +#include "../Goals/ExchangeSwapTownHeroes.h" namespace NKAI { @@ -78,20 +81,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(path.containsHero(hero)) continue; - - if(path.turn() == 0 && hero->inTownGarrison) + if(path.containsHero(hero)) { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Selfcontaining path. Ignore"); #endif continue; } - if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) + bool garrisoned = false; + + if(path.turn() == 0 && hero->inTownGarrison) + { +#if NKAI_TRACE_LEVEL >= 1 + garrisoned = true; +#endif + } + + if(path.turn() > 0 && ai->nullkiller->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()); @@ -109,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her HeroExchange heroExchange(hero, path); - float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); + uint64_t armyValue = heroExchange.getReinforcementArmyStrength(); + float armyRatio = (float)armyValue / hero->getArmyStrength(); // avoid transferring very small amount of army - if(armyValue < 0.1f && armyValue < 20000) + if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Army value is too small."); @@ -172,7 +183,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her exchangePath.closestWayRatio = 1; composition.addNext(heroExchange); - composition.addNext(exchangePath); + + if(garrisoned && path.turn() == 0) + { + auto lockReason = ai->nullkiller->getHeroLockedReason(hero); + + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown)), + sptr(exchangePath), + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason)) + }); + } + else + { + composition.addNext(exchangePath); + } auto blockedAction = path.getFirstBlockedAction(); @@ -212,18 +237,42 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) #endif auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); + auto goals = CaptureObjectsBehavior::getVisitGoals(paths); + std::vector> waysToVisitObj; #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Found %d paths", paths.size()); #endif + bool hasMainAround = false; + for(const AIPath & path : paths) { + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + hasMainAround = true; + } + + for(int i = 0; i < paths.size(); i++) + { + auto & path = paths[i]; + auto visitGoal = goals[i]; + #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) + + if(visitGoal->invalid()) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Ignore path. Not valid way."); +#endif + continue; + } + + if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Town has visiting hero."); @@ -261,18 +310,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); - if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) + if(!upgrader->garrisonHero + && ( + hasMainAround + || ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)) { - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanGet( + ArmyUpgradeInfo armyToGetOrBuy; + + armyToGetOrBuy.addArmyToGet( + ai->nullkiller->armyManager->getBestArmy( path.targetHero, path.heroArmy, - upgrader->getUpperArmy()); + upgrader->getUpperArmy())); + + armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); + + armyToGetOrBuy.addArmyToBuy( + ai->nullkiller->armyManager->toSlotInfo( + ai->nullkiller->armyManager->getArmyAvailableToBuy( + path.heroArmy, + upgrader, + ai->nullkiller->getFreeResources(), + path.turn()))); + + upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; + upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; + vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); + + if(!upgrade.upgradeValue + && armyToGetOrBuy.upgradeValue > 20000 + && ai->nullkiller->heroManager->canRecruitHero(town) + && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + { + for(auto hero : cb->getAvailableHeroes(town)) + { + auto scoutReinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town) + + ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town); + + if(scoutReinforcement >= armyToGetOrBuy.upgradeValue + && ai->nullkiller->getFreeGold() >20000 + && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE) + { + Composition recruitHero; + + recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero)); + } + } + } } auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // avoid small upgrades { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Army value is too small (%f)", armyValue); @@ -297,11 +386,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(isSafe) { - ExecuteHeroChain newWay(path, upgrader); - - newWay.closestWayRatio = 1; - - tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay))); + tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal))); } } diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index bbc9dd737..581c725e6 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const } } + int treasureSourcesCount = 0; + + for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) + { + if((obj->ID == Obj::RESOURCE) + || obj->ID == Obj::TREASURE_CHEST + || obj->ID == Obj::CAMPFIRE + || isWeeklyRevisitable(obj) + || obj->ID ==Obj::ARTIFACT) + { + auto tile = obj->visitablePos(); + auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile); + + if(town == closestTown) + treasureSourcesCount++; + } + } + + if(treasureSourcesCount < 5) + continue; + if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index b1abec00c..a6560989f 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -52,6 +52,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -114,6 +115,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index f9ac898dc..2757cb35a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) case Obj::MINE: case Obj::ABANDONED_MINE: case Obj::PANDORAS_BOX: - { - const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); - } case Obj::CRYPT: //crypt case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); + { + const CArmedInstance * a = dynamic_cast(obj); + return a->getArmyStrength(); + } case Obj::PYRAMID: { if(obj->subID == 0) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c5aa3324f..66b28ca8e 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) armyManager.reset(new ArmyManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this)); decomposer.reset(new DeepDecomposer()); + armyFormation.reset(new ArmyFormation(cb, this)); } Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const @@ -117,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi void Nullkiller::resetAiState() { lockedResources = TResources(); - scanDepth = ScanDepth::FULL; + scanDepth = ScanDepth::MAIN_FULL; playerID = ai->playerID; lockedHeroes.clear(); dangerHitMap->reset(); @@ -133,10 +134,14 @@ void Nullkiller::updateAiState(int pass, bool fast) activeHero = nullptr; setTargetObject(-1); + decomposer->reset(); + buildAnalyzer->update(); + if(!fast) { memory->removeInvisibleObjects(cb.get()); + dangerHitMap->calculateTileOwners(); dangerHitMap->updateHitMap(); boost::this_thread::interruption_point(); @@ -156,11 +161,15 @@ void Nullkiller::updateAiState(int pass, bool fast) PathfinderSettings cfg; cfg.useHeroChain = useHeroChain; - cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; - if(scanDepth != ScanDepth::FULL) + if(scanDepth == ScanDepth::SMALL) { - cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); + cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT; + } + + if(scanDepth != ScanDepth::ALL_FULL) + { + cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; } boost::this_thread::interruption_point(); @@ -173,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast) } armyManager->update(); - buildAnalyzer->update(); - decomposer->reset(); logAi->debug("AI state updated in %ld", timeElapsed(start)); } @@ -222,7 +229,7 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7; + const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; resetAiState(); @@ -231,8 +238,8 @@ void Nullkiller::makeTurn() updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); - - do + + for(;i <= MAXPASS; i++) { Goals::TTaskVec fastTasks = { choseBestTask(sptr(BuyArmyBehavior()), 1), @@ -246,7 +253,11 @@ void Nullkiller::makeTurn() executeTask(bestTask); updateAiState(i, true); } - } while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); + else + { + break; + } + } Goals::TTaskVec bestTasks = { bestTask, @@ -265,7 +276,6 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(bestTasks); HeroPtr hero = bestTask->getHero(); - HeroRole heroRole = HeroRole::MAIN; if(hero.validAndSet()) @@ -274,20 +284,39 @@ void Nullkiller::makeTurn() if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) useHeroChain = false; + // TODO: better to check turn distance here instead of priority if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) - && scanDepth == ScanDepth::FULL) + && scanDepth == ScanDepth::MAIN_FULL) { useHeroChain = false; scanDepth = ScanDepth::SMALL; logAi->trace( - "Goal %s has too low priority %f so increasing scan depth", + "Goal %s has low priority %f so decreasing scan depth to gain performance.", bestTask->toString(), bestTask->priority); } if(bestTask->priority < MIN_PRIORITY) { + auto heroes = cb->getHeroesInfo(); + auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool + { + return h->movementPointsRemaining() > 100; + }); + + if(hasMp && scanDepth != ScanDepth::ALL_FULL) + { + logAi->trace( + "Goal %s has too low priority %f so increasing scan depth to full.", + bestTask->toString(), + bestTask->priority); + + scanDepth = ScanDepth::ALL_FULL; + useHeroChain = false; + continue; + } + logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); return; diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 1b5e513e6..36f3504fd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -18,6 +18,7 @@ #include "../Analyzers/ArmyManager.h" #include "../Analyzers/HeroManager.h" #include "../Analyzers/ObjectClusterizer.h" +#include "../Helpers/ArmyFormation.h" namespace NKAI { @@ -39,9 +40,11 @@ enum class HeroLockedReason enum class ScanDepth { - FULL = 0, + MAIN_FULL = 0, - SMALL = 1 + SMALL = 1, + + ALL_FULL = 2 }; class Nullkiller @@ -67,6 +70,7 @@ public: std::unique_ptr memory; std::unique_ptr dangerEvaluator; std::unique_ptr decomposer; + std::unique_ptr armyFormation; PlayerColor playerID; std::shared_ptr cb; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 05be5a68c..6e3e5f754 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -23,6 +23,7 @@ #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" #include "../Goals/ExchangeSwapTownHeroes.h" +#include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" @@ -33,6 +34,7 @@ namespace NKAI #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us +const float MIN_CRITICAL_VALUE = 2.0f; EvaluationContext::EvaluationContext(const Nullkiller * ai) : movementCost(0.0), @@ -49,10 +51,16 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) turn(0), strategicalValue(0), evaluator(ai), - enemyHeroDangerRatio(0) + enemyHeroDangerRatio(0), + armyGrowth(0) { } +void EvaluationContext::addNonCriticalStrategicalValue(float value) +{ + vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE)); +} + PriorityEvaluator::~PriorityEvaluator() { delete engine; @@ -64,6 +72,7 @@ void PriorityEvaluator::initVisitTile() std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); + armyGrowthVariable = engine->getInputVariable("armyGrowth"); heroRoleVariable = engine->getInputVariable("heroRole"); dangerVariable = engine->getInputVariable("danger"); turnVariable = engine->getInputVariable("turn"); @@ -99,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons auto town = cb->getTown(target->id); auto fortLevel = town->fortLevel(); - if(town->hasCapitol()) return booster * 2000; + if(town->hasCapitol()) + return booster * 2000; // probably well developed town will have city hall if(fortLevel == CGTownInstance::CASTLE) return booster * 750; @@ -153,18 +163,18 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero { result += (c.data.type->getAIValue() * c.data.count) * c.chance; } - else + /*else { //we will need to discard the weakest stack result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance; - } + }*/ } result /= 100; //divide by total chance return result; } -uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold) +uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold) { auto dwelling = dynamic_cast(target); uint64_t score = 0; @@ -185,6 +195,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool return score; } +uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor) +{ + auto dwelling = dynamic_cast(target); + uint64_t score = 0; + + if(dwelling->getOwner() == myColor) + return 0; + + for(auto & creLevel : dwelling->creatures) + { + if(creLevel.second.size()) + { + auto creature = creLevel.second.back().toCreature(); + + score += creature->getAIValue() * creature->getGrowth(); + } + } + + return score; +} + int getDwellingArmyCost(const CGObjectInstance * target) { auto dwelling = dynamic_cast(target); @@ -247,23 +278,13 @@ uint64_t RewardEvaluator::getArmyReward( { const float enemyArmyEliminationRewardRatio = 0.5f; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID); + if(!target) return 0; switch(target->ID) { - case Obj::TOWN: - { - auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2; - - if(fortLevel < CGTownInstance::CITADEL) - return town->hasFort() ? booster * 500 : 0; - else - return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); - } - case Obj::HILL_FORT: return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; case Obj::CREATURE_BANK: @@ -272,7 +293,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: - return getDwellingScore(ai->cb.get(), target, checkGold); + return getDwellingArmyValue(ai->cb.get(), target, checkGold); case Obj::CRYPT: case Obj::SHIPWRECK: case Obj::SHIPWRECK_SURVIVOR: @@ -283,7 +304,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::DRAGON_UTOPIA: return 10000; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() : 0; case Obj::PANDORAS_BOX: @@ -293,6 +314,47 @@ uint64_t RewardEvaluator::getArmyReward( } } +uint64_t RewardEvaluator::getArmyGrowth( + const CGObjectInstance * target, + const CGHeroInstance * hero, + const CCreatureSet * army) const +{ + if(!target) + return 0; + + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + + if(relations != PlayerRelations::ENEMIES) + return 0; + + switch(target->ID) + { + case Obj::TOWN: + { + auto town = dynamic_cast(target); + auto fortLevel = town->fortLevel(); + auto neutral = !town->getOwner().isValidPlayer(); + auto booster = isAnotherAi(town, *ai->cb) || neutral ? 1 : 2; + + if(fortLevel < CGTownInstance::CITADEL) + return town->hasFort() ? booster * 500 : 0; + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); + } + + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner()); + case Obj::ARTIFACT: + // it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well. + return 0; + default: + return 0; + } +} + int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const { if(!target) @@ -338,7 +400,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy 2. The formula quickly approaches 1.0 as hero level increases, but higher level always means higher value and the minimal value for level 1 hero is 0.5 */ - return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level)))); + return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level)))); } float RewardEvaluator::getResourceRequirementStrength(int resType) const @@ -366,10 +428,26 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const return 0; float ratio = dailyIncome[resType] == 0 - ? (float)requiredResources[resType] / 50.0f - : (float)requiredResources[resType] / dailyIncome[resType] / 50.0f; + ? (float)requiredResources[resType] / 10.0f + : (float)requiredResources[resType] / dailyIncome[resType] / 20.0f; - return std::min(ratio, 1.0f); + return std::min(ratio, 2.0f); +} + +uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const +{ + uint64_t result = 0; + + for(auto creatureInfo : town->creatures) + { + if(creatureInfo.second.empty()) + continue; + + auto creature = creatureInfo.second.back().toCreature(); + result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); + } + + return result; } float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const @@ -407,18 +485,28 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons case Obj::TOWN: { if(ai->buildAnalyzer->getDevelopmentInfo().empty()) - return 1; + return 10.0f; auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1; - if(town->hasCapitol()) return 1; + 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 = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f; + + if(town->hasCapitol()) + return booster * 1.5; if(fortLevel < CGTownInstance::CITADEL) - return booster * (town->hasFort() ? 0.6 : 0.4); + return booster * (town->hasFort() ? 1.0 : 0.8); else - return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); + return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2); } case Obj::HERO: @@ -463,15 +551,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::GARDEN_OF_REVELATION: case Obj::MARLETTO_TOWER: case Obj::MERCENARY_CAMP: - case Obj::SHRINE_OF_MAGIC_GESTURE: - case Obj::SHRINE_OF_MAGIC_INCANTATION: case Obj::TREE_OF_KNOWLEDGE: return 1; case Obj::LEARNING_STONE: return 1.0f / std::sqrt(hero->level); case Obj::ARENA: - case Obj::SHRINE_OF_MAGIC_THOUGHT: return 2; + case Obj::SHRINE_OF_MAGIC_INCANTATION: + return 0.2f; + case Obj::SHRINE_OF_MAGIC_GESTURE: + return 0.3f; + case Obj::SHRINE_OF_MAGIC_THOUGHT: + return 0.5f; case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: @@ -513,12 +604,13 @@ int32_t getArmyCost(const CArmedInstance * army) return value; } -/// Gets aproximated reward in gold. Daily income is multiplied by 5 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const { if(!target) return 0; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; @@ -559,7 +651,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG //Objectively saves us 2500 to hire hero return GameConstants::HERO_GOLD_COST; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast(target)) : 0; default: @@ -579,7 +671,8 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(); - evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength()); + evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get()); } }; @@ -596,7 +689,7 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; - evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); } }; @@ -621,23 +714,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin class DefendTownEvaluator : public IEvaluationContextBuilder { -private: - uint64_t townArmyIncome(const CGTownInstance * town) const - { - uint64_t result = 0; - - for(auto creatureInfo : town->creatures) - { - if(creatureInfo.second.empty()) - continue; - - auto creature = creatureInfo.second.back().toCreature(); - result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); - } - - return result; - } - public: virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { @@ -648,22 +724,34 @@ public: const CGTownInstance * town = defendTown.town; auto & treat = defendTown.getTreat(); - auto armyIncome = townArmyIncome(town); - auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; - - auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f; - - if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) - strategicalValue = 1; + auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town); float multiplier = 1; if(treat.turn < defendTown.getTurn()) multiplier /= 1 + (defendTown.getTurn() - treat.turn); - evaluationContext.armyReward += armyIncome * multiplier; + multiplier /= 1.0f + treat.turn / 5.0f; + + if(defendTown.getTurn() > 0 && defendTown.isCounterAttack()) + { + auto ourSpeed = defendTown.hero->movementPointsLimit(true); + auto enemySpeed = treat.hero->movementPointsLimit(true); + + if(enemySpeed > ourSpeed) multiplier *= 0.7f; + } + + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town); + + evaluationContext.armyGrowth += armyGrowth * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.strategicalValue += strategicalValue * multiplier; + + if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) + vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue); + else + evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -709,18 +797,22 @@ public: auto army = path.heroArmy; const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); + auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); - if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) + if(heroRole == HeroRole::MAIN) + evaluationContext.heroRole = heroRole; + + if (target) { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); - evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); + evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); + evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); - evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -760,7 +852,7 @@ public: evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; @@ -798,6 +890,31 @@ public: } }; +class DismissHeroContextBuilder : public IEvaluationContextBuilder +{ +private: + const Nullkiller * ai; + +public: + DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {} + + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::DISMISS_HERO) + return; + + Goals::DismissHero & dismissCommand = dynamic_cast(*task); + const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); + + auto role = ai->heroManager->getHeroRole(dismissedHero); + auto mpLeft = dismissedHero->movementPointsRemaining(); + + evaluationContext.movementCost += mpLeft; + evaluationContext.movementCostByRole[role] += mpLeft; + evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero); + } +}; + class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder { public: @@ -813,39 +930,47 @@ public: evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; + evaluationContext.closestWayRatio = 1; if(bi.creatureID != CreatureID::NONE) { - evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0; + evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0); if(bi.baseCreatureID == bi.creatureID) { - evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount); evaluationContext.armyReward += bi.armyStrength; } else { auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); - evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } } else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) { - evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f; + evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f); evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; } - else + else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) + { + evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + } + + if(evaluationContext.goldReward) { auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); - evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount); } if(bi.notEnoughRes && bi.prerequisitesCount == 1) { - evaluationContext.strategicalValue /= 2; + evaluationContext.strategicalValue /= 3; + evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5; + evaluationContext.turn += 5; } } }; @@ -872,6 +997,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); + evaluationContextBuilders.push_back(std::make_shared(ai)); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const @@ -909,6 +1035,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) + (evaluationContext.armyReward > 0 ? 1 : 0) + (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.strategicalValue > 0 ? 1 : 0); + + float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10); double result = 0; @@ -918,8 +1046,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) heroRoleVariable->setValue(evaluationContext.heroRole); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(evaluationContext.goldReward); + goldRewardVariable->setValue(goldRewardPerTurn); armyRewardVariable->setValue(evaluationContext.armyReward); + armyGrowthVariable->setValue(evaluationContext.armyGrowth); skillRewardVariable->setValue(evaluationContext.skillReward); dangerVariable->setValue(evaluationContext.danger); rewardTypeVariable->setValue(rewardType); @@ -940,13 +1069,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::SCOUT], - evaluationContext.goldReward, + goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, evaluationContext.danger, diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 840169970..fb8085494 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -33,6 +33,7 @@ public: RewardEvaluator(const Nullkiller * ai) : ai(ai) {} uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const; + uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getResourceRequirementStrength(int resType) const; @@ -43,6 +44,7 @@ public: int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; + uint64_t townArmyGrowth(const CGTownInstance * town) const; }; struct DLL_EXPORT EvaluationContext @@ -54,6 +56,7 @@ struct DLL_EXPORT EvaluationContext float closestWayRatio; float armyLossPersentage; float armyReward; + uint64_t armyGrowth; int32_t goldReward; int32_t goldCost; float skillReward; @@ -64,6 +67,8 @@ struct DLL_EXPORT EvaluationContext float enemyHeroDangerRatio; EvaluationContext(const Nullkiller * ai); + + void addNonCriticalStrategicalValue(float value); }; class IEvaluationContextBuilder @@ -95,6 +100,7 @@ private: fl::InputVariable * turnVariable; fl::InputVariable * goldRewardVariable; fl::InputVariable * armyRewardVariable; + fl::InputVariable * armyGrowthVariable; fl::InputVariable * dangerVariable; fl::InputVariable * skillRewardVariable; fl::InputVariable * strategicalValueVariable; diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index ddbabe936..f2e4aca05 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai) throw cannotFulfillGoalException("No creatures to buy."); } - if(town->visitingHero) + if(town->visitingHero && !town->garrisonHero) { ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get()); } diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index a0a487820..30d3791f9 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -31,9 +31,17 @@ std::string Composition::toString() const { std::string result = "Composition"; - for(auto goal : subtasks) + for(auto step : subtasks) { - result += " " + goal->toString(); + result += "["; + for(auto goal : step) + { + if(goal->isElementar()) + result += goal->toString() + " => "; + else + result += goal->toString() + ", "; + } + result += "] "; } return result; @@ -41,17 +49,34 @@ std::string Composition::toString() const void Composition::accept(AIGateway * ai) { - taskptr(*subtasks.back())->accept(ai); + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + taskptr(*task)->accept(ai); + } + else + { + break; + } + } } TGoalVec Composition::decompose() const { - return subtasks; + TGoalVec result; + + for(const TGoalVec & step : subtasks) + vstd::concatenate(result, step); + + return result; } -Composition & Composition::addNext(const AbstractGoal & goal) +Composition & Composition::addNextSequence(const TGoalVec & taskSequence) { - return addNext(sptr(goal)); + subtasks.push_back(taskSequence); + + return *this; } Composition & Composition::addNext(TSubgoal goal) @@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal) } else { - subtasks.push_back(goal); + subtasks.push_back({goal}); } return *this; } +Composition & Composition::addNext(const AbstractGoal & goal) +{ + return addNext(sptr(goal)); +} + bool Composition::isElementar() const { - return subtasks.back()->isElementar(); + return subtasks.back().front()->isElementar(); } int Composition::getHeroExchangeCount() const { - return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0; + auto result = 0; + + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + result += taskptr(*task)->getHeroExchangeCount(); + } + } + + return result; } } diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index f7de02bb0..ecb4a1ab9 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -18,7 +18,7 @@ namespace Goals class DLL_EXPORT Composition : public ElementarGoal { private: - TGoalVec subtasks; + std::vector subtasks; // things we want to do now public: Composition() @@ -26,16 +26,12 @@ namespace Goals { } - Composition(TGoalVec subtasks) - : ElementarGoal(Goals::COMPOSITION), subtasks(subtasks) - { - } - virtual bool operator==(const Composition & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; Composition & addNext(const AbstractGoal & goal); Composition & addNext(TSubgoal goal); + Composition & addNextSequence(const TGoalVec & taskSequence); virtual TGoalVec decompose() const override; virtual bool isElementar() const override; virtual int getHeroExchangeCount() const override; diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 41b0d4e49..d367f96b4 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + auto targetObject = ai->myCb->getObj(static_cast(objid), false); + + if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN) + { + auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner()); + + if(relations == PlayerRelations::ENEMIES) + { + ai->nullkiller->armyFormation->rearrangeArmyForSiege( + dynamic_cast(targetObject), + chainPath.targetHero); + } + } + std::set blockedIndexes; for(int i = chainPath.nodes.size() - 1; i >= 0; i--) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 9dc63020b..e787c7529 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -24,7 +24,10 @@ using namespace Goals; std::string RecruitHero::toString() const { - return "Recruit hero at " + town->getNameTranslated(); + if(heroToBuy) + return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated(); + else + return "Recruit hero at " + town->getNameTranslated(); } void RecruitHero::accept(AIGateway * ai) @@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai) throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } - auto heroToHire = heroes[0]; + auto heroToHire = heroToBuy; - for(auto hero : heroes) + if(!heroToHire) { - if(objid == hero->id.getNum()) + for(auto hero : heroes) { - heroToHire = hero; - break; + if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength()) + heroToHire = hero; } - - if(hero->getTotalStrength() > heroToHire->getTotalStrength()) - heroToHire = hero; } + if(!heroToHire) + throw cannotFulfillGoalException("No hero to hire!"); + if(t->visitingHero) { cb->swapGarrisonHero(t); diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 34a968f33..78c6b0867 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -22,18 +22,20 @@ namespace Goals { class DLL_EXPORT RecruitHero : public ElementarGoal { + private: + const CGHeroInstance * heroToBuy; + public: RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy) - : RecruitHero(townWithTavern) + : ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy) { - objid = heroToBuy->id.getNum(); + town = townWithTavern; + priority = 1; } RecruitHero(const CGTownInstance * townWithTavern) - : ElementarGoal(Goals::RECRUIT_HERO) + : RecruitHero(townWithTavern, nullptr) { - priority = 1; - town = townWithTavern; } virtual bool operator==(const RecruitHero & other) const override diff --git a/AI/Nullkiller/Helpers/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp new file mode 100644 index 000000000..464cb10d0 --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp @@ -0,0 +1,68 @@ +/* +* ArmyFormation.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#include "StdInc.h" +#include "ArmyFormation.h" +#include "../../../lib/mapObjects/CGTownInstance.h" + +namespace NKAI +{ + +void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +{ + auto freeSlots = attacker->getFreeSlotsQueue(); + + while(!freeSlots.empty()) + { + auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair & slot) -> int + { + return slot.second->getCount() == 1 + ? std::numeric_limits::max() + : slot.second->getCreatureID().toCreature()->getAIValue(); + }); + + if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) + { + break; + } + + cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); + freeSlots.pop(); + } + + if(town->fortLevel() > CGTownInstance::FORT) + { + std::vector stacks; + + for(auto slot : attacker->Slots()) + stacks.push_back(slot.second); + + boost::sort( + stacks, + [](CStackInstance * slot1, CStackInstance * slot2) -> bool + { + auto cre1 = slot1->getCreatureID().toCreature(); + auto cre2 = slot2->getCreatureID().toCreature(); + auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING); + + if(flying != 0) return flying < 0; + else return cre1->getAIValue() < cre2->getAIValue(); + }); + + for(int i = 0; i < stacks.size(); i++) + { + auto pos = vstd::findKey(attacker->Slots(), stacks[i]); + + if(pos.getNum() != i) + cb->swapCreatures(attacker, attacker, static_cast(i), pos); + } + } +} + +} diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h new file mode 100644 index 000000000..817c6158d --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -0,0 +1,38 @@ +/* +* ArmyFormation.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +#include "../AIUtility.h" + +#include "../../../lib/GameConstants.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/CTownHandler.h" +#include "../../../lib/CBuildingHandler.h" + +namespace NKAI +{ + +struct HeroPtr; +class AIGateway; +class FuzzyHelper; +class Nullkiller; + +class DLL_EXPORT ArmyFormation +{ +private: + std::shared_ptr cb; //this is enough, but we downcast from CCallback + +public: + ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB) {} + + void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); +}; + +} diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index bc475583d..0f6d41090 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up sethero(upgradePath.targetHero); } +ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) + : CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue), + initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD]) +{ + sethero(targetMain); +} + bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const { return false; diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.h b/AI/Nullkiller/Markers/ArmyUpgrade.h index df30c3588..1af41a5bf 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.h +++ b/AI/Nullkiller/Markers/ArmyUpgrade.h @@ -27,6 +27,7 @@ namespace Goals public: ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); + ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); virtual bool operator==(const ArmyUpgrade & other) const override; virtual std::string toString() const override; diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index b09952f22..dd7d08e25 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -18,8 +18,8 @@ namespace NKAI using namespace Goals; -DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath) - : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()) +DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack) + : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack) { settown(town); sethero(defencePath.targetHero); diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index 083f9b07f..34b8b3427 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -24,9 +24,10 @@ namespace Goals uint64_t defenceArmyStrength; HitMapInfo treat; uint8_t turn; + bool counterattack; public: - DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath); + DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); virtual bool operator==(const DefendTown & other) const override; @@ -37,6 +38,8 @@ namespace Goals uint64_t getDefenceStrength() const { return defenceArmyStrength; } uint8_t getTurn() const { return turn; } + + bool isCounterAttack() { return counterattack; } }; } diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 562215bb3..499122327 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const std::string HeroExchange::toString() const { - return "Hero exchange " + exchangePath.toString(); + return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString(); } uint64_t HeroExchange::getReinforcementArmyStrength() const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c0d650464..4ff2e4ef9 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) + if(hero.first->getOwner() == ai->playerID + && hero.first->inTownGarrison + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + { continue; + } uint64_t mask = FirstActorMask << actors.size(); auto actor = std::make_shared(hero.first, hero.second, mask, ai); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 22c2d6b21..c127f294b 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -24,8 +24,8 @@ namespace NKAI { - const int SCOUT_TURN_DISTANCE_LIMIT = 3; - const int MAIN_TURN_DISTANCE_LIMIT = 5; + const int SCOUT_TURN_DISTANCE_LIMIT = 5; + const int MAIN_TURN_DISTANCE_LIMIT = 10; namespace AIPathfinding { @@ -258,7 +258,7 @@ public: { double ratio = (double)danger / (armyValue * hero->getFightingStrength()); - return (uint64_t)(armyValue * ratio * ratio * ratio); + return (uint64_t)(armyValue * ratio * ratio); } STRONG_INLINE diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 17cff54db..b51d19bbe 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map heroes storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit); storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit); + logAi->trace( + "Scout turn distance: %s, main %s", + std::to_string(pathfinderSettings.scoutTurnDistanceLimit), + std::to_string(pathfinderSettings.mainTurnDistanceLimit)); + if(pathfinderSettings.useHeroChain) { storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs); diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 3fd24a222..bf176b3ec 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base) armyCost = base->armyCost; actorAction = base->actorAction; tiCache = base->tiCache; + actorExchangeCount = base->actorExchangeCount; } void HeroActor::setupSpecialActors() diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index b90a350f7..1989bc3c7 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -5,10 +5,10 @@ InputVariable: mainTurnDistance enabled: true range: 0.000 10.000 lock-range: true - term: LOWEST Ramp 0.250 0.000 - term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000 - term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000 - term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000 + term: LOWEST Ramp 0.400 0.000 + term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000 + term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000 + term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800 InputVariable: scoutTurnDistance description: distance to tile in turns enabled: true @@ -23,11 +23,11 @@ InputVariable: goldReward enabled: true range: 0.000 5000.000 lock-range: true - term: LOW Triangle 10.000 500.000 2000.000 - term: MEDIUM Triangle 500.000 2000.000 5000.000 - term: HIGH Ramp 2000.000 5000.000 - term: NONE Ramp 100.000 0.000 - term: LOWEST Triangle 0.000 100.000 500.000 + term: LOWEST Triangle 0.000 100.000 200.000 + term: SMALL Triangle 100.000 200.000 400.000 + term: MEDIUM Triangle 200.000 400.000 1000.000 + term: BIG Triangle 400.000 1000.000 5000.000 + term: HUGE Ramp 1000.000 5000.000 InputVariable: armyReward enabled: true range: 0.000 10000.000 @@ -43,6 +43,7 @@ InputVariable: armyLoss term: LOW Ramp 0.200 0.000 term: MEDIUM Triangle 0.000 0.200 0.500 term: HIGH Ramp 0.200 0.500 + term: ALL Ramp 0.700 1.000 InputVariable: heroRole enabled: true range: -0.100 1.100 @@ -82,20 +83,21 @@ InputVariable: closestHeroRatio InputVariable: strategicalValue description: Some abstract long term benefit non gold or army or skill enabled: true - range: 0.000 1.000 + range: 0.000 3.000 lock-range: false term: NONE Ramp 0.200 0.000 term: LOWEST Triangle 0.000 0.010 0.250 - term: LOW Triangle 0.000 0.250 0.700 - term: MEDIUM Triangle 0.250 0.700 1.000 - term: HIGH Ramp 0.700 1.000 + term: LOW Triangle 0.000 0.250 1.000 + term: MEDIUM Triangle 0.250 1.000 2.000 + term: HIGH Triangle 1.000 2.000 3.000 + term: CRITICAL Ramp 2.000 3.000 InputVariable: goldPreasure description: Ratio between weekly army cost and gold income enabled: true range: 0.000 1.000 lock-range: false term: LOW Ramp 0.300 0.000 - term: HIGH Discrete 0.100 0.000 0.250 0.100 0.300 0.200 0.400 0.700 1.000 1.000 + term: HIGH Discrete 0.100 0.000 0.250 0.200 0.300 0.300 0.400 0.700 1.000 1.000 InputVariable: goldCost description: Action cost in gold enabled: true @@ -121,106 +123,154 @@ InputVariable: fear term: LOW Triangle 0.000 0.500 1.000 term: MEDIUM Triangle 0.500 1.000 1.500 term: HIGH Ramp 1.000 1.800 +InputVariable: armyGrowth + enabled: true + range: 0.000 20000.000 + lock-range: false + term: NONE Ramp 100.000 0.000 + term: SMALL Triangle 0.000 1000.000 3000.000 + term: MEDIUM Triangle 1000.000 3000.000 8000.000 + term: BIG Triangle 3000.000 8000.000 20000.000 + term: HUGE Ramp 8000.000 20000.000 OutputVariable: Value enabled: true - range: -0.500 1.500 + range: -1.500 2.500 lock-range: false aggregation: AlgebraicSum defuzzifier: Centroid 100 default: 0.500 lock-previous: false - term: LOWEST Discrete -0.500 0.000 -0.500 1.000 -0.200 1.000 -0.200 0.000 0.200 0.000 0.200 1.000 0.500 1.000 0.500 0.000 0.500 - term: BITLOW Rectangle -0.010 0.010 0.500 - term: LOW Discrete -0.150 0.000 -0.150 1.000 -0.050 1.000 -0.050 0.000 0.050 0.000 0.050 1.000 0.150 1.000 0.150 0.000 0.500 - term: MEDIUM Triangle 0.450 0.500 0.550 0.050 - term: HIGH Discrete 0.850 0.000 0.850 1.000 0.950 1.000 0.950 0.000 1.050 0.000 1.050 1.000 1.150 1.000 1.150 0.000 0.500 - term: HIGHEST Discrete 0.500 0.000 0.500 1.000 0.800 1.000 0.800 0.000 1.200 0.000 1.200 1.000 1.500 1.000 1.500 0.000 0.500 - term: BITHIGH Rectangle 0.990 1.010 0.500 -RuleBlock: gold reward + term: WORST Binary -1.000 -inf 0.700 + term: BAD Rectangle -1.000 -0.700 0.500 + term: BASE Rectangle -0.200 0.200 0.350 + term: MEDIUM Rectangle 0.910 1.090 0.500 + term: SMALL Rectangle 0.960 1.040 0.600 + term: BITHIGH Rectangle 0.850 1.150 0.400 + term: HIGH Rectangle 0.750 1.250 0.400 + term: HIGHEST Rectangle 0.500 1.500 0.350 + term: CRITICAL Ramp 0.500 2.000 0.500 +RuleBlock: basic enabled: true conjunction: AlgebraicProduct disjunction: AlgebraicSum implication: AlgebraicProduct activation: General - rule: if turn is NOW and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.5 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.8 - rule: if turn is NEXT and scoutTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW with 0.3 - rule: if turn is FUTURE and scoutTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is NONE then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is LOW then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is MEDIUM then Value is LOW with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is HIGH then Value is BITLOW - rule: if turn is FUTURE and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if scoutTurnDistance is MEDIUM and heroRole is SCOUT then Value is BITLOW - rule: if mainTurnDistance is MEDIUM then Value is BITLOW - rule: if scoutTurnDistance is LOW and heroRole is SCOUT then Value is MEDIUM - rule: if mainTurnDistance is LOW then Value is MEDIUM - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH with 0.7 - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is SCOUT and danger is not NONE then Value is MEDIUM - rule: if goldReward is MEDIUM and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is BITLOW - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is MAIN and danger is not NONE then Value is BITHIGH - rule: if goldReward is LOW and goldPreasure is HIGH and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is LOW and heroRole is MAIN and danger is not NONE and rewardType is SINGLE and armyLoss is LOW then Value is BITLOW - rule: if goldReward is LOW and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOW - rule: if goldReward is LOWEST and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOWEST - rule: if armyReward is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is HIGH with 0.5 - rule: if armyReward is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and mainTurnDistance is LOWEST then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is NONE and fear is not HIGH then Value is HIGH - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH with 0.2 - rule: if armyReward is MEDIUM and heroRole is SCOUT and danger is NONE then Value is HIGHEST with 0.5 - rule: if armyReward is LOW and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is NONE then Value is BITLOW with 0.5 - rule: if armyReward is LOW and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH - rule: if skillReward is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH - rule: if skillReward is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is BITHIGH - rule: if skillReward is MEDIUM and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 - rule: if skillReward is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if skillReward is MEDIUM and heroRole is SCOUT then Value is LOWEST - rule: if skillReward is HIGH and heroRole is SCOUT then Value is LOWEST - rule: if strategicalValue is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH - rule: if strategicalValue is LOWEST and heroRole is MAIN and armyLoss is LOW then Value is LOW - rule: if strategicalValue is LOW and heroRole is SCOUT and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 - rule: if strategicalValue is MEDIUM and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGH - rule: if strategicalValue is HIGH and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is MEDIUM and fear is not HIGH then Value is HIGH - rule: if strategicalValue is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if rewardType is NONE then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is not HIGH and heroRole is MAIN then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is HIGH and heroRole is MAIN then Value is LOW - rule: if armyLoss is HIGH and heroRole is SCOUT then Value is LOWEST - rule: if heroRole is SCOUT and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is SCOUT and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH with 0.2 - rule: if heroRole is SCOUT then Value is BITLOW - rule: if goldCost is not NONE and goldReward is NONE and goldPreasure is HIGH then Value is LOWEST - rule: if turn is NOW then Value is LOW with 0.3 - rule: if turn is not NOW then Value is LOW with 0.4 - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and scoutTurnDistance is LOW and armyLoss is LOW then Value is HIGH with 0.5 - rule: if fear is MEDIUM then Value is LOW - rule: if fear is HIGH then Value is LOWEST \ No newline at end of file + rule: if heroRole is MAIN then Value is BASE + rule: if heroRole is SCOUT then Value is BASE + rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if armyLoss is ALL then Value is WORST + rule: if turn is not NOW then Value is BAD with 0.1 + rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST + rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD + rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is LONG then Value is WORST + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD + rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8 + rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is MEDIUM then Value is BAD with 0.2 +RuleBlock: strategicalValue + enabled: true + conjunction: AlgebraicProduct + disjunction: NormalizedSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD + rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7 + rule: if strategicalValue is CRITICAL and heroRole is MAIN then Value is CRITICAL + rule: if strategicalValue is CRITICAL and heroRole is SCOUT then Value is CRITICAL with 0.7 +RuleBlock: armyReward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL +RuleBlock: gold + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is HUGE and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is MEDIUM + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is MEDIUM + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is BITHIGH + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL + rule: if goldReward is LOWEST then Value is SMALL with 0.1 + rule: if goldReward is SMALL then Value is SMALL with 0.2 + rule: if goldReward is MEDIUM then Value is SMALL with 0.5 + rule: if goldReward is BIG then Value is SMALL + rule: if goldReward is HUGE then Value is BITHIGH +RuleBlock: skill reward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH \ No newline at end of file